ECS Queries
Queries define dynamic entity sets. Each query specifies:
required: components an entity must haveexcluded: components an entity must not have (optional)where: value predicates on component fields (optional)
Membership updates automatically as entities gain/lose components and when relevant component values change.
Defining queries (required/excluded)
export class RenderUISystem extends createSystem({
panels: { required: [PanelUI], excluded: [ScreenSpace] },
}) {
/*…*/
}
IWSDK re‑exports elics predicate helpers for readable, type‑safe value filters:
import { eq, ne, lt, le, gt, ge, isin, nin } from '@iwsdk/core';
export class DangerHUD extends createSystem({
lowHealth: {
required: [Health],
where: [lt(Health, 'current', 30)],
},
statusBar: {
required: [Status],
where: [isin(Status, 'phase', ['combat', 'boss'])],
},
}) {
update() {
// Only entities with Health.current < 30
for (const e of this.queries.lowHealth.entities) {
/* … */
}
}
}
- Equality/inequality:
eq, ne - Numeric comparisons:
lt, le, gt, ge - Set membership:
isin (in), nin (not in)
// Types.Entity fields are nullable entity references; test for null/non-null
export const Target = createComponent('Target', {
entity: { type: Types.Entity, default: null },
});
export class Targeting extends createSystem({
locked: { required: [Target], where: [ne(Target, 'entity', null)] },
free: { required: [Target], where: [eq(Target, 'entity', null)] },
}) {
/* … */
}
Notes:
- Predicates can reference fields across different required components.
- Predicates on numeric fields validate against min/max if your schema defines them (see components).
- Vector fields (Vec2/3/4) are exposed as typed arrays; if you need to filter by a vector field, prefer aliasing a numeric key (for example,
radius) and update that via setValue.
How membership updates when values change
When you call entity.setValue(Component, 'key', value), the ECS:
- writes the value into the component’s storage
- calls
updateEntityValue(entity, component) to re‑evaluate queries whose where depends on that component
This happens automatically for scalar fields. For vector fields obtained via getVectorView, direct mutation does not trigger value re‑evaluation. If a query’s where depends on a vector field, update it through setValue (with a whole tuple) or keep a scalar mirror for filtering.
Live update diagram:
entity.setValue(C,'hp',20) → QueryManager.updateEntityValue(entity,C)
→ re-evaluate queries with where on C → emit qualify/disqualify → update query.entities
Reacting to membership changes
this.queries.panels.subscribe('qualify', (e) => {
// attach resources exactly once when an entity starts matching
});
this.queries.panels.subscribe('disqualify', (e) => {
// cleanup when it stops matching
});
Iterating entities efficiently
for (const e of this.queries.panels.entities) {
// per-frame work
}
Avoid nested O(N²) loops across two large queries. Build an index (for example, Map from id → entity) or tag associations during qualify and reuse.
Dynamic difficulty adjustment
export class DifficultySystem extends createSystem({
// Only enemies that need difficulty scaling
weakEnemies: {
required: [Health, Enemy],
where: [lt(Health, 'current', 20)],
},
strongEnemies: {
required: [Health, Enemy],
where: [gt(Health, 'current', 80)],
},
playerNearby: {
required: [Player, Transform],
where: [lt(Transform, 'distanceToPlayer', 10)],
},
}) {
update() {
const playerCount = this.queries.playerNearby.entities.size;
// Buff weak enemies when players are close
if (playerCount > 0) {
for (const enemy of this.queries.weakEnemies.entities) {
const current = enemy.getValue(Health, 'current')!;
enemy.setValue(Health, 'current', Math.min(100, current + 5));
}
}
}
}
Smart resource management
export class ResourceSystem extends createSystem({
// Entities that need expensive updates
highDetail: {
required: [Mesh, Transform],
where: [lt(Transform, 'distanceToCamera', 50)],
},
// Entities that can use cheap updates
lowDetail: {
required: [Mesh, Transform],
where: [gt(Transform, 'distanceToCamera', 50)],
},
// Completely invisible (can skip entirely)
invisible: {
required: [Mesh, Visibility],
where: [eq(Visibility, 'isVisible', false)],
},
}) {
update() {
// Expensive processing only for nearby objects
for (const entity of this.queries.highDetail.entities) {
this.expensiveUpdate(entity);
}
// Cheap processing for distant objects
for (const entity of this.queries.lowDetail.entities) {
this.cheapUpdate(entity);
}
// Skip invisible entities entirely
console.log(
`Skipping ${this.queries.invisible.entities.size} invisible entities`,
);
}
}