Queries
Queries let you iterate over entities that have specific components. They are the primary way systems access entity data from the scene.
Basic Query
A query matches all entities in the scene that have all the listed components:
import { defineSystem, addSystem, Query, Mut, Transform, Sprite } from 'esengine';
addSystem(defineSystem([Query(Mut(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { transform.position.x += 1; }}));The for...of loop yields tuples of [entity, ...components] in the same order as the query parameters.
Mutable Queries
By default, query results are read-only. Use Mut() to get mutable access to specific components:
import { Query, Mut, Transform, Sprite } from 'esengine';
defineSystem( [Query(Mut(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { transform.position.x += 1; // writable // sprite is read-only } });Iteration Methods
for…of
for (const [entity, transform, sprite] of query) { transform.position.x += 1;}forEach
query.forEach((entity, transform, sprite) => { transform.position.x += 1;});single
Get the first matching entity. Returns null if no entities match.
const result = query.single();if (result) { const [entity, transform, sprite] = result; // ...}Utility Methods
query.isEmpty(); // true if no entities matchquery.count(); // number of matching entitiesquery.toArray(); // collect all results into an arrayQuery Filters
Use .with() and .without() to filter entities by the presence or absence of components, without including them in the result tuple:
import { Query, Mut, Transform, Sprite, defineTag } from 'esengine';
const Player = defineTag('Player');const Hidden = defineTag('Hidden');
// Match entities with Transform AND Sprite, but only those that also have PlayerdefineSystem( [Query(Mut(Transform), Sprite).with(Player)], (query) => { for (const [entity, transform, sprite] of query) { // Player is guaranteed to be present, but not in the tuple } });
// Match entities with Transform, excluding any with HiddendefineSystem( [Query(Transform).without(Hidden)], (query) => { for (const [entity, transform] of query) { // Only entities that do NOT have Hidden } });You can chain multiple .with() and .without() calls:
const Disabled = defineTag('Disabled');Query(Mut(Transform), Sprite).with(Player).without(Hidden, Disabled)Change Detection Filters
Wrap components in Added(), Changed(), or use Removed() to react to component lifecycle events.
Added
Match entities that received a component since the last time this system ran:
import { Query, Added, Transform } from 'esengine';
defineSystem([Query(Added(Transform))], (query) => { for (const [entity, transform] of query) { console.log('New transform on', entity); }});Changed
Match entities whose component data was mutated (via Mut()) since the last tick:
import { Query, Changed, Mut, Text } from 'esengine';
defineSystem([Query(Changed(Text))], (query) => { for (const [entity, text] of query) { console.log('Text changed on', entity, text.content); }});Removed
Removed() is a standalone parameter (not inside Query). It yields entities that had a component removed since the last tick:
import { Removed } from 'esengine';import { Health } from './components';
defineSystem([Removed(Health)], (removed) => { for (const entity of removed) { console.log('Health removed from', entity); }});See Change Detection for details on how tick-based tracking works.
Multiple Queries
A system can have multiple queries to cross-reference different entity types:
import { defineTag } from 'esengine';
const Player = defineTag('Player');const Enemy = defineTag('Enemy');
defineSystem( [Query(Transform, Player), Query(Transform, Enemy)], (players, enemies) => { for (const [_, playerTransform] of players) { for (const [_, enemyTransform] of enemies) { // Check distance between player and each enemy } } });Combining with Resources
Queries are often used together with resources:
import { Res, Time, Input, Query, Mut, Transform } from 'esengine';
defineSystem( [Res(Time), Res(Input), Query(Mut(Transform), Player)], (time, input, query) => { for (const [entity, transform] of query) { if (input.isKeyDown('KeyD')) { transform.position.x += 200 * time.delta; } } });Example: Collision Detection
Define Hitbox, Player, and Enemy components, attach them to entities in the scene editor, then query:
import { defineSystem, defineComponent, defineTag, addSystem, Query, Transform } from 'esengine';
const Hitbox = defineComponent('Hitbox', { width: 50, height: 50 });const Player = defineTag('Player');const Enemy = defineTag('Enemy');
addSystem(defineSystem( [Query(Transform, Hitbox, Player), Query(Transform, Hitbox, Enemy)], (players, enemies) => { for (const [_, pPos, pBox] of players) { for (const [_, ePos, eBox] of enemies) { const dx = Math.abs(pPos.position.x - ePos.position.x); const dy = Math.abs(pPos.position.y - ePos.position.y);
if (dx < (pBox.width + eBox.width) / 2 && dy < (pBox.height + eBox.height) / 2) { // Collision! } } } }));Iteration Safety
During query iteration, you cannot directly create or destroy entities, or insert or remove components. These operations modify the underlying data structures and would invalidate the iterator. The engine detects such operations during iteration and throws an error to prevent data corruption.
The correct approach is to use Commands to defer modifications until after iteration completes:
import { defineSystem, defineComponent, addSystem, Commands, Query } from 'esengine';
const Health = defineComponent('Health', { value: 100 });
const despawnDead = defineSystem( [Commands(), Query(Health)], (commands, query) => { for (const [entity, health] of query) { if (health.value <= 0) { commands.despawn(entity); // deferred, safe } } });Commands operations are flushed after the current system function returns. At that point iteration has finished, so the modifications do not affect any in-progress traversal.
Next Steps
- Change Detection — tick-based tracking for Added, Changed, Removed
- Events — decoupled inter-system communication
- Resources — global singleton data like Time and Input
- Components — all builtin and custom component types