Events
EventBus
Section titled “EventBus”The EventBus is a global publish/subscribe system. Every bus.on() call
returns an unsubscribe function — call it to stop listening:
import { EventBus } from "@yagejs/core";
const bus = new EventBus();
// Subscribe — returns an unsubscribe functionconst off = bus.on("player:hit", (data) => { console.log("Ouch!", data.damage);});
// Subscribe for a single occurrencebus.once("level:complete", (data) => { console.log("Level cleared:", data.levelId);});
// Emit an eventbus.emit("player:hit", { damage: 25 });
// Unsubscribe when doneoff();Built-in Engine Events
Section titled “Built-in Engine Events”The engine emits these events automatically through the global EventBus:
| Event | Payload | When |
|---|---|---|
entity:created | { entity } | An entity is spawned |
entity:destroyed | { entity } | An entity is destroyed (EndOfFrame) |
component:added | { entity, component } | A component is added to an entity |
component:removed | { entity, component } | A component is removed from an entity |
scene:pushed | { scene } | A scene is pushed onto the stack |
scene:popped | { scene } | A scene is popped from the stack |
scene:replaced | { previous, next } | The top scene is replaced |
engine:started | {} | The engine loop has started |
engine:stopped | {} | The engine is being destroyed |
Access the engine-wide bus through the DI container:
import { EventBusKey } from "@yagejs/core";
class ScoreTracker extends Component { private off?: () => void;
onAdd() { const bus = this.use(EventBusKey); this.off = bus.on("entity:destroyed", ({ entity }) => { if (entity.tags.has("enemy")) { this.score += 100; } }); }
onDestroy() { this.off?.(); }}Typed EventBus
Section titled “Typed EventBus”For custom events with full type safety, create a typed bus with an interface:
interface GameEvents { "player:hit": { damage: number; source: string }; "item:collected": { itemId: string; value: number }; "wave:started": { waveNumber: number };}
const bus = new EventBus<GameEvents>();
// Fully typed — data shape is inferredbus.on("player:hit", (data) => { // ^? { damage: number; source: string } console.log(data.damage);});
// Type error if payload doesn't matchbus.emit("item:collected", { itemId: "gem", value: 50 });EventToken and defineEvent
Section titled “EventToken and defineEvent”For events that cross module boundaries, use defineEvent to create a
reusable, type-safe event token:
import { defineEvent } from "@yagejs/core";
// Define once in a shared moduleexport const PlayerHitEvent = defineEvent<{ damage: number }>("player:hit");export const WaveStartEvent = defineEvent<{ waveNumber: number }>("wave:started");
// Use the token to subscribe and emitbus.on(PlayerHitEvent, (data) => { console.log(data.damage);});
bus.emit(PlayerHitEvent, { damage: 30 });Event tokens carry the event name and payload type together, eliminating string-typing errors across your codebase.
Entity-Scoped Events
Section titled “Entity-Scoped Events”Entities have their own lightweight event emitter for local communication between components on the same entity:
import { defineEvent } from "@yagejs/core";
const DamagedEvent = defineEvent<{ amount: number }>("damaged");
class Health extends Component { current = 100;
takeDamage(amount: number) { this.current -= amount; this.entity.emit(DamagedEvent, { amount }); }}
class DamageFlash extends Component { onAdd() { this.entity.on(DamagedEvent, ({ amount }) => { this.flash(amount > 50 ? "red" : "white"); }); }}Entity events are automatically cleaned up when the entity is destroyed — no manual unsubscribe needed.
Scene-Level Event Subscription
Section titled “Scene-Level Event Subscription”A scene can subscribe to an EventToken and pick up both events it
emits itself and events emitted by any of its entities. The handler
takes an optional second argument — the source Entity for bubbled
events, or undefined for scene-emitted ones — so a single handler can
distinguish the two cases:
import { defineEvent, type Entity } from "@yagejs/core";
const DamagedEvent = defineEvent<{ amount: number }>("damaged");
class GameScene extends Scene { readonly name = "game";
onEnter() { this.on(DamagedEvent, (data, entity?: Entity) => { if (entity) { // Bubbled — the source called `entity.emit(DamagedEvent, ...)` console.log(`${entity.name} took ${data.amount} damage`); } else { // Scene-emitted via `scene.emit(DamagedEvent, ...)` console.log(`scene-wide damage broadcast: ${data.amount}`); } });
// Triggers the `entity = undefined` branch above this.emit(DamagedEvent, { amount: 5 });
// Triggers the bubbled branch with `entity = enemy` const enemy = this.spawn("enemy"); enemy.emit(DamagedEvent, { amount: 10 }); }}Scene.on returns an unsubscribe function. The handler signature is
(data, entity?) for every subscription — check entity to decide
whether you can read source state (entity.get(Health), etc.) or
you’re handling a scene-wide broadcast.
This is the canonical way to react to per-entity events at the scene
level — for example, when a Health component on any entity emits
DamagedEvent, a scene-level subscriber can roll those into a global
damage feed, trigger a screen-shake, or update an aggregate UI store
without each entity having to know who’s listening.