Skip to content

Queries

Queries match archetype tables. A matching query returns the entities in each table as a batch; component fields are exposed through ecs_field() in C or as typed callback arguments in C++.

ecs_query_id_t moving = ecs_query({
.components = { ecs_inout(Position), ecs_in(Velocity) },
});

The C descriptor contains up to ECS_QUERY_TERM_CAPACITY component terms and up to ECS_QUERY_RELATION_CAPACITY relation terms. Terms are zero-terminated inside their fixed-size descriptor arrays. A query may contain component terms, relation terms, an is_a target, or an order comparator.

The C++ builder creates a temporary query for each() and first(). Use build_handle() when a query must be kept and iterated repeatedly.

Term Meaning
ecs_in(T) T must exist and is returned by ecs_field() for reading.
ecs_out(T) T must exist locally and is returned for writing.
ecs_inout(T) T must exist locally and is returned for reading and writing.
ecs_in_optional(T) T is returned when present; the field is NULL for tables without it.
ecs_inout_optional(T) Local T is optional and writable when present.
ecs_filter(T) T must exist but does not create a field.
ecs_not(T) T must not exist and does not create a field.
ecs_up(T, Relation) Read the nearest inherited T through an acyclic ByTarget relation.
ecs_up_optional(T, Relation) Same lookup, but the inherited field may be absent.

Only in, out, inout, and their optional variants create field indexes. Fields are numbered in declaration order; ecs_filter and ecs_not do not consume an index. ecs_up fields are also returned by ecs_field() and may be shared with an ancestor.

Queries, systems, and observers exclude Disabled and Abstract by default. Mention either component explicitly when it must be included:

ecs_query_id_t disabled = ecs_query({
.components = { ecs_in(Position), ecs_filter(Disabled) },
});

Relations are separate from components. Register a relation, then use relation terms to match its presence, target, or depth:

ecs_query_id_t members = ecs_query({
.components = { ecs_in(Position) },
.relations = { ecs_to(GroupOf, group) },
});
Term Meaning
ecs_rel(Relation) / with_relation<Relation>() The source has the relation.
ecs_rel_opt(Relation) The relation is optional and does not exclude sources without it.
ecs_not_rel(Relation) The source must not have the relation.
ecs_to(Relation, target) / to<Relation>(target) A ByTarget relation must point exactly to target.
ecs_depth(Relation, depth) / depth<Relation>(depth) A ByDepth relation must have this depth.

ecs_to and ecs_depth validate that the relation uses the matching storage mode. Relation terms are filters; they do not create component fields.

For a Dense or ByDepth relation, ecs_targets() returns contiguous ecs_relation_target_t records for the current batch. For a ByTarget relation, ecs_target_shared() returns the target shared by the batch:

while (ecs_iter_next(&it)) {
const ecs_relation_target_t *targets = ecs_targets(&it, GroupOf);
for (uint32_t row = 0; row < it.count; row++) {
ecs_entity_t target = targets[row].entity;
/* use it.entities[row] and target */
}
}

Use is_a to restrict matches to entities that inherit from a base entity. The match is transitive through the inheritance chain:

ecs_query_id_t enemies = ecs_query({
.components = { ecs_in(Position) },
.is_a = enemy_base,
});

ecs_order_by_depth(Relation) orders matching tables by relation depth. It is valid only for an acyclic ByDepth relation. ecs_order_by_target(Relation) orders ByTarget tables by their target handle. Both orderings are stable for tables with equal keys; neither ordering defines an order between entities in the same table:

ecs_query_id_t hierarchy = ecs_query({
.components = { ecs_in(Position) },
.relations = { ecs_rel(ChildOf) },
.order_by = ecs_order_by_depth(ChildOf),
});
ecs_iter_t it = ecs_query_iter(moving);
while (ecs_iter_next(&it)) {
Position *positions = ecs_field(&it, 0);
Velocity *velocities = ecs_field(&it, 1);
for (uint32_t i = 0; i < it.count; i++) {
positions[i].x += velocities[i].x;
}
}

ecs_iter_next() advances to the next non-empty table batch. it.count is the number of entities in that batch. ecs_query_count() returns the current number of matching entities without creating an iterator.

ecs_field_kind() distinguishes EcsFieldOwned, EcsFieldShared, and EcsFieldNone for optional fields. A shared field is inherited or otherwise stored outside the current table; its pointer must be treated as read-only.

Optional fields are batch-wide: the pointer is either a valid array for every entity in the batch or NULL for the entire batch.

ecs_query_each() is useful for setup, tests, tools, and short one-off scans:

ecs_query_each(it, i, ecs_in(Position)) {
Position *positions = ecs_field(&it, 0);
positions[i].x += 1.0f;
}

The macro creates and destroys a temporary C query. An id returned by ecs_query() or ecs_query_init() must be released with ecs_query_fini(); in C++, each() and first() use temporary queries while query_handle is a move-only RAII owner for repeated iteration:

The C++ example above uses the same persistent query pattern through its move-only query_handle.

Do not retain component or field pointers across an operation that can migrate an entity between tables. Query ids remain valid as tables are created and grown; the query cache refreshes its field pointers when table storage moves.

Resources can be declared as scheduler metadata in their own namespace:

ecs_query({
.components = {
ecs_inout(Position),
ecs_in(Velocity),
},
.resources = {
ecs_in(Time),
ecs_inout(Gravity),
},
});

ECS_QUERY_TERM_CAPACITY limits component terms and ECS_QUERY_RESOURCE_CAPACITY limits resource accesses. EcsIn is a scheduler read; EcsOut and EcsInOut are scheduler writes. Resource presence is never a query condition: resources do not affect matching and produce no fields. Optional, filter, not, and up modes are reserved for components. A resources-only query has a cache but zero entity batches.