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() { // another scene was pushed on top }
onResume() { // the scene on top was popped, this scene is active again }}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()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.