Entity & Archetype Model
Structured 64-bit entity identity, C# class-hierarchy archetypes, and typed zero-copy component handles — the schema backbone of every other ECS feature.
Status: ✅ Implemented · Visibility: Public · Level: 🟢 Start Here · Category: Ecs
🎯 What it solves
A flat EntityId with independent per-component tables gives no guarantee that a "Factory" entity actually has the components a Factory needs, and forces one B+Tree lookup per component just to read an entity's data. The Entity & Archetype Model fixes both: an archetype declares an entity's complete component set up front (engine-enforced, not caller-maintained), and a single per-archetype lookup resolves every component slot at once. It also removes a whole class of bugs around stale references — entity identity here can never collide with a previously deleted entity occupying the same slot.
⚙️ How it works (in brief)
Archetypes are plain C# classes in a single-inheritance hierarchy, each declaring its components as static readonly Comp<T> fields; a derived archetype (e.g. House : Archetype<House, Building>) inherits its parent's components at stable slot indices and adds its own. Every EntityId packs a 48-bit monotonic EntityKey and a 16-bit per-DB archetype routing id into one ulong — the routing id routes to the entity's archetype, the EntityKey is the lookup key within it. Because keys are never recycled, a stale EntityId simply misses on lookup; no version/generation field is needed. Opening an entity (Open/OpenMut) pays one lookup and returns an EntityRef that amortizes it across all subsequent Read/Write calls for that entity. Comp<T> is the typed handle used both to declare a component on an archetype and to address it later — slot resolution is O(1) and type-checked by the compiler.
💻 Usage
// ─── Component types: plain unmanaged structs ───
[Component("Game.Placement", 1)]
public struct Placement
{
public float X, Y, Z;
}
[Component("Game.HouseData", 1)]
public struct HouseData
{
public int Residents;
}
// ─── Archetype hierarchy ───
[Archetype]
public class Building : Archetype<Building>
{
public static readonly Comp<Placement> Placement = Register<Placement>();
}
[Archetype]
public class House : Archetype<House, Building>
{
public static readonly Comp<HouseData> HouseInfo = Register<HouseData>();
}
// ─── Spawn, open, and access ───
using var t = dbe.CreateQuickTransaction();
var id = t.Spawn<House>(
Building.Placement.Set(new Placement { X = 1, Y = 0, Z = 2 }),
House.HouseInfo.Set(new HouseData { Residents = 4 }));
var house = t.Open(id);
ref readonly var placement = ref house.Read(Building.Placement); // inherited slot, zero-copy
ref readonly var info = ref house.Read(House.HouseInfo);
t.Commit();
| Constraint | Limit | Effect |
|---|---|---|
| Components per archetype | 16 | [Archetype] registration throws if exceeded |
| Registered archetypes | up to 65,536 per database (16-bit per-DB routing id) | Identity is the CLR type name (or [Archetype(Name = "...")]); the engine auto-assigns the catalog + per-DB routing ids |
ComponentValue inline payload |
112 bytes | Larger components use incremental Spawn (Spawn + OpenMut + Write) |
| Archetype inheritance | single parent only | No diamond inheritance — enforced at registration |
⚠️ Guarantees & limits
- Schema enforcement at creation:
Spawn<T>always allocates every component slot of the archetype (omitted ones zero-initialized and disabled) — there is no "entity missing a required component" error class. - O(1) entity→component routing: one lookup per
Open/OpenMutresolves all component locations for that entity;Read/Writeafterward are direct, ~1-5ns for SingleVersion/Transient, ~50-100ns for Versioned (MVCC revision walk). - Zero-copy access:
Readreturnsref readonly T,Writereturnsref T— no struct copies into or out of storage. - Stale references always miss, safely:
EntityKeyis monotonic and never recycled, so there's no version field and no ABA hazard — a heldEntityIdfor a destroyed entity simply fails to resolve. - Polymorphic slot stability: a component inherited from a parent archetype sits at the same slot index in every descendant, so handles like
Building.Placementwork uniformly acrossBuilding,House, and any other subclass. - Archetype identity is the type name, routing is per-DB: an archetype is identified by its CLR type name (override with
[Archetype(Name = "...")]); the engine auto-assigns a per-process catalog id and a per-DB routing id (persisted inArchetypeR1and re-matched by name on reopen). The routing id is embedded in every persistedEntityId. Renaming stays safe —[Archetype(Name = "New", PreviousName = "Old")]re-matches the old name on reopen and carries the data forward. - Component removal is non-structural: an archetype's component set is fixed after spawn — "removing" a component means disabling it (O(1) bit), not migrating the entity to a different archetype.
🧪 Tests
- EntityIdTests —
EntityId64-bit packing (EntityKey/ per-DB routing id), null semantics, equality, raw-value round-trip - ArchetypeRegistrationTests — archetype registration, parent-first slot inheritance, component-type dedup across archetypes, subtree building
🔗 Related
- Sibling: Entity Lifecycle & CRUD API —
Open/OpenMut/Spawnresolve entities through the archetype model described here - Sibling: Query System (EcsQuery) — Tier 1
ArchetypeMaskconstraints are built directly on this model