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++.
Create a query
Section titled “Create a query”ecs_query_id_t moving = ecs_query({ .components = { ecs_inout(Position), ecs_in(Velocity) },});auto moving = ecs::query() .require<Position>() .require<Velocity>() .build_handle();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.
Component terms
Section titled “Component terms”| 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) },});auto disabled = ecs::query() .require<Position>() .require<Disabled>() .build_handle();Relation terms
Section titled “Relation terms”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) },});auto members = ecs::query() .require<Position>() .to<GroupOf>(group) .build_handle();| 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 */ }}auto members = ecs::query().to<GroupOf>(group).build_handle();members.each([](ecs::entity member) { auto target = member.target<GroupOf>();});Hierarchies and ordering
Section titled “Hierarchies and ordering”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,});auto enemies = ecs::query() .is_a(enemy_base) .require<Position>();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),});auto hierarchy = ecs::query() .with_relation<ChildOf>() .order_by_depth<ChildOf>() .build_handle();Iterate
Section titled “Iterate”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; }}moving.each([](Position &position, const Velocity &velocity) { position.x += velocity.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.
Temporary and persistent queries
Section titled “Temporary and persistent queries”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;}auto moving = ecs::query().require<Position>().build_handle();moving.each([](Position &position) { position.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.
Resource access
Section titled “Resource access”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.