Skip to content

System

Systems implement behavior. They declare queries (entity sets) and optional config schema (reactive signals).

createSystem(queries, schema) receives only query descriptors and optional configuration fields. Define lifecycle methods such as init(), update(), and destroy() in the subclass body, not inside the queries object.

Mental Model: Behavior that reacts to data

Systems are functions over sets of entities. Think of them as database stored procedures that run every frame:

text
Traditional Game Loop:           ECS System Approach:
──────────────────────           ───────────────────
for each object:                 for each relevant entity set:
  object.update(dt)                system.update(dt, entities)

Problems:                        Benefits:
- Tight coupling                 - Data/behavior separation
- Hard to optimize               - Cache-friendly iteration
- Difficult to disable          - Easy to enable/disable
- Hard to test                   - Systems are pure functions

Queries pick the rows (entities) to process; systems implement the logic of what to do with that data each frame.

Anatomy

ts
import { Types, createSystem } from '@iwsdk/core';

export class MySystem extends createSystem(
  {
    groupA: { required: [CompA], excluded: [CompB] },
    groupB: { required: [CompC] },
  },
  {
    speed: { type: Types.Float32, default: 1 },
  },
) {
  init() {
    this.queries.groupA.subscribe('qualify', (e) => {
      /*…*/
    });
  }
  update(dt: number) {
    for (const e of this.queries.groupB.entities) {
      /*…*/
    }
  }
  destroy() {
    /* cleanup */
  }
}

Config Signals

System config values are reactive signals powered by @preact/signals. Each config key is a Signal<T> at this.config.key.

ts
export class CameraShake extends createSystem(
  {},
  {
    intensity: { type: Types.Float32, default: 0 },
    decayPerSecond: { type: Types.Float32, default: 1 },
  },
) {
  update(dt: number) {
    const i = this.config.intensity.peek();
    if (i <= 0) return;
    // apply random offset scaled by i
    this.camera.position.x += (Math.random() - 0.5) * 0.01 * i;
    this.config.intensity.value = Math.max(
      0,
      i - this.config.decayPerSecond.peek() * dt,
    );
  }
}

Reading vs Tracking

  • .value — set and track.
  • .peek() — read without tracking to avoid incidental subscriptions.
  • .subscribe(fn) — run when the value changes.
ts
const unsubscribe = this.config.intensity.subscribe((v) => console.log(v));
// later
unsubscribe();

UI ↔ ECS Wiring

Config signals are ideal for developer tools and UI controls:

ts
// debugging slider
document.getElementById('intensity')!.addEventListener('input', (e) => {
  this.config.intensity.value = Number((e.target as HTMLInputElement).value);
});

Creating/Destroying Entities in Systems

ts
const e = this.createEntity();
e.addComponent(CompA);
e.destroy(); // remove when done

Priorities

Systems run each frame in ascending priority (more negative runs earlier). IWSDK registers some priorities by default when you enable features in World.create:

  • Locomotion: −5 (if enabled)
  • Input: −4 (always)
  • Grabbing: −3 (if enabled)

You control your system’s order via { priority } when registering:

ts
world.registerSystem(MySystem, { priority: -2 });

Query events and resource management

Use qualify/disqualify to perform one‑time setup/teardown per entity. Cache handles on the entity (e.g., via a component or a symbol) instead of recomputing every frame.

Batching and allocations

  • Avoid creating objects in update loops; reuse vectors/arrays.
  • Keep derived numbers in components if they are consumed by multiple systems.

Debugging patterns

  • Log query sizes: console.debug('N panels', this.queries.panels.entities.size).
  • Temporarily increase verbosity of specific systems with a debug config signal.

Sharing data via globals

this.globals is a reference to world.globals — a simple shared object store. Use sparingly for cross‑system coordination.

ts
this.globals.navMesh = myNavMesh;

Privacy | Terms