Files
skills/docs/gaming/games/ecs/src/query/query.md
2026-05-29 08:20:56 +01:00

3.6 KiB

Query

(no submodules)

Purpose

Query compiles a declarative QueryDescriptor (include / optional exclude schemas) into bitset masks and evaluates it against the archetype graph. Because archetypes group entities by their exact component set, a query only needs to test each archetype's mask once — every entity inside a matching archetype is automatically a match. The result is O(archetypes) filtering plus O(entities-in-matching-archetypes) collection.

defineQuery is the canonical factory; raw Query instances are exposed for advanced consumers building custom system runners.

Features

  • Bitset-only match predicate (containsAll + intersects).
  • Cached matchedArchetypes list, invalidated by World whenever archetypes change.
  • Handles include-only, include + exclude, and empty-exclude descriptors cleanly.
  • Result is a flat ReadonlyArray<Entity> in archetype-discovery order.

Usage: Basic Example

import { defineComponent, defineQuery, createWorld } from "@suaveplan/ecs";

const Position = defineComponent("Position", { x: 0, y: 0 });
const world = createWorld();
const entity = world.spawn();
world.addComponent(entity, Position);

const result = world.query(defineQuery([Position]));
console.log([...result]); // [1]

Usage: Advanced Example

import {
    defineComponent,
    defineQuery,
    Query,
    createWorld,
} from "@suaveplan/ecs";

const Position = defineComponent("Position", {});
const Disabled = defineComponent("Disabled", {});

const world = createWorld();
world.registerComponent(Position);
world.registerComponent(Disabled);

// Build a long-lived Query and re-evaluate on every tick.
const activeMovers = new Query(
    defineQuery([Position], [Disabled]),
    world.registry,
);

function tick() {
    const entities = activeMovers.evaluate(
        // archetype graph is exposed indirectly via world.query; advanced
        // consumers run their own archetype scans through reflection.
        [],
    );
    // ... iterate entities ...
}

API Reference

Member Description
new Query(descriptor, registry) Compile masks from a descriptor. Throws if descriptor references unregistered schemas.
defineQuery(include, exclude?) Build a descriptor with optional exclude.
matches(archetype) Predicate: does this archetype's mask satisfy the query?
evaluate(archetypes) Collect every entity from every matching archetype. Result is cached until invalidate().
invalidate() Drop the cached match list; the next evaluate rebuilds it.
includeMask / excludeMask Read-only masks for debug / inspection.

Implementation Notes

  • excludeMask is null (not an empty BitSet) when no exclude is declared, so the predicate skips the intersects test entirely in the common case.
  • Empty exclude: [] is treated as "no exclude" — the descriptor normalises the difference.
  • The world owns its Query objects; one-off world.query(descriptor) calls build an ad-hoc Query per call and discard it. Long-lived systems should pin a Query instance and call evaluate repeatedly.
  • The cached matchedArchetypes list is invalidated whenever the archetype graph mutates (component add/remove that creates a new archetype, or clear). Mutations within an existing archetype do not invalidate — entity lists are read fresh on every evaluate.
  • Iteration order is "every entity in archetype 0, then every entity in archetype 1, …". Within an archetype the order matches insertion order (subject to swap-remove side-effects).