Systems

A system is the behaviour of the whole SCENE — input, a game mode, a spawner, the HUD, the FX pools: logic with no single node to live on, attached with scene.system(Class). Behaviour of ONE node (a door, an enemy, a pickup) is an Aspect; neither is a hand-written setLoop / tick() loop.

  • read the keyboard once per frame for the whole game
  • run a game mode or a spawner for the whole scene
  • show a HUD that updates every frame
  • share one particle pool between every object in the scene
  • replace a hand-written game loop or tick()
  • choose between a system and an aspect

Platforms: iOS, Android, desktop, the web player, headless tests; the web (Web Lite) on a 3D Scene only — it has no Scene2D.

At a glance

TypeScript
class Controls extends System<'controls'> {
  moveX = 0; moveY = 0; fire = false
  updateBefore() {                                     // EARLY: poll before the sim steps
    this.moveX = (Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0)
    this.moveY = (Input.key('KeyW') ? 1 : 0) - (Input.key('KeyS') ? 1 : 0)
    this.fire = Input.key('MouseLeft')
  }
}
class Hud extends System<'hud', Scene, { announce: (text: string) => void }> {
  static after = [Controls]
  private banner = UIText('').style({ fontSize: 30, color: '#ffffff' })
  onAttach() { this.scene.widgets.append(UIWidget(this.banner).style({ left: '50%', top: '20%' })) }
  announce(text: string) { this.banner.text = text; this.emit('announce', text) }
  update(dt: number) { this.banner.style.opacity = this.scene.get(Controls)!.fire ? 0.5 : 1 }
}

const scene = new Scene().system(Controls).system(Hud)
scene.open()
scene.hud.on('announce', (t) => console.log('[hud]', t))
scene.hud.announce('ROUND 1')

Add a system to a scene: scene.system, get, removeSystem

scene.system(Class, opts) attaches a system to the scene and names it on the scene: scene.system(Fx) → scene.fx.

TypeScript
scene.system(Class, opts?): scene & { name: instance }   // attach + configure (or reconfigure)
scene.get(Class): instance | undefined                   // safe access
scene.has(Class): boolean                                // existence check AND type guard
scene.removeSystem(Class): scene                         // detach (runs onDetach); chainable
scene.destroy(): void                                    // every system detaches, then the scene closes
Aspect.all(Class): readonly instance[]                   // systems are registered like aspects

Same rules as node.aspect(): the accessor name comes from the first generic parameter, opts are assigned before onAttach, a second system(Class, opts) reconfigures instead of re-attaching, and the accessor is typed on the chain's result (const scene = new Scene().system(Hud) → scene.hud). destroy() detaches the systems last-attached first, while the scene still exists; a Scene2D is then destroyed natively too, a 3D Scene is closed and its nodes stay yours.

Write a system instead of a game loop: class … extends System

A system takes the place of a hand-written game loop — a "game" class whose tick() calls everything in order. It has exactly the lifecycle of an aspect (onAttach / onDetach / onReconfigure, updateBefore / updateFixed / update, updateOrder / static after, events), attached to the scene instead of a node.

TypeScript
class Fx extends System<'fx'> {                 // <accessor name, scene kind = Scene, events = {}>
  private sparks!: Particles
  onAttach() {                                  // this.scene is the scene it was attached to
    this.sparks = new Particles({ blend: 'add', rate: 0, lifetime: 0.4, size: 0.05 })
    this.scene.add(this.sparks)
  }
  onDetach() { this.sparks.destroy() }
  impact(p: Vec3) { this.sparks.position = p; this.sparks.spawn(20) }
}
  • this.scene (and this.node — the same object) is the scene, typed to the second generic: System<'fx'> targets a 3D Scene, System<'fx', Scene2D> a 2D one. Attaching a 2D system to a 3D scene (or the other way round) is a compile error.
  • Inside a system this.scene is typed as the plain scene kind, without the other systems' accessors: reach them with this.scene.get(Controls)!.
  • Systems are created by scene.system(), never new — no constructor; initialize in onAttach.
  • Order between systems and aspects is one list per phase: static after = [Controls] on an aspect can name a system and vice versa (see aspects.md).

System or aspect: which one to use {#what-belongs-in-a-system-vs-an-aspect}

Aspect (on a node) System (on the scene)
Owns one node's behaviour / state scene-wide state, pools, services
Examples Health, ExplosiveBarrel, PlayerController, CameraFollow Controls, Fx, Hud, GameMode, Spawner
Found via node.get(Ctor), Aspect.all(Ctor), scene-file use() scene.get(Ctor), scene.fx
Lifetime until removeAspect / node.destroy() until removeSystem / scene.destroy()

Don't force scene-wide things onto a dummy node ("an empty with a Loop aspect"): that is what a system is for. Don't turn per-object state into a system either — a barrel's hp lives on the barrel.

See also

  • Aspects — phases, ordering, events (all shared with systems)
  • Time — pause / time scale
  • Project architecture — a whole game built from systems and aspects