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, andentities[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 orarchetype:getMut(Component)before writes, then index them by row.
This row/column layout is why query loops bind columns once per archetype:
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
endArchetype properties
| Name | Type | Description |
|---|---|---|
id | integer | Unique identifier of the archetype. |
entities | DoubleArray | Entity 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.
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.
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
endset
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.
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:
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
endforEachRelationship
Iterates all relationship instances of the given relationship container for an entity. Only concrete relationship instances are visited; the container itself is not included.
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:
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.
function Archetype:getFirstRelationship<T is Relationship>(relationshipContainer: T, row: integer): TParameters:
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
nilif none exists.
Example:
local ChildOf = tecs.builtins.ChildOf
local childOf: ChildOf = archetype:getFirstRelationship(ChildOf, 5)
if childOf then
print("parent id:", childOf.target)
endmarkComponentDirty / markAllComponentsDirty
Explicit dirty markers for cases where mutation happens through a path the framework can't intercept.
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.
function Archetype:isComponentDirty(component: Component): boolean
function Archetype:anyComponentDirty(): boolean
function Archetype:dirtyComponents(): function(): ComponentBits 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.
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).