Skip to content

Archetype

An archetype is the fundamental grouping mechanism in the ECS.

  • Entities that share the same set of components share an archetype
  • Archetypes map directly to a table schema definitions.
class Archetype:
    LEGACY_BASE_SCHEMA = pa.schema([
        pa.field("world_id", pa.string(), nullable=False),
        pa.field("run_id", pa.string(), nullable=False),
        pa.field("entity_id", pa.int32(), nullable=False),
        pa.field("tick", pa.int32(), nullable=False),
        pa.field("is_active", pa.bool_(), nullable=False),
    ])

    BASE_SCHEMA = pa.schema([
        *LEGACY_BASE_SCHEMA,
        pa.field("commit_token", pa.string(), nullable=False),
        pa.field("writer_epoch", pa.int64(), nullable=False),
    ])
    PARTITION_KEYS = ["world_id", "run_id", "tick"]

    def __init__(self, components: list["Component"]):
        self.components = components
        self.sig: ArchetypeSignature = self.sig_from_components(components)
        self.name = self.get_name(self.sig)
        self.schema = self.get_archetype_schema(self.sig)

    @staticmethod
    def sig_from_components(components: list["Component"]) -> ArchetypeSignature:
        component_types = [type(c) for c in components]
        return tuple(sorted(component_types, key=lambda t: t.__name__))

    @staticmethod
    def get_name(sig: ArchetypeSignature) -> str:
        combined_schema = Archetype.get_archetype_schema(sig)
        schema_hash = hashlib.sha256(str(combined_schema).encode()).hexdigest()[:16]
        return f"a_{len(sig)}c_s{schema_hash}"

    @staticmethod
    def get_archetype_schema(sig: ArchetypeSignature) -> pa.Schema:
        archetype_schema = Archetype.BASE_SCHEMA
        for component_type in sig:
            component_schema = component_type.get_prefixed_schema()
            archetype_schema = pa.unify_schemas([archetype_schema, component_schema])
        return archetype_schema

    @staticmethod
    def to_row_dict(
        entity_id: int, tick: int, components: list[Component], world_id: str, run_id: str
    ) -> dict[str, Any]:
        row_dict = {
            "world_id": str(world_id), "run_id": str(run_id),
            "entity_id": entity_id, "tick": tick, "is_active": True,
            "commit_token": "", "writer_epoch": 0,
        }
        for c in components:
            row_dict.update({c.get_prefix() + k: v for k, v in c.model_dump().items()})
        return row_dict

Signatures

An ArchetypeSignature is a tuple of component types, sorted alphabetically by class name:

from archetype.core.archetype import Archetype
from archetype.core.interfaces import ArchetypeSignature

# ArchetypeSignature = tuple[type[Component], ...]

sig = Archetype.sig_from_components([Position(x=0, y=0), Velocity(vx=1, vy=0)])
# sig == (Position, Velocity)  -- sorted by __name__

Sorting ensures signatures are deterministic regardless of the order components are passed in.

Naming

Each archetype gets a compact, filesystem-safe table name:

a_2c_s9f3a1b2c4d5e6f7
|  |   |
|  |   +-- SHA-256 hash of the PyArrow schema (first 16 chars)
|  +------ number of component types
+--------- "a" prefix (archetype)

Names are stable within a schema generation -- the same set of component types and storage metadata produces the same name, regardless of component order. Because the schema hash includes the base columns, adding commit identity in v0.3 produced new table ids instead of mutating v0.2 tables in place. Stores may read a legacy table through its old schema-derived name, but new writes use the current name.

The name identifies a physical table, not a unique Python signature. Distinct signatures with the same component count and identical storage schemas map to the same name. Empty tag components contribute no fields, so two one-tag signatures are the common edge case. AsyncWorld interns one canonical signature per table name to prevent processing that shared table twice. See System Execution for the executable example and processor-matching consequences.

name = Archetype.get_name(sig)  # "a_2c_s9f3a1b2c4d5e6f7"

Schema

An archetype schema combines a base set of housekeeping columns with prefixed component fields:

schema = Archetype.get_archetype_schema(sig)

Base columns (present in every archetype):

Column Type Description
world_id string Which world this entity belongs to
run_id string Which run produced this row
entity_id int32 Unique entity identifier
tick int32 Simulation tick when this row was written
is_active bool Whether the entity is alive
commit_token string Identity of the tick-commit attempt that wrote the row
writer_epoch int64 Fenced writer epoch for that attempt

Component columns are prefixed with the lowercase class name. A Position(x=5, y=10) component adds columns position__x and position__y.

The full schema for an archetype with (Health, Position) would be:

world_id | run_id | entity_id | tick | is_active | commit_token | writer_epoch | health__current | health__max_hp | position__x | position__y

The commit columns are raw ledger metadata. Raw archetype reads expose them so visibility can be inspected; component projections omit them and return the five legacy housekeeping columns plus the requested component fields. A tick is visible only when its commit token appears in a published manifest. See Atomic Tick Visibility.

Partition Keys

Archetypes are partitioned by ["world_id", "run_id", "tick"] for efficient storage filtering. This lets the querier skip irrelevant partitions when reading a specific world at a specific tick.

Composing Signatures

Add or remove component types from an existing signature:

# Add a component type
new_sig = Archetype.add_components(sig, [Health])
# (Health, Position, Velocity)

# Remove a component type
new_sig = Archetype.remove_components(sig, [Velocity])
# (Position,)

Both return a new sorted tuple -- signatures are immutable.

Row Serialization

Convert an entity's components to a flat dictionary for storage:

row = Archetype.to_row_dict(
    entity_id=1,
    tick=0,
    components=[Position(x=5, y=10), Velocity(vx=1, vy=0)],
    world_id="abc-123",
    run_id="run-001",
)
# {
#     "world_id": "abc-123",
#     "run_id": "run-001",
#     "entity_id": 1,
#     "tick": 0,
#     "is_active": True,
#     "commit_token": "",
#     "writer_epoch": 0,
#     "position__x": 5.0,
#     "position__y": 10.0,
#     "velocity__vx": 1.0,
#     "velocity__vy": 0.0,
# }

to_row_dict() uses the uncoordinated epoch-0 placeholders shown above. During a coordinated tick, the updater replaces them with the tick's real commit token and writer epoch before append.

Entities

An entity is an integer ID (entity_id). It carries no logic — its state is the union of its component fields. The world tracks each entity's current archetype signature via its entity2sig mapping.

When you add or remove components from an entity, it migrates to a different archetype: the old row is marked inactive, and a new row is spawned in the target archetype's table with the updated component set. See Worlds -- Entity Migration for the full algorithm.

Further Reading

  • Components -- field types, Arrow serialization, the column prefixing contract
  • System Execution -- how signatures drive the subset rule for processor matching
  • Worlds -- tick lifecycle, spawn/despawn caches, entity migration
  • Stores -- how archetype tables are persisted
  • Atomic Tick Visibility -- commit identity, manifests, and fencing

Source Reference

The archetype system is defined in packages/archetype-ecs/src/archetype/core/archetype.py.