On this page
Queries
A query tracks archetypes whose component signatures match one descriptor:
local Transform2D <const> = tecs.Transform2D
local movers <const> = world:newQuery({
name = "game.Movers",
include = {Transform2D, Velocity},
exclude = {Frozen},
type = "logic",
})
for archetype, length, entities in movers:iter() do
local transforms <const> = archetype:getMut(Transform2D)
local velocities <const> = archetype:get(Velocity)
for row = 1, length do
transforms[row].x = transforms[row].x + velocities[row].x * dt
print(entities[row])
end
endinclude requires every listed component. exclude rejects every archetype with a listed component. includeAny adds an OR group:
local drawn <const> = world:newQuery({
include = {tecs.Transform2D, tecs.gfx.Renderable2D},
includeAny = {tecs.gfx.Sprite, tecs.gfx.Material},
type = "render",
})A query exposes its descriptor for inspection. Tecs owns the compiled masks, subscriptions, and grouping state; callers must treat the descriptor as read-only after construction. Changing it does not rebuild the query.
Archetype iteration
query:iter() yields each non-empty matching archetype, its row count, and its entity-ID column. 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 <const> = archetype:getMut(Transform2D)
local velocities <const> = archetype:get(Velocity)
for row = 1, length do
local transform <const> = transforms[row]
local velocity <const> = velocities[row]
transform.x = transform.x + velocity.x * dt
transform.y = transform.y + velocity.y * dt
end
endLuaJIT cannot enforce const cdata, so writing through get may change memory 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.
query:count() sums archetype lengths without visiting entity rows.
Iteration supports nesting, including two loops over the same query. Iterators own traversal state only; they do not control structural transaction lifetime.
Structural changes
Structural calls such as spawn, despawn, component-adding set, remove, and batch operations always stage. Iteration continues over the committed rows and the pipeline publishes at its next declared barrier:
local expiring <const> = world:newQuery({
include = {tecs.ecs.TTL},
type = "logic",
})
for archetype, length, entities in expiring:iter() do
local ttls <const> = archetype:getMut(tecs.ecs.TTL)
for row = 1, length do
ttls[row].remaining = ttls[row].remaining - dt
if ttls[row].remaining <= 0 then
world:despawn(entities[row])
end
end
endIterator exhaustion does not publish those changes. The mutation model 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, entities in query:iter() do
if matchesSelection(archetype) then
selected = entities[1]
break
end
endThe same rule applies to groups() and group(id). Nested and interleaved loops keep independent traversal state, including multiple loops over the same query.
Persistent and temporary queries
Persistent queries subscribe to new archetypes and remain suitable for systems that run every frame. Build them once during plugin setup.
temp = true takes a one-shot view of the current archetype set without registering observers:
for archetype, length in world:newQuery({
include = {tecs.gfx.PointLight2D},
temp = true,
}):iter() do
inspectLights(archetype, length)
endA temporary query cannot define onEntitiesAdded or onEntitiesRemoved. Query callbacks cover persistent match-set reactions. Grouping sorts matching archetypes under integer keys.
Disabled entities
Every query excludes tecs.ecs.Disabled unless include explicitly names the tag. Renderer queries follow the same rule.
local disabledRenderables <const> = world:newQuery({
include = {
tecs.Transform2D,
tecs.gfx.Renderable2D,
tecs.ecs.Disabled,
},
})Paused entities
type = "logic" excludes tecs.ecs.Paused. type = "render" records that paused entities should continue to match. An omitted type applies no pause filter.
local movement <const> = world:newQuery({
include = {tecs.Transform2D, Velocity},
type = "logic",
})
local sprites <const> = world:newQuery({
include = {tecs.Transform2D, tecs.gfx.Sprite},
type = "render",
})Explicitly including Paused overrides the filter. Listing it under exclude matches the logic behavior.
One-component archetype scans
world:findArchetypes(Component) walks the component-to-archetype index without constructing a query:
for archetype, length, entities in world:findArchetypes(
tecs.gfx.PointLight2D
) do
local lights <const> = archetype:get(tecs.gfx.PointLight2D)
for row = 1, length do
print(entities[row], lights[row].radius)
end
endThis iterator reads the live archetype index directly. Do not make structural changes while it runs; call it only where the surrounding scheduler contract keeps publication out of the traversal.
Module contents
Submodules
| Submodule | Description |
|---|---|
Query callbacks |
Batch onEntitiesAdded and onEntitiesRemoved query hooks with row ranges and deferred-drain semantics |
Query grouping |
Grouping matching archetypes by integer key with groupBy, groups, group, getGroup, and getGroupCount |