Develop
Develop
Select your platform

ECS Systems

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

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:
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, and systems implement the logic of what to do with that data each frame.

Anatomy

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.
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.
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:
// debugging slider
document.getElementById('intensity')!.addEventListener('input', (e) => {
  this.config.intensity.value = Number((e.target as HTMLInputElement).value);
});

Creating/destroying entities in systems

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 through { priority } when registering:
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 (for example, through 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 through globals

this.globals is a reference to world.globals. It’s a simple shared object store. Use sparingly for cross‑system coordination.
this.globals.navMesh = myNavMesh;
Did you find this page helpful?
Thumbs up icon
Thumbs down icon