Skip to content

Entity Query System

Entity querying is one of the core features of ECS architecture. This guide introduces how to use Matcher and QuerySystem to query and filter entities.

Matcher is a chainable API used to describe entity query conditions. It doesn’t execute queries itself but passes conditions to EntitySystem or QuerySystem.

QuerySystem is responsible for actually executing queries, using reactive query mechanisms internally for automatic performance optimization.

This is the most common usage. EntitySystem automatically filters and processes entities matching conditions through Matcher.

import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
}
class VelocityComponent extends Component {
public vx: number = 0;
public vy: number = 0;
}
class MovementSystem extends EntitySystem {
constructor() {
// Method 1: Use Matcher.empty().all()
super(Matcher.empty().all(PositionComponent, VelocityComponent));
// Method 2: Use Matcher.all() directly (equivalent)
// super(Matcher.all(PositionComponent, VelocityComponent));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent)!;
const vel = entity.getComponent(VelocityComponent)!;
pos.x += vel.vx;
pos.y += vel.vy;
}
}
}
// Add to scene
scene.addEntityProcessor(new MovementSystem());
class HealthSystem extends EntitySystem {
constructor() {
// Entity must have both Health and Position components
super(Matcher.empty().all(HealthComponent, PositionComponent));
}
protected process(entities: readonly Entity[]): void {
// Only process entities with both components
}
}
class DamageableSystem extends EntitySystem {
constructor() {
// Entity must have at least Health or Shield
super(Matcher.any(HealthComponent, ShieldComponent));
}
protected process(entities: readonly Entity[]): void {
// Process entities with health or shield
}
}
class AliveEntitySystem extends EntitySystem {
constructor() {
// Entity must not have DeadTag component
super(Matcher.all(HealthComponent).none(DeadTag));
}
protected process(entities: readonly Entity[]): void {
// Only process living entities
}
}
class CombatSystem extends EntitySystem {
constructor() {
super(
Matcher.empty()
.all(PositionComponent, HealthComponent) // Must have position and health
.any(WeaponComponent, MagicComponent) // At least weapon or magic
.none(DeadTag, FrozenTag) // Not dead or frozen
);
}
protected process(entities: readonly Entity[]): void {
// Process living entities that can fight
}
}

Used for systems that only need lifecycle methods (onBegin, onEnd) but don’t process entities.

class FrameTimerSystem extends EntitySystem {
constructor() {
// Match no entities
super(Matcher.nothing());
}
protected onBegin(): void {
// Execute at frame start
Performance.markFrameStart();
}
protected process(entities: readonly Entity[]): void {
// Never called because no matching entities
}
protected onEnd(): void {
// Execute at frame end
Performance.markFrameEnd();
}
}
MethodBehaviorUse Case
Matcher.empty()Match all entitiesProcess all entities in scene
Matcher.nothing()Match no entitiesOnly need lifecycle callbacks

If you don’t need to create a system, you can use Scene’s querySystem directly.

// Get scene's query system
const querySystem = scene.querySystem;
// Query entities with all specified components
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
console.log(`Found ${result1.count} moving entities`);
console.log(`Query time: ${result1.executionTime.toFixed(2)}ms`);
// Query entities with any specified component
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
console.log(`Found ${result2.count} combat units`);
// Query entities without specified components
const result3 = querySystem.queryNone(DeadTag);
console.log(`Found ${result3.count} living entities`);
// Query by tag
const playerResult = querySystem.queryByTag(Tags.PLAYER);
for (const player of playerResult.entities) {
console.log('Player:', player.name);
}
// Query by name
const bossResult = querySystem.queryByName('Boss');
if (bossResult.count > 0) {
const boss = bossResult.entities[0];
console.log('Found Boss:', boss);
}
// Query by single component
const healthResult = querySystem.queryByComponent(HealthComponent);
console.log(`${healthResult.count} entities have health`);

QuerySystem uses reactive queries internally with automatic caching:

// First query, executes actual query
const result1 = querySystem.queryAll(PositionComponent);
console.log('fromCache:', result1.fromCache); // false
// Second same query, uses cache
const result2 = querySystem.queryAll(PositionComponent);
console.log('fromCache:', result2.fromCache); // true

Query cache updates automatically when entities add/remove components:

// Query entities with weapons
const before = querySystem.queryAll(WeaponComponent);
console.log('Before:', before.count); // Assume 5
// Add weapon to entity
const enemy = scene.createEntity('Enemy');
enemy.addComponent(new WeaponComponent());
// Query again, automatically includes new entity
const after = querySystem.queryAll(WeaponComponent);
console.log('After:', after.count); // Now 6