Skip to content

Scenes and the app lifecycle ​

A scene is one screen of your game: a title, a level, a pause menu. gamekit gives you three layers, and you can stop at any of them:

PartUse it for
AppSceneA base class with async enter, exit, suspend and resume hooks, focus, and an optional willow node.
SceneStackStarting, pushing, popping and replacing scenes. The only thing that runs scene hooks.
SceneCoordinatorA SceneStack plus app states (boot, load, active, suspended, shutdown, failure), clocks, input, asset scopes and saves registered per scene.
ts
import { AppScene, AppState, SceneCoordinator } from "gamekit";

class TitleScene extends AppScene {
    constructor() {
        super("title");
    }
    override async enter(): Promise<void> {
        console.log("title: enter");
    }
    override async exit(): Promise<void> {
        console.log("title: exit");
    }
}

class PlayScene extends AppScene {
    constructor() {
        super("play");
    }
    override async enter(): Promise<void> {
        console.log("play: enter");
    }
    override async suspend(): Promise<void> {
        console.log("play: suspend");
    }
    override async resume(): Promise<void> {
        console.log("play: resume");
    }
}

class PauseScene extends AppScene {
    constructor() {
        super("pause");
    }
}

const app: SceneCoordinator = new SceneCoordinator((state: AppState): void => {
    console.log("app state: " + state);
});

async function main(): Promise<void> {
    await app.boot(new TitleScene());
    await app.replace(new PlayScene());   // title exits, play enters
    await app.push(new PauseScene());     // play is suspended underneath
    await app.pop();                      // play resumes
    console.log("top scene: " + (app.scenes.top as AppScene).name);
    await app.quit();
}

await main();
text
app state: boot
app state: load
title: enter
app state: active
app state: load
title: exit
play: enter
app state: active
play: suspend
app state: active
play: resume
app state: active
top scene: play
app state: shutdown

SceneStack ​

CallWhat it does
start(scene)Enters the first scene.
push(scene, options?)Suspends the top scene and enters a new one above it.
pop(options?)Exits the top scene and resumes the one below. Popping an empty stack throws.
replace(scene, options?)Exits the top scene and enters a new one in its place.
suspend(), resume(), shutdown()Act on the whole stack.
top, count, scenes(), transitioningWhat is there now.
addObserver(observer)Watch changes; returns a registration to dispose.

Each change returns Promise<boolean>. Scene hooks are async, so a change can be in flight; a newer change cancels the older one through its SceneOperation, and the older call resolves false. A cancelled scene is exited and never becomes the top. If a scene's enter throws, the stack exits it and goes back to the scene it had.

A scene belongs to one stack at a time. Only the ready top scene has input enabled (isInputEnabled).

Scene nodes and screens ​

A scene can own a willow node: call attachNode(node) in its constructor. The stack then pauses that node when the scene is covered and disposes it when the scene exits, along with its tickers and tweens. That is how a WorldTicker on the node stops when you push a pause menu.

scene.screen is a container for the scene's screen-sized UI, shown, hidden and paused with the scene. ScreenLayers.scenes holds every scene's screen, and ScreenLayers.overlay is one app-wide layer drawn above them, for things like a toast or an FPS counter.

Transitions ​

push, pop and replace take an optional { transition: Transition }, using willow's Transition (for example Transition.fade(0.3)):

ts
import { Transition } from "willow";

await scenes.replace(new PlayScene(), { transition: Transition.fade(0.5) });

The transition plays between the leaving and arriving scene nodes. Input stays off for both scenes until it ends and the new scene is ready, and the old node is disposed only afterwards. Without the option, or when a scene has no node, the change happens at once. The scene-fade example shows this in a window.

SceneCoordinator ​

new SceneCoordinator(observer?) wraps a stack (coordinator.scenes) and reports an AppState to the observer. boot(scene) starts it; push, pop, replace, suspend, resume, save and quit drive it.

Register things against a scene and the coordinator keeps them in step with it:

CallWhat it does
registerClock(scene, clock)Pauses the clock (a core Ticker or a test clock) while the scene is not the ready top, and resumes it after entry or resume completes.
registerInputContext(scene, router, context)Pushes the scene's input context only while it is the ready top.
registerAssetScope(scene, scope)Disposes the asset scope when the scene exits.
registerSave(work)Runs this save work on save(), and drains it before quit() retires the last scene.

Each returns an AppRegistration; disposing it is safe more than once. If entering a scene fails, the coordinator restores the previous scene's input, clocks and focus, and moves to failure with the original error in coordinator.failure.

Focus ​

AppScene is a focus group (IFocusGroup): activateFocus(), blur(), isFocused, and handleInput(input) for keys the scene handles itself. With willowui windows on top, SceneWindowLayer decides which group has focus.

Reference: AppScene, SceneStack, SceneCoordinator, AppState, ScreenLayers, SceneOperation.