Queries#

A query tracks archetypes whose component signatures match one descriptor:

local Transform2D = tecs.ecs.Transform2D
local movers = world:newQuery({
    include = {Transform2D, Velocity},
    exclude = {Frozen, tecs.ecs.Disabled, tecs.ecs.Paused},
})

for archetype, length in movers:iter() do
    local entities = archetype.entities
    local transforms = assert(archetype:getMut(Transform2D))
    local velocities = assert(archetype:get(Velocity))
    unsafe do
        for row = 1, length as integer do
            transforms[row].x = transforms[row].x + velocities[row].x * dt
            print(entities[row])
        end
    end
end

include requires every listed component. exclude rejects every archetype with a listed component. Build the descriptor before creating the query. Include and exclude constraints are fixed for its lifetime.

Archetype iteration#

query:iter() yields each non-empty matching archetype, and its row count. The archetype exposes its entity-ID column as entities. Tecs owns the entity-ID column; callers treat it as read-only.

Bind each component column once per archetype. archetype:get gives a read-only access path. archetype:getMut gives caller-writable values and marks that component dirty:

for archetype, length in movers:iter() do
    local transforms = assert(archetype:getMut(Transform2D))
    local velocities = assert(archetype:get(Velocity))
    unsafe do
        for row = 1, length as integer do
            local transform = transforms[row]
            local velocity = velocities[row]
            transform.x = transform.x + velocity.x * dt
            transform.y = transform.y + velocity.y * dt
        end
    end
end

Writing through get may change a value without dirtying it. Use getMut for unconditional writes. For a conditional write, read through get and call archetype:markComponentDirty(Component) only when the write occurs.

Use query:count() to count matches without visiting rows.

Iteration supports nesting, including two loops over the same query. Iterators own traversal state only; they do not control structural transaction lifetime. Persistent queries retain active-only lists. The iterator methods return the generic-for triple: a reusable function, query-owned state and an initial cursor. Normal loops allocate no closure. For manual stepping, retain all three:

local step, state, cursor = movers:iter()
local candidate, count, entities = step(state, cursor)
-- The first return becomes the next cursor.
if candidate ~= nil then
    candidate, count, entities = step(state, candidate)
end

Structural changes#

Structural calls such as spawn, despawn, component-adding set, and remove always stage. Iteration continues over the committed rows and the pipeline publishes at its next declared barrier:

local expiring = world:newQuery({
    include = {tecs.ecs.TTL},
    exclude = {tecs.ecs.Disabled, tecs.ecs.Paused},
})

for archetype, length in expiring:iter() do
    local entities = archetype.entities
    local ttls = assert(archetype:getMut(tecs.ecs.TTL))
    unsafe do
        for row = 1, length as integer do
            ttls[row].remaining = ttls[row].remaining - dt
            if ttls[row].remaining <= 0 then
                world:despawn(entities[row])
            end
        end
    end
end

Iterator exhaustion does not publish those changes. The world guide defines their visibility and ordering.

Early exit#

An early break or return is safe because iteration owns no transaction scope or resource that needs cleanup:

for archetype, _length in query:iter() do
    local entities = archetype.entities
    if matchesSelection(archetype) then
        selected = entities[1]
        break
    end
end

Nested and interleaved loops keep independent traversal state, including multiple loops over the same query.

Persistent queries#

Queries subscribe to new archetypes and remain suitable for systems that run every frame. Build them once during plugin setup. Do not create a new query inside every run call.

Disabled entities#

Queries exclude tecs.ecs.Disabled automatically unless their include list explicitly requests it. Rendering also excludes this tag.

local movement = world:newQuery({
    include = {tecs.ecs.Transform2D, Velocity},
    exclude = {tecs.ecs.Disabled, tecs.ecs.Paused},
})

Paused entities#

Set type = "logic" to exclude tecs.ecs.Paused automatically, or add an explicit exclusion to an ordinary query. Render extraction keeps paused entities visible. Listing the tag in include selects paused entities for inspection.