Skip to content

Archetypes

An archetype is a storage group for entities with the same component signature. Each entity belongs to exactly one archetype at a time. When an entity gains or loses a component, it moves to another archetype.

Most code reaches archetypes through queries. A query finds the archetypes whose signatures match its descriptor, then each loop step gives you an archetype, a row count, and the entity IDs for those rows.

Entities, rows, and columns

Archetypes store entities by row. The same row index addresses the entity ID and each of that entity's component values:

  • Entity IDs live in archetype.entities. The array is 1-based, and entities[0] stores the current length.
  • Rows are the current positions inside an archetype. Use rows while iterating; don't cache them as stable identifiers because despawns and archetype moves can reorder rows.
  • Columns store component values for every row in the archetype. Bind columns with archetype:get(Component) for reads or archetype:getMut(Component) before writes, then index them by row.

This row/column layout is why query loops bind columns once per archetype:

teal
for archetype, len, entities in query:iter() do
    local transforms = archetype:get(Transform)
    local sprites = archetype:get(Sprite)

    for row = 1, len do
        local entity = entities[row]
        local transform = transforms[row]
        local sprite = sprites[row]
        -- ...
    end
end

Archetype properties

NameTypeDescription
idintegerUnique identifier of the archetype.
entitiesDoubleArrayEntity IDs by row. entities[0] is the length; valid rows are 1..entities[0].
componentList{Component}Component types in this archetype's fixed signature. Use #componentList / ipairs to inspect the signature.
columns{Component: {Component}}Raw data columns. Treat as read-only; prefer get / getMut so sparse relationship proxies and dirty marking work correctly.

Archetype methods

get / getMut

Column access by component type. Both methods return the row-indexed column for a component when one exists. The difference is dirty marking.

teal
function Archetype:get<T is Component>(component: T): {T}
function Archetype:getMut<T is Component>(component: T): {T}

Use get when you only read values. Use getMut when you will mutate values through the returned column. getMut marks the component dirty on the archetype so incremental-sync consumers such as renderer shadow buffers and snapshots can resync.

get does not protect the column from writes; it simply does not mark the component dirty.

teal
for archetype, len in query:iter() do
    local transforms = archetype:getMut(Transform)
    local velocities = archetype:get(Velocity)  -- read-only
    for row = 1, len do
        transforms[row].x = transforms[row].x + velocities[row].x * dt
    end
end

set

Replaces a component value at a row and marks the component dirty on the archetype. No archetype transition happens; the row stays where it is. The component must already be present on the archetype. Use world:set when you need to add a component to an entity or move it to another archetype.

teal
function Archetype:set<C is Component>(row: integer, value: C)

Parameters:

  • row: The 1-based row position of the entity.
  • value: The new component instance.

Example:

teal
for archetype, len in query:iter() do
    for row = 1, len do
        -- Replace the component and mark dirty in one call.
        archetype:set(row, Color(1, 0, 0, 1))
    end
end

forEachRelationship

Iterates all relationship instances of the given relationship container for an entity. Only concrete relationship instances are visited; the container itself is not included.

teal
function Archetype:forEachRelationship<T is Relationship>(
    relationshipContainer: T,
    row: integer,
    callback: function(T)
)

Parameters:

  • relationshipContainer: The relationship container to iterate.
  • row: The 1-based row position of the entity in this archetype.
  • callback: Called with each relationship instance of this type.

Example:

teal
local Likes = tecs.newRelationship({name = "Likes"})

archetype:forEachRelationship(Likes, 5, function(likes: Likes)
    print("Entity likes", likes.target)
end)

getFirstRelationship

Gets the first relationship instance of the given relationship container for an entity, if any. For exclusive relationships (e.g. ChildOf) this is the single instance.

teal
function Archetype:getFirstRelationship<T is Relationship>(relationshipContainer: T, row: integer): T

Parameters:

  • relationshipContainer: The relationship container to retrieve a relationship from.
  • row: The 1-based row position of the entity in this archetype.

Returns:

  • The first relationship of this type for the entity, or nil if none exists.

Example:

teal
local ChildOf = tecs.builtins.ChildOf

local childOf: ChildOf = archetype:getFirstRelationship(ChildOf, 5)
if childOf then
    print("parent id:", childOf.target)
end

markComponentDirty / markAllComponentsDirty

Explicit dirty markers for cases where mutation happens through a path the framework can't intercept.

teal
function Archetype:markComponentDirty(component: Component)
function Archetype:markAllComponentsDirty()

getMut, world:set, archetype:set, spawn, and archetype move-in / swap-pop all mark dirty internally. Reach for the explicit markers when none of those apply.

isComponentDirty / anyComponentDirty / dirtyComponents

Read dirty state.

teal
function Archetype:isComponentDirty(component: Component): boolean
function Archetype:anyComponentDirty(): boolean
function Archetype:dirtyComponents(): function(): Component

Bits are cleared automatically at the end of each world:update.

clearDirtyComponents

Clear every component-dirty bit on this archetype. The world's end-of-update loop calls this for each archetype that touched the dirty set during the frame; callers rarely invoke it directly.

teal
function Archetype:clearDirtyComponents()

Observing entity lifecycle

To receive callbacks when entities join or leave an archetype's match set, attach query callbacks (onEntitiesAdded / onEntitiesRemoved) to a world:query(...). Queries handle archetype discovery, filtering, and observer registration for you.

To discover new archetypes as they're created, observe the ArchetypeCreated event on the world (address 0).