Skip to content

Queries

world.query returns a generator-backed view over the underlying stores. Three ways to consume it:

const q = w.query([pos_c, vel_c] as const);
// 1. for...of (live iterator)
for (const [id, p, v] of q) {
p.x += v.dx; // safe: in-place updates of existing components
}
// 2. .each callback
q.each((id, p, v) => { p.x += v.dx; });
// 3. .collect() snapshot
for (const [id, p, v] of q.collect()) {
if (p.x > 100) w.despawn(id); // safe — the array was captured first
}

Component<true> markers (e.g. player_c: Component<true>) are filter-only — they restrict which entities match but never appear in the yielded tuple. The destructure shape is [Id, ...data]:

// player_c is a marker — it filters but is elided from the tuple
for (const [id, p, d] of w.query([pos_c, player_c, dir_c] as const).collect()) {
// id: Id, p: Pos, d: Dir — no `true` slot to skip
p.x += d.dx;
}
// pure-marker query — only `Id` in the tuple
for (const [id] of w.query([player_c] as const)) { /* … */ }

The as const is required for TypeScript to keep the component tuple ordered. Marker-only / data-only / mixed queries all share the same iterator semantics.

set-ting the same entity in a component already in the iterator is fine. Adding/removing the keysetdespawn, remove, or set on a new entity for one of the iterated components — invalidates the iterator. Always .collect() before structural mutation.

In __DEV__ builds, the iterator tracks the version of every iterated store. If it changes mid-iteration, you get:

[forge] world.query([pos, coin]) iterator detected mid-iteration mutation.
Snapshot via .collect() before mutating to avoid undefined behaviour.

Production builds (NODE_ENV=production or globalThis.__DEV__ = false) skip the check entirely.

The canonical collect-then-mutate pattern from coin-collector/src/systems/collection.ts:

export const collection_system: System = (w, ctx) => {
const players = w.query([pos_c, player_c] as const).collect();
if (players.length === 0) return;
const score = ctx.res.get(score_r);
if (!score.ok) return;
for (const [, pp] of players) {
for (const [cid, cp] of w.query([pos_c, coin_c] as const).collect()) {
const dx = pp.x - cp.x;
const dy = pp.y - cp.y;
if (dx * dx + dy * dy <= radius_sq) {
w.despawn(cid);
score.value.value += 10;
}
}
}
};

The without option excludes entities that have any of the listed components:

const dead = w.query([pos_c] as const, { without: [alive_c] }).collect();

query_data(data, markers) is removed. query now does the same job — markers in the component list are auto-elided from the tuple:

// v0.2.0
for (const [id, p, d] of w.query_data([pos_c, dir_c], [player_c]).collect()) { /* … */ }
// v0.3.0
for (const [id, p, d] of w.query([pos_c, dir_c, player_c] as const).collect()) { /* … */ }

One array, one as const, identical destructure shape.