Skip to content

Queries

Queries match archetype tables and return component arrays in batches.

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

The terms array is a zero-terminated list of component terms.

TermMeaning
ecs_in(T)T must exist locally or through inheritance and is returned by ecs_field() for reading.
ecs_out(T)T must exist locally and is returned by ecs_field() for writing.
ecs_inout(T)T must exist locally and is returned by ecs_field() for reading and writing.
ecs_in_optional(T)T may be absent and is returned as a nullable read field.
ecs_inout_optional(T)Local T may be absent and is returned as a nullable read/write field.
ecs_filter(T)T must exist but is not returned.
ecs_not(T)T must not exist.

Current descriptor limits:

FieldMaximum entries
terms16

Queries exclude entities with the built-in Disabled component by default. This implicit ecs_not(Disabled) term is added only when the query does not already mention Disabled.

Use is_a when a query should only match entities that inherit from a specific base entity:

ecs_query_id_t enemies = ecs_query({
    .is_a = enemy_base,
    .terms = { ecs_in(Position) },
});
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;
        positions[i].y += velocities[i].y;
    }
}

ecs_iter_next() advances to the next non-empty batch. it.count is the number of entities in the current batch.

ecs_field(&it, index) returns component arrays for ecs_in, ecs_out, ecs_inout, ecs_in_optional, and ecs_inout_optional terms in declaration order. ecs_filter and ecs_not do not create field indexes.

In the C++ API, ecs::res<T> and ecs::res<const T> callback parameters are resources, not query terms. They are resolved from the world before table iteration and do not consume field indexes:

ecs::query().each([](ecs::res<const Time> time,
Position &position,
const Velocity &velocity) {
position.x += velocity.x * time->dt;
});

Use ecs_filter when a component must exist but does not need to be returned:

ecs_query_id_t visible_positions = ecs_query({
    .terms = { ecs_inout(Position), ecs_filter(Visible) },
});

Use ecs_not to skip entities with a component:

ecs_query_id_t active_positions = ecs_query({
    .terms = { ecs_inout(Position), ecs_not(Disabled) },
});

The ecs_not(Disabled) term is implicit for normal queries, so the previous example can usually be written as:

ecs_query_id_t active_positions = ecs_query({
    .terms = { ecs_inout(Position) },
});

To match disabled entities, mention Disabled explicitly:

ecs_query_id_t disabled_positions = ecs_query({
    .terms = { ecs_inout(Position), ecs_filter(Disabled) },
});

Optional terms let one query handle tables where a component may or may not be present:

ecs_query_id_t q = ecs_query({
    .terms = { ecs_inout(Position), ecs_in_optional(Velocity) },
});

ecs_iter_t it = ecs_query_iter(q);
while (ecs_iter_next(&it)) {
    Position *positions = ecs_field(&it, 0);
    const Velocity *velocities = ecs_field(&it, 1);

    for (uint32_t i = 0; i < it.count; i++) {
        if (velocities != NULL) {
            positions[i].x += velocities[i].x;
        }
    }
}

The optional field pointer is either a component array for the whole batch or NULL for the whole batch. It does not vary per entity inside the same batch.

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

ecs_query_each(it, i, ecs_in(Position)) {
    const Position *positions = ecs_field(&it, 0);
    (void)positions[i];
}

It creates and destroys a temporary query. For repeated frame work, create a persistent query once and iterate it with ecs_query_iter().

ecs_query_fini(moving);

Destroy queries before destroying the world if you no longer need them. The world also owns query storage and releases it during ecs_fini().