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] },
}) {
/*…*/
}
Value Predicates (where
)
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) {
/* … */
}
}
}
Supported operators
- Equality/inequality:
eq
,ne
- Numeric comparisons:
lt
,le
,gt
,ge
- Set membership:
isin
(in),nin
(not in)
Entity references
// 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” (e.g.,
radius
) and update that viasetValue
.
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 whosewhere
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 (e.g., Map from id → entity) or tag associations during qualify
and reuse.
Advanced Query Examples
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`,
);
}
}