Skip to content

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 match
query.count(); // number of matching entities
query.toArray(); // collect all results into an array

Query 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 Player
defineSystem(
[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 Hidden
defineSystem(
[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