Scenes
What is a Scene?
Section titled “What is a Scene?”A scene is a self-contained slice of your game — a title screen, a gameplay level, a pause menu. Scenes own entities and receive lifecycle callbacks as they are pushed onto or popped from the scene stack.
Defining a Scene
Section titled “Defining a Scene”Extend the Scene base class:
import { Scene, Transform, Vec2 } from "@yagejs/core";
class GameplayScene extends Scene { readonly name = "gameplay"; readonly pauseBelow = true; // pause scenes underneath readonly transparentBelow = false; // hide scenes underneath
onEnter() { const player = this.spawn("player"); player.add(new Transform({ position: new Vec2(400, 300) })); player.add(new PlayerController()); }
onExit() { // cleanup when this scene is removed from the stack }
onPause() { // the scene became paused: a scene was pushed on top, // `paused` was set, or the tab blurred with autoPauseOnBlur }
onResume() { // the scene is running again: the scene on top was popped, // `paused` was cleared, or focus returned }}Scene Properties
Section titled “Scene Properties”| Property | Default | Purpose |
|---|---|---|
name | — | Unique identifier for the scene |
pauseBelow | true | When true, scenes below this one stop updating. Set to false for HUD or overlay scenes that should let the underlying scene keep running. |
transparentBelow | false | When true, scenes below this one still render. When false (the default), below-stack scene trees are hidden — both world layers and screen-space UI/HUD. (Detached scenes mounted via _mountDetached, e.g. the debug overlay, are not affected.) |
A pause menu inherits the default pauseBelow: true and adds
transparentBelow: true — the game world is visible but frozen
underneath. A HUD overlay typically sets pauseBelow: false and
transparentBelow: true so gameplay continues to tick under it.
transparentBelow composes through the stack: a below scene stays
visible only while every scene above it has transparentBelow: true. As
soon as one scene above is opaque (transparentBelow: false, the
default), everything underneath it is hidden — world layers and any
screen-space UI alike. While a scene transition is running both the
outgoing and incoming scenes render regardless of the flag, so a
crossFade push from a menu into a transparentBelow: false gameplay
scene still dissolves cleanly; the visibility chain is reapplied when
the transition ends.
Scene Stack Management
Section titled “Scene Stack Management”The engine exposes a stack-based scene manager:
// Push a new scene onto the stackengine.scenes.push(new GameplayScene());
// Pop the top sceneengine.scenes.pop();
// Replace the top scene (pop + push in one call)engine.scenes.replace(new GameOverScene());
// Get the currently active (top) sceneconst current = engine.scenes.active;Lifecycle Flow
Section titled “Lifecycle Flow”push(A) → A.onEnter()push(B) → A.onPause() → B.onEnter()pop() → B.onExit() → A.onResume()replace(C) → B.onExit() → C.onEnter()onPause and onResume fire on every effective pause transition, not
just stack changes: setting scene.paused = true from game code, the
manager’s autoPauseOnBlur pausing scenes on tab blur, and a snapshot
restoring a scene that was saved paused all fire onPause; the matching
un-pause fires onResume. The hooks track scene.isPaused — a write that
does not change the effective state (re-assigning the same value, or
flipping the flag while the scene is already paused by a scene above)
fires nothing. Ducking music or muting input in onPause therefore covers
manual pauses and tab blur, not only pushed pause menus.
To start a scene already paused, set scene.paused = true before pushing
it — the push fires onPause once. Don’t write scene.paused from inside a
lifecycle hook (onEnter, onExit, onPause, onResume): that write races
the stack transition’s own pause diff, so the hooks can fire twice or
unpaired. A dev-mode warning flags this case.
Querying Entities
Section titled “Querying Entities”Scenes provide helpers to find entities:
// Find an entity by name (returns first match or undefined)const player = scene.findEntity("player");
// Find all entities with a given tagconst enemies = scene.findEntitiesByTag("enemy");
// Get all entities in the sceneconst all = scene.getEntities();Subclassing Scene
Section titled “Subclassing Scene”Scenes are always classes. Override onEnter to spawn entities, resolve
services, and wire up the scene:
import { Component, Scene, SceneManagerKey, Transform, Vec2 } from "@yagejs/core";import { SpriteComponent, texture } from "@yagejs/renderer";import { InputManagerKey } from "@yagejs/input";
const LogoTex = texture("assets/logo.png");
// Per-frame input polling lives in a Component, not in the Scene.class TitleController extends Component { private readonly input = this.service(InputManagerKey); private readonly scenes = this.service(SceneManagerKey);
update(): void { if (this.input.isJustPressed("start")) { this.scenes.replace(new GameplayScene()); } }}
class TitleScene extends Scene { readonly name = "title";
onEnter() { const logo = this.spawn("logo"); logo.add(new Transform({ position: new Vec2(640, 200) })); logo.add(new SpriteComponent({ texture: LogoTex }));
this.spawn("controller").add(new TitleController()); }}
engine.scenes.push(new TitleScene());Reentrant scene swaps
Section titled “Reentrant scene swaps”push, pop, replace, and popAll are safe to call from inside any scene
lifecycle hook — onEnter, onExit, onPause, onResume, or a registered
beforeEnter / afterExit hook. The call is queued on the manager’s
internal pending chain and runs after the current mutation finishes. The
returned promise resolves once the deferred operation completes.
class TitleScene extends Scene { readonly name = "title";
onEnter() { // Skip the title screen if a saved game exists — fully safe inside onEnter. if (saveSystem.hasAutosave()) { this.context.resolve(SceneManagerKey).replace(new GameplayScene()); } }}In dev builds you’ll see a console.warn when this fires. Reentrant swaps
are usually a smell (an onEnter that immediately replaces the scene rarely
matches intent, and the dropped-on-the-floor promise can hide errors), so the
engine flags it — but it works correctly. Prefer doing the redirect from a
component’s update() or a one-shot Process for new code.