Core
Query
Query declarations, matching semantics, and typed cell access surfaces.
Queries are the read-side center of ECS gameplay code. They describe which
entities a system is allowed to see and exactly which component cells that
system may read or write once a match is found.
In practice this module answers three different design questions at once:
- which entities should this system iterate over
- which components prove an entity belongs in that iteration
- which slots are readable, writable, or only optionally available
Reach for this module whenever game logic needs to iterate entity state,
perform host sync from ECS data, or resolve durable handles back into
current-world entity proofs.
Examples
// Describe exactly which entities the movement system may see.
const MovingActors = Game.Query({
selection: {
// Writable slots declare mutation capability in the query result.
position: Game.Query.write(Position),
// Read-only slots prove presence without granting mutation.
velocity: Game.Query.read(Velocity),
// Optional slots do not affect matching and must be narrowed explicitly.
sprite: Game.Query.optional(Sprite)
},
// Structural filters describe who participates in the system at all.
with: [Movable],
without: [Frozen]
})
const MoveActors = Game.System("MoveActors", {
queries: { moving: MovingActors },
resources: { dt: Game.System.readResource(DeltaTime) }
}, ({ queries, resources }) => {
const dt = resources.dt.get()
for (const { data } of queries.moving.each()) {
const velocity = data.velocity.get()
data.position.update((position) => ({
x: position.x + velocity.x * dt,
y: position.y + velocity.y * dt
}))
// Optional reads stay explicit at the use site.
if (data.sprite.present) {
void data.sprite.get()
}
}
})
Declares read-only access to a component in a query selection.
A required read slot is both a matching requirement and a typing decision:
the entity must have that component, and the resulting slot becomes a
ReadCell. Use this for data the system needs to inspect but must not
mutate.
Declares writable access to a component in a query selection.
Use this when the system is responsible for mutating component state in
place. A write slot both requires component presence and exposes the slot as
a WriteCell, so mutation capability stays visible in the query spec rather
than appearing ad hoc in the loop body.
Declares maybe-present read-only access to a component in a query
specification.
optional(...) is for enrichment, not matching. It keeps the entity set
broad while letting one system opportunistically read extra data when
present. The returned cell forces an explicit present check before use, so
the possibility of absence remains visible in the type surface.
// Keep the main query focused on movers, but read sprite data when available.
const query = Game.Query({
selection: {
position: Game.Query.read(Position),
sprite: Game.Query.optional(Sprite)
}
})
Declares a filter that matches components added since the reading system's
previous run.
Change detection is per system: every system sees each addition exactly
once, on its first run after the addition, independently of other systems.
A system's first run sees every existing component as added. This is the
usual entrypoint for incremental host sync, such as creating renderer nodes.
const AddedRenderableQuery = Game.Query({
selection: {
position: Game.Query.read(Position),
renderable: Game.Query.read(Renderable)
},
filters: [Game.Query.added(Renderable)]
})
Declares a filter that matches components added or written since the
reading system's previous run.
Any write through a write cell counts, even when the value is equal to the
previous one. Writes from a system whose run failed are rolled back and do
not count.
const MovedQuery = Game.Query({
selection: {
position: Game.Query.read(Position)
},
filters: [Game.Query.changed(Position)]
})
Creates a typed missing-entity error.
Creates a typed query-mismatch error.
Creates a typed zero-match error.
Creates a typed multi-match error.
Creates an explicit query specification.
Use this inside system specs instead of relying on callback parameter
inference. The resulting value drives both runtime execution and the derived
query result type.
A query spec is purely declarative. It does not access the world by itself;
systems receive QueryHandles derived from the spec. In practice this is
where you encode the exact shape of one gameplay iteration pass.
// Define one reusable iteration contract for a movement system.
const Moving = Game.Query({
selection: {
position: Game.Query.write(Position),
velocity: Game.Query.read(Velocity)
},
with: [Position, Velocity],
without: [Sleeping]
})