A System is logic that processes entities matching a query. Override onUpdate(dt) for per-frame work. Override onEntityAdded/onEntityRemoved for setup and teardown when entities enter or leave the query.
The smallest possible System
The base class:
- Stores
eventBus(passed via constructor) - Maintains
this.entities— automatically kept in sync with the query - Provides empty hook defaults so you only override what you need
Query options
The query field has three lists. All are optional. An empty query matches every entity, which you almost never want.
The framework re-evaluates the query whenever a Component is added/removed or a tag changes. Entities flow in and out of this.entities automatically. Never iterate world.getAllEntities() yourself in a System.
Lifecycle hooks
Listening to events
Systems coordinate via the EventBus rather than holding references to each other. Subscribe in onInitialize with this.listen() — the subscription's lifetime is tied to the System, so it's unsubscribed automatically when the System is removed:
Listen by name with this.listen(name, callback). Emit with this.eventBus.emit(name, data). Two variants cover the other subscription shapes: this.listenOnce(name, callback) fires once, and this.listenForEntity(name, entityId, callback) only fires for events about one entity. All three return an unsubscribe function for early manual removal, but you rarely need it — the base class drains every listen() subscription when the System shuts down. (Raw eventBus.on is the primitive underneath; use it outside Systems, where you own the unsubscribe yourself.)
Reading the renderer
The System gets at the renderer adapter through this.world.renderer:
The adapter is typed as RendererAdapter. You can write Systems that work under any adapter — Babylon, Three, or Mock — without changes.
Order of execution
Systems run in the order they were added to the World. If SystemA should compute a value that SystemB reads, add SystemA first.
There's no priority field — execution order is the addition order. Keep it simple.
Why query-based filtering matters
Without an ECS, the typical pattern is:
Every if is paying for itself once per object per frame. Adding a new behavior means touching the central loop. With Systems, the dispatching is the framework's job:
Each System sees only the entities its query matches. New behavior is a new System; no edits to existing code.