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
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.
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 aspectsSame 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.
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(andthis.node— the same object) is the scene, typed to the second generic:System<'fx'>targets a 3DScene,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.sceneis typed as the plain scene kind, without the other systems' accessors: reach them withthis.scene.get(Controls)!. - Systems are created by
scene.system(), nevernew— no constructor; initialize inonAttach. - 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