Events
Game logic talks through entity events. You declare an event once with
defineEvent, emit it on the entity where something happens, and listen for it
from a component. An event emitted on an entity also reaches the scene, so a
component elsewhere can react to every entity’s events. The engine’s
EventBus is separate: it carries engine events such as entity:created and
scene:pushed.
EventToken and defineEvent
Section titled “EventToken and defineEvent”Entity and scene events are addressed by an EventToken, not by a string.
defineEvent creates one and pairs the event name with its payload type, so
every on and emit site is checked against the same shape:
import { Entity, 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");
class Player extends Entity {}
// Tokens address entity.on / entity.emit and scene.on / scene.emitconst player = scene.spawn(Player);player.on(PlayerHitEvent, (data) => { console.log(data.damage);});player.emit(PlayerHitEvent, { damage: 30 });
scene.emit(WaveStartEvent, { waveNumber: 2 });The engine EventBus is keyed by string and does not take
tokens: bus.on(PlayerHitEvent, ...) does not compile.
Dispatch is by the token’s name, not by the token object. Two defineEvent
calls with the same name are one channel, and their payload types are not
checked against each other. Prefix names with the owning module
("inventory:item-added") to keep them distinct. In dev builds the second
definition of a name logs a warning; an empty name throws.
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 { Component, 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.listen(this.entity, DamagedEvent, ({ amount }) => { this.flash(amount > 50 ? "red" : "white"); }); }
private flash(color: "red" | "white") { // tint the entity's sprite with `color` for a few frames }}A subscription made with this.listen is released when the component is
removed or its entity is destroyed. this.listen takes any entity, so a
component can follow another entity’s events, and that subscription is still
released with the component. A bare entity.on subscription is released only
when that entity is destroyed.
Scene-Level Event Subscription
Section titled “Scene-Level Event Subscription”A component can listen at the scene level with this.listenScene. It hears
both events the scene emits itself and events emitted by any of the
scene’s entities. The handler takes an optional second argument: the source
Entity for bubbled events, or undefined for scene-emitted ones. A single
handler can tell the two cases apart:
import { Component, Entity, defineEvent } from "@yagejs/core";
const DamagedEvent = defineEvent<{ amount: number }>("damaged");
/** Rolls every entity's damage into one feed. */class DamageFeed extends Component { onAdd() { this.listenScene(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}`); } }); }}
class Enemy extends Entity {}const enemy = scene.spawn(Enemy);
// Triggers the bubbled branch, with `entity = enemy`enemy.emit(DamagedEvent, { amount: 10 });
// Triggers the `entity = undefined` branchscene.emit(DamagedEvent, { amount: 5 });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.
A subscription made with this.listenScene is released with the component.
Outside a component, scene.on(token, handler) subscribes and returns an
unsubscribe function. Every scene-level subscription is released when the
scene exits, together with its entities. A scene instance that is pushed again
starts with no subscriptions.
This is how one component reacts to every entity’s events without each entity
knowing who’s listening. When a Health component on any entity emits
DamagedEvent, a component such as DamageFeed can roll those into a damage
total, trigger a screen shake, or update the HUD. The component that keeps
the state is the one that listens, as in
State Management.
A scene’s onEnter may connect an event to a component method in one line,
such as this.on(PlayerHit, () => player.get(Health).damage(1)). The state and
the rule stay in the component.
Engine EventBus
Section titled “Engine EventBus”The engine’s EventBus carries the engine’s own events: entities created and
destroyed, scenes pushed and popped, the engine starting and stopping. Resolve
it with EventBusKey. Every bus.on() call returns an unsubscribe function;
call it to stop listening:
import { EventBusKey } from "@yagejs/core";
const bus = context.resolve(EventBusKey);
// Subscribe — returns an unsubscribe functionconst off = bus.on("scene:pushed", ({ scene }) => { console.log("Scene pushed:", scene.name);});
// Subscribe for a single occurrencebus.once("engine:started", () => { console.log("Engine started");});
// Unsubscribe when doneoff();The bus is typed with the engine’s event map, EngineEvents, so it accepts
only the events below. Game events are defineEvent tokens emitted on an
entity or a scene, as described above.
Built-in Engine Events
Section titled “Built-in Engine Events”The engine emits these events automatically:
| Event | Payload | When |
|---|---|---|
entity:created | { entity } | An entity is spawned |
entity:destroyed | { entity, scene } | An entity is destroyed, on the end-of-frame flush or on scene teardown |
component:added | { entity, component } | A component is added to an entity |
component:removed | { entity, componentClass } | 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 | { oldScene, newScene } | The top scene is replaced |
scene:transition:started | { kind, fromScene, toScene } | A scene transition begins |
scene:transition:ended | { kind, fromScene, toScene } | A scene transition finishes |
scene:loading:progress | { scene, ratio } | A loading scene reports progress (0 to 1) |
scene:loading:done | { scene } | A loading scene has finished loading |
engine:started | undefined | The engine loop has started |
engine:stopped | undefined | The engine is being destroyed |
screen:fullscreen | { active } | The canvas host entered or left fullscreen |
screen:orientation | { type } | The device orientation changed |
entity in the entity:* and component:* payloads is the live Entity,
so a listener can read its tags and components. The scene fields carry
{ name }, except in entity:destroyed and the loading events, which carry
the Scene.
From a component, subscribe with this.listenBus(event, handler). The
subscription is released when the component is removed or its entity is
destroyed:
import { Component } from "@yagejs/core";
/** Counts destroyed entities for a debug readout. */class DestroyedCount extends Component { count = 0;
onAdd() { this.listenBus("entity:destroyed", () => { this.count += 1; }); }}For a rule about a specific kind of entity, such as scoring a defeated enemy,
have that entity emit its own event and listen with this.listenScene.
Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/core-concepts.md.