EventBus
The EventBus system is a typed publish/subscribe channel. Game logic emits named events with payloads; other systems subscribe and react. It’s the spine for decoupling scenes from each other and from systems.
Open it from Inspector → Systems → EventBus → Open.
Schema
Section titled “Schema”Each event has:
- A name (
snake_case, starts with a lowercase letter):score_changed,player_died,quest_complete. - A payload type literal (TypeScript-style):
{ score: number; cause: string }or{}for no payload. - A ring-buffer flag: when on, the bus caches the last emit so late subscribers receive it on subscribe.
The editor renders this as a four-column table you can add rows to, delete rows from, and edit in place.
Why typed events
Section titled “Why typed events”The point of the type literal isn’t runtime enforcement (the bus is plain JavaScript). It’s documentation and editor support. When your code subscribes to score_changed, the inferred handler signature gives you autocomplete and warns you on shape drift.
Bundles usually define events for the obvious decouplings:
| Event | Payload | Emitted by | Listened by |
|---|---|---|---|
score_changed | { score: number; delta: number } | Game logic | HUD, save system |
player_died | { cause: 'fall' | 'enemy' } | Game logic | Audio, game-over scene |
quest_complete | { questId: string } | Quest FSM | Banner, save, audio |
goal_reached | { goalId: string } | Goals tracker | Banner, unlock logic |
dialogue_chapter_complete | { endingId: string } | Dialogue runner | Complete scene |
Ring-buffer mode
Section titled “Ring-buffer mode”When ring-buffer is on, the bus remembers the last payload emitted for that event and replays it to any subscriber that joins after the emit. This solves the classic “the HUD subscribes after the first score_changed fired” race.
Use ring-buffer for state-shaped events (score_changed, theme_changed, current_level). Avoid it for spike-shaped events (button_clicked, enemy_killed). Spikes shouldn’t replay; the player hearing the coin sound on every HUD remount would be miserable.
Common patterns
Section titled “Common patterns”Decoupling the HUD from gameplay: gameplay emits score_changed. The HUD subscribes once on boot. Neither needs to know about the other.
Save on milestone: SaveSystem subscribes to a list of milestone events (configured in its own editor). Anyone can emit a milestone without knowing the save system exists.
Cross-scene messages: scene A emits player_entered_forest. Scene B’s AmbientController subscribes and swaps the music bed. No scene-to-scene direct call.
When NOT to use it
Section titled “When NOT to use it”The EventBus is overkill for tight intra-scene coupling. If two objects in the same scene need to talk every frame, give them direct references; don’t route through the bus. The bus is for the across-the-spine messages that decouple subsystems.
Editing the config by hand
Section titled “Editing the config by hand”The EventBus’s config in .forge/gamemanager.toml is a simple list:
[systems.EventBus.config]events = [ { name = "score_changed", payload = "{ score: number; delta: number }", ringBuffer = true }, { name = "player_died", payload = "{ cause: 'fall' | 'enemy' }", ringBuffer = false }, { name = "quest_complete", payload = "{ questId: string }", ringBuffer = false },]Validation: every event name must be unique and match ^[a-z][a-z0-9_]*$. Duplicates and invalid names block the save.