Skip to content

Entities and Components

Entities

An entity is an EntityId — a uint wrapped in a lightweight struct. It carries no data itself. It is a key into the ECS world’s component stores.

// Create an entity
EntityId enemy = world.CreateEntity();
// Destroy an entity and all its components
world.DestroyEntity(enemy);
// Check if an entity still exists
bool alive = world.IsAlive(enemy);

Entities are created and destroyed frequently at runtime. The world maintains a free-list of recycled IDs with a generation counter to prevent stale-ID access bugs.

Defining a component

Components are value types (struct) with no methods beyond constructors. They are pure data.

using Softfire.Engine.Core;
public struct HealthComponent : IComponent
{
public float Current;
public float Max;
public bool IsInvincible;
public HealthComponent(float max)
{
Max = max;
Current = max;
IsInvincible = false;
}
}

IComponent is a marker interface. It carries no methods — it simply tells the ECS world how to store the component.

Adding and removing components

// Add
world.Add(enemy, new HealthComponent(100f));
world.Add(enemy, new TransformComponent { Position = new Vector2(300, 200) });
// Get a reference (writable ref to the backing store)
ref HealthComponent hp = ref world.Get<HealthComponent>(enemy);
hp.Current -= 10f;
// Check existence
bool hasHealth = world.Has<HealthComponent>(enemy);
// Remove
world.Remove<HealthComponent>(enemy);

world.Get<T>() returns a ref to the component’s memory in the backing store. This avoids copying struct data on every read. Modifying the returned ref modifies the component in place.

Writing a system

Systems inherit from SystemBase and override Update. They declare which components they read or write using the Query builder.

using Softfire.Engine.Core;
using Softfire.Engine.Core.Systems;
public class DamageFlashSystem : SystemBase
{
private readonly Query<TransformComponent, HealthComponent, SpriteComponent> _query;
public DamageFlashSystem(World world) : base(world)
{
_query = world.Query<TransformComponent, HealthComponent, SpriteComponent>();
}
public override void Update(GameTime gameTime)
{
foreach (var (entity, transform, health, sprite) in _query)
{
if (health.Current <= 0)
{
sprite.ColorTint = Color.Red;
}
}
}
}

Register the system in Initialize:

Systems.Register<DamageFlashSystem>(priority: 50);

Lower priority values run earlier. The default priority is 100.

Parallel system scheduling

Systems can be marked [Parallel] to allow the scheduler to dispatch them concurrently with other parallel systems in the same frame. The scheduler verifies at registration time that concurrent parallel systems do not write to overlapping component types. If they do, registration throws a SystemScheduleConflictException at startup — not at runtime.

[Parallel]
public class ParticleUpdateSystem : SystemBase { ... }
[Parallel]
public class AudioListenerUpdateSystem : SystemBase { ... }

These two systems write to different component types, so they will run concurrently. The scheduler groups compatible parallel systems and dispatches them as a Parallel.ForEach batch.

Transform hierarchy

Entities can be parented to other entities. The TransformComponent carries a Parent field. When the parent transforms, child world-space positions update accordingly.

EntityId child = world.CreateEntity();
world.Add(child, new TransformComponent
{
Position = new Vector2(0, -20), // 20 units above parent
Parent = parent,
});

The TransformSystem resolves the hierarchy each frame, computing world-space transforms from local-space values. You always write to local space; read from WorldTransform when you need the resolved position.

Prefabs

A prefab is a reusable entity template defined in a .prefab JSON asset. It describes the set of components and their default values. Prefabs are authored in the editor but are plain JSON files — they are tracked in source control and compiled by the MGCB PrefabProcessor.

Creating a prefab from a scene selection:

In the editor, select one or more entities in the scene, right-click, and choose Save as Prefab. The editor serialises the entity’s components to a .prefab file in your Content/Prefabs/ directory.

Instantiating from code:

var prefabDef = Content.Load<PrefabDefinition>("Prefabs/Enemy_Goblin");
EntityId goblin = world.Instantiate(prefabDef, position: new Vector2(500, 300));

Per-instance overrides:

When you instantiate a prefab, you can pass an override delegate to change specific component values without modifying the source prefab:

EntityId eliteGoblin = world.Instantiate(prefabDef,
position: new Vector2(500, 300),
overrides: components =>
{
ref var hp = ref components.Get<HealthComponent>();
hp.Max = 200f;
hp.Current = 200f;
});

Overrides are applied after all default component values are set, so you only need to touch the fields that differ.