Documentation · how it works

The lifecycle of a scene

Everything that happens to a scene from the moment it starts until it leaves, in the exact order the engine does it.

In NacatamalOn a scene is a function. This page tells what the engine does with it: when it runs it, what happens every frame, what pausing it means, how you switch from one to another and what gets cleaned up when it leaves. Every step is read from the engine's code, in the order it happens.

The states of a scene

start · launch · changecreateScene()change with transitionat the swappause()resume()stop · change · destroystop · change · destroyit can start again, from scratchRegistereda name, not startedBeing bornits body runs onceHeldloading, not seen, not runningRunninguseUpdate every framePauseddrawn, not updatedStoppeduseSceneUnmount runs
A scene goes through these states. Only "Running" runs its useUpdate; "Running" and "Paused" are the ones on screen.

Being born: the body runs once

const Level = () => {
    // All of this runs ONCE, when the scene starts.
    const hero = useLoadTexture({ src: '/hero.png' });
    const player = createSprite({ texture: hero, transform: { x: 160, y: 120 } });
    const keys = useKeyboard();
 
    // This does not run now: it is registered, and the engine calls it every frame.
    useUpdate((dt) => {
        if (keys.isDown('ArrowRight')) player.transform.x += 120 * dt;
    });
 
    return createScene();
};

Three things about this moment matter:

At the end, createScene() closes the scene with everything declared and the engine adds it to the list of running scenes.

The game's first scene is the first key of the object, or the one you name:

const game = createGame('#app', { width: 320, height: 240 });
game({ Menu, Level });            // starts Menu
game({ Menu, Level }, 'Level');   // starts Level

One frame, in order

Sixty times a second, in this order:

  1. 1
    Inputalso while paused

    The clicks and touches that arrived since the last frame are handed to their listeners, and the gamepad is read. It happens here, at a moment the game chooses, and not whenever the browser fires them.

  2. 2
    Transitionalso while paused

    If a scene change with a transition is under way, it moves on. It never freezes: the screen always ends up uncovered.

  3. 3
    Updates

    Each running scene, in the order it was launched, runs its useUpdate callbacks with dt. Paused and held scenes are skipped.

  4. 4
    Keyboard closesalso while paused

    justPressed stops being true. A press counts for a single frame, and in that frame it counts for everyone who asks.

  5. 5
    Removalsalso while paused

    What was destroyed with destroy leaves now, while nobody is walking the scenes. It is never drawn one last time.

  6. 6
    Soundalso while paused

    The listener and the sounds placed in the world follow what is on screen.

  7. 7
    Drawingalso while paused

    Where everything is gets worked out and the frame is painted, scene by scene in the order they were launched.

With the game paused only step 3 is skipped: everything else carries on. That is why a pause menu can be seen and pressed.

dt is the seconds since the previous frame. It has two adjustments worth knowing:

If a useUpdate throws, the rest of its scene's updates for that frame are lost, nothing more: the other scenes carry on and the frame is drawn. The game reports it with its error event.

Inside the scene: who goes first

Scene1Player2Sword3Enemy4Bulletborn this frame: starts on the next
First the parent's useUpdate callbacks, in the order they were registered, then each child with its whole branch. Whatever is born during a frame does not move until the next one.

A scene is a tree of objects, walked from the top down: first the object, then its children, branch by branch. So when the sword updates, the player it hangs from has already moved this frame.

Two details that head off strange bugs:

Pausing is not stopping

PausedStopped
Its useUpdatedoes not rundoes not run
Drawnyesno
Its clicksdoes not reactdoes not react
useSceneUnmountdoes not runruns, once
How it comes backresume(), where it wasby starting it again, from scratch

A pause menu is exactly both at once: the level paused, still on screen, and a menu scene launched on top.

const Level = () => {
    const scene = useScene();
    const keys = useKeyboard();
 
    useUpdate(() => {
        if (keys.justPressed('Escape')) {
            scene.pause();              // the level freezes, but stays on screen
            scene.launch('PauseMenu');  // and the menu is drawn on top
        }
    });
 
    return createScene();
};
 
const PauseMenu = () => {
    const scene = useScene();
    const keys = useKeyboard();
 
    useUpdate(() => {
        if (keys.justPressed('Escape')) {
            scene.resume('Level');      // the level carries on where it was
            scene.stop();               // and the menu goes away
        }
    });
 
    return createScene();
};

The level cannot unpause itself: while paused, its useUpdate callbacks do not run. That is why the menu does it.

Changing scene

scene.change('Level2') replaces the scene that calls it with another one. There are two ways.

scene.change('Level2')
This frameThe next oneMenuStops on the spot: no longer drawn, and its useSceneUnmount runsLevel2Is born (its body runs) and is already drawnIts first useUpdate
The hard cut: the new scene is born first and the old one stops after it, in the same frame. So if the name does not exist, the error comes before anything stops.
scene.change('Level2', { transition: fade(300) })
CoverSwapUncoverMenuOn screen and still updatingStops: its useSceneUnmount runsLevel2Held: already born and loading, but neither seen nor updatedReleased: updates and is drawn, still under the coverOn screen
With a transition, the swap waits for the later of two things: the screen being covered, and everything the new scene asked to load having landed. The transition hides the cut and the loading as well.

Rules worth knowing:

Several scenes at once

Maplaunched later, on topLevellaunched first
Running scenes are drawn in the order they were launched: the last one on top.

scene.launch('Map') starts another scene alongside the ones already running, without stopping any. Each has its own updates, its own pause and its own cleanups, and the one launched later is drawn on top.

const Level = () => {
    const scene = useScene();
    const keys = useKeyboard();
    let mapOpen = false;
 
    // M opens the map over the level, and closes it.
    useUpdate(() => {
        if (keys.justPressed('m')) {
            if (mapOpen) scene.stop('Map');
            else scene.launch('Map');
            mapOpen = !mapOpen;
        }
    });
 
    return createScene();
};

A scene can only be running once: launching a name that is already running is an error. And scene.stop() with no name stops the scene that calls it, like a menu closing on its own button.

Leaving: cleaning up what is not the engine's

When a scene stops (with stop, with change, or because the whole game is destroyed), the engine walks its tree and runs its cleanups, children before parents: that way a sword's cleanup can still reach the player it hung from.

What the engine gave you goes away with the scene by itself: its sprites, its useUpdate callbacks, its listeners, its signals. Loaded images are kept, so the next scene that asks for them does not download them again.

What you started outside the engine stays alive unless you stop it: a setInterval, a listener on window, a request you want to abort. That is what useSceneUnmount is for. It runs once when the scene leaves (and not when it is paused):

const Level = () => {
    const onKey = (event: KeyboardEvent) => console.log(event.key);
    window.addEventListener('keydown', onKey);
 
    // Without this, the listener stays alive after the level ends and fires in the next scene.
    useSceneUnmount(() => window.removeEventListener('keydown', onKey));
 
    return createScene();
};

The whole game

Above the scenes sits the game, with its own beginning and its own end:

  1. createGame('#app', { … }) keeps the settings and returns the function that starts it.
  2. Called with the scenes, the engine picks what to draw with (WebGPU or, failing that, WebGL2).
  3. It registers each scene under its name.
  4. It starts the initial scene: its body runs.
  5. It draws the first frame, with dt equal to 0.
  6. It announces itself with its ready event.

Pausing the game is not pausing a scene. useGame().pause() freezes the whole game: no scene updates and dt is 0, but everything is still drawn and input is still read. The two pauses are independent: resuming the game does not resume a scene paused on its own, nor the other way round.

At the end, destroy() stops every scene (with all their cleanups) and the game announces its destroy event.

In one table

WhatWhen it runs
The scene's bodyOnce, when it starts
useUpdateEvery frame, while the scene is running
useSceneUnmountOnce, when the scene leaves (not when it is paused)
scene.changeCut: at once. With a transition: the old one leaves at the swap
scene.launchAt once, alongside the others and drawn on top
scene.pause / resumeAt once; the scene is still drawn
destroy(object)Stops updating at once, and leaves at the end of the frame
The game's ready eventAfter the first frame