Queries
Use queries to find entities with specific components. Most game logic creates queries inside plugins, then reuses them from systems.
World methods
These methods are available on every World.
| Method | Description |
|---|---|
world:query | Create a persistent or temporary query from a descriptor. |
world:findArchetypes | Iterate archetypes that contain one component. |
world:query
Creates a query to find entities with specific components.
function World:query(descriptor: queries.QueryDescriptor): QueryParameters:
descriptor: Description of the components to query for.
Returns:
- A query object you can iterate to access matching entities.
Creating queries
Create queries with world:query(), passing a QueryDescriptor.
The include property is a list of components an entity must have to match the query.
-- Find all entities that have the `tecs.builtins.Name` component:
world:query({
include = {tecs.builtins.Name}
})The exclude property is a list of components an entity must not have to match the query.
-- Find all entities that have the `tecs.builtins.Name` component and
-- don't have the `tecs.builtins.Ttl` component.
world:query({
include = {tecs.builtins.Name},
exclude = {tecs.builtins.Ttl}
})The includeAny property is a list of components where an entity must have at least one to match the query. This acts as an OR condition combined with the AND condition of include.
-- Find all entities that have Position AND either Sprite OR Circle
world:query({
include = {Position},
includeAny = {Sprite, Circle}
})You can give queries a name using the name property. This makes it easier to identify queries in debug logs.
world:query({
name = "NameQuery",
include = {tecs.builtins.Name}
})Descriptor reference
The full set of fields accepted by world:query():
| Field | Type | Description |
|---|---|---|
include | {Component} | Archetype must contain all of these components. |
exclude | {Component} | Archetype must contain none of these components. |
includeAny | {Component} | Archetype must contain at least one of these components. Combined with include as AND (include) + OR (includeAny). |
name | string | Human-readable name shown in debug logs and the MCP inspector. Optional. |
temp | boolean | If true, the query is a one-shot snapshot of the currently-matching archetypes. Skips observer registration. Cannot be combined with onEntitiesAdded / onEntitiesRemoved. See Temporary queries. |
groupBy | function | Groups matching archetypes by an integer key for sorted iteration. See Grouping. |
onEntitiesAdded | function | Fires once per contiguous range of entities when they first match the query. See Callbacks. |
onEntitiesRemoved | function | Fires once per contiguous range of entities when they stop matching. See Callbacks. |
Iterating over queries
Iterate a query by calling query:iter() in a generic for loop. Each step returns (archetype, length, entities) for the next non-empty archetype (empty archetypes are skipped). Inside the loop, bind component columns once per archetype and index them by row.
Consider the following query:
local Name <const> = tecs.builtins.Name
local Transform <const> = tecs.builtins.Transform
local query = world:query({
include = {
Name,
Transform
}
})Iterate it:
for archetype, len, entities in query:iter() do
local names = archetype:get(Name)
local transforms = archetype:get(Transform)
for row = 1, len do
local name = names[row]
local transform = transforms[row]
love.graphics.print(name.value, transform.x, transform.y)
end
end- Line
1: gets each non-empty archetype, the number of entities in the archetype, and an array of entity IDs. - Line
2and3: bind component columns witharchetype:get(Component). Teal typesnamesas{Name}. - Line
4: iterates over the entities in the archetype. Each value is the entity's "row", which you use to index into component columns. - Line
5and6: grab components for an entity by indexing into the columns.
Mutating component columns
Use archetype:get(Component) when you only read a column. Use archetype:getMut(Component) when you will mutate values through the returned column. getMut returns the same row-indexed column and marks that component dirty on the archetype, so dirty-tracked consumers such as rendering and snapshots can resync.
local movementQuery = world:query({
include = {Position, Velocity}
})
for archetype, len in movementQuery:iter() do
local positions = archetype:getMut(Position) -- mutated below
local velocities = archetype:get(Velocity) -- read-only
for row = 1, len do
positions[row].x = positions[row].x + velocities[row].x * dt
positions[row].y = positions[row].y + velocities[row].y * dt
end
endget does not protect the column from writes; it simply does not mark the component dirty. Treat getMut as the write-intent API for direct field changes. Use world:set when you need to replace a component value or add a component to an entity.
Mutations during iteration
Structural changes while iterating a query need special handling. world:set, world:remove, world:spawn, world:despawn, and the batch* APIs can move entities between archetypes or resize columns, which would otherwise invalidate the loop.
To make this safe, Tecs defers mutations issued inside a query loop. When for … in query:iter() takes its first step, the world enters a deferred scope; world:set, world:remove, world:spawn, world:despawn, and the batch* APIs all stage during iteration and apply in a drain phase when the loop exits. You can therefore stage structural changes inside the loop body:
for archetype, len, entities in query:iter() do
local healths = archetype:get(Health)
for row = 1, len do
if healths[row].current <= 0 then
world:despawn(entities[row]) -- staged; applies after the loop
end
end
endBreaking out early
Normal exhaustion of for … in query:iter() drains staged mutations automatically. An early break does not; the deferred scope stays open, and your staged writes stay invisible until the next flush point. Call world:commit() after the break to drain right away:
for archetype, len, entities in query:iter() do
if shouldStop then
world:set(entities[1], SomeFlag) -- entities are 1-indexed
break
end
end
world:commit() -- drains the leaked scopeworld:commit() is safe to call unconditionally: it's a no-op when no scope was leaked and nothing was dirtied. If you don't need same-frame visibility, you can skip it; the mutations flush at the next world:update().
Only the archetype-level query loop holds the deferred scope. Breaking out of an inner for row = 1, len row loop has no effect on it.
See Deferred scope on the callbacks page for the full drain and wave model that applies to any deferred region.
Using queries with systems
Create queries outside of systems (usually in plugins), then use them within systems to process entities.
Creating queries in plugins
Create the query once in a plugin and close over it from systems:
-- Create a movement plugin
local function movementPlugin(world: tecs.World)
-- Create the query once in the plugin
local movableQuery = world:query({
name = "MovableEntities",
include = {Position, Velocity}
})
-- Add a system that uses the query
world:addSystem({
name = "MovementSystem",
phase = tecs.phases.FixedUpdate,
run = function(dt: number)
-- Use the query created in the plugin
for arch, len, entities in movableQuery:iter() do
local positions = arch:getMut(Position)
local velocities = arch:get(Velocity)
for row = 1, len do
local id = entities[row]
local pos = positions[row]
local vel = velocities[row]
pos.x = pos.x + vel.vx * dt
pos.y = pos.y + vel.vy * dt
end
end
end
})
end
-- Add the plugin to the world
world:addPlugin(movementPlugin)Temporary queries
By default, queries are persistent: they register as archetype observers and subscribe to new archetypes. For one-shot iteration where you don't need live updates, use temp = true to skip observer registration. Temp queries cannot use onEntitiesAdded or onEntitiesRemoved callbacks.
for archetype, len in world:query({include = {Health}, temp = true}):iter() do
-- iterate once and discard query
endDisabled entities
By default, all queries automatically exclude entities that have the tecs.builtins.Disabled component. This behavior makes it easy to temporarily hide entities without despawning them.
-- This entity won't appear in queries by default
world:spawn(tecs.builtins.Disabled)To find disabled entities in your queries, explicitly include the Disabled component:
-- Find all entities with Position, including disabled ones
local allPositionQuery = world:query({
include = {Position, tecs.builtins.Disabled}
})Ad-hoc archetype lookup
Use world:findArchetypes(component) for simple one-component scans when you do not need a persistent query object. It uses the world's component-to-archetype index and returns an iterator over matching archetypes.
world:findArchetypes
Finds all archetypes that have a specific component.
function World:findArchetypes(component: Component): function(): (Archetype, integer, {integer})Parameters:
component: Component to find.
Returns:
- An iterator over matching archetypes.
for archetype, len, entities in world:findArchetypes(tecs.builtins.Name) do
local names = archetype:get(tecs.builtins.Name)
for row = 1, len do
print(entities[row] .. " has name " .. names[row].value)
end
end