Skip to content

Scenes & Persistence

Scripts move the player between named scenes and remember things - either for the session (the state bag) or forever (save/load). This page covers all three.

A finished game is more than one scene, and it needs to remember score across a level change and a high score across a browser restart. Awaken gives you three storage lifetimes, each with a clear boundary:

StoreLives forCleared whenUse for
Local script variables (this.x)One behaviour instanceScene load, restart, or StopPer-object working state
api.state (session bag)The whole play sessionThe game reloads, or StopScore, lives, current level
api.save / api.loadForever (localStorage)The player clears browser dataHigh scores, unlocks, options

Scenes

A game ships a library of named scenes - a menu, levels, a credits screen. Scripts switch between them.

MemberSignatureDescription
loadScene(name): voidSwitch to a named scene from the library.
reloadScene(): voidRestart the current scene - e.g. Restart from a Game Over screen.

Transitions are deferred

Requesting a scene load does not swap the world immediately. It queues the transition, which is applied at the top of the next frame, before any systems run. This guarantees scripts never see a half-swapped world mid-frame.

When the swap happens, the old scene's script host, physics sim, and UI handlers are torn down, the new scene's entities are built, and its systems restart - exactly as if you had just pressed Play on it. Shared assets (meshes, textures) stay resident on the GPU across the swap.

ts
// exit trigger
onCollisionEnter(api: ScriptApi, other: Entity) {
  if (api.world.getName(other) === "Player") api.loadScene("Level2");
}
ts
// game-over restart button
onStart(api: ScriptApi) {
  api.ui.onClick("restartButton", () => api.reloadScene());
}

reloadScene restarts the scene currently loaded - which advances every time you loadScene, so it always restarts wherever you are now (at Play start that is the scene you pressed Play on). loadScene with an unknown scene name is a silent no-op.

The session state bag

api.state is a plain object shared by every script this session. Unlike a local this. field - reset every time a behaviour instance is rebuilt - state survives scene loads. That makes it the right home for anything that must carry across levels.

ts
// somewhere in Level1
api.state.score = (api.state.score ?? 0) + 10;
api.state.level = 1;

// after loadScene("Level2"), a script there still reads:
api.setHud(`Score ${api.state.score}`);  // preserved across the load

The bag is in-memory: it survives scene transitions within a run, but is cleared when the game reloads the page - and, in the editor, when you press Stop (so the next Play starts clean). Persisted saves are untouched by Stop.

Saving across reloads

save and load persist JSON-serialisable values to the browser's localStorage, namespaced per game so two games on the same host never collide.

MemberSignatureDescription
save(key, value): voidPersist a JSON-serialisable value under a per-game key.
load(key): unknownRead it back, or undefined if never saved.
ts
// record a high score at game over
onStart(api: ScriptApi) {
  const best = (api.load("highScore") as number) ?? 0;
  const score = (api.state.score as number) ?? 0;
  if (score > best) {
    api.save("highScore", score);
    api.ui.setText("newRecord", "New record!");
  }
  api.ui.setText("bestLabel", `Best: ${Math.max(best, score)}`);
}

Notes:

  • JSON only. Values are JSON.stringify-ed, so pass numbers, strings, arrays, and plain objects - not entities or functions.
  • Failures are swallowed. If storage is full or blocked (private mode), save does nothing and load returns undefined rather than throwing.
  • Survives everything short of clearing browser data - page reloads, closing the tab, restarting the browser.

Worked example: level transition with saved progress

Two scripts - a level-exit trigger that banks progress, and a boot script that resumes it:

ts
// exit.ts - on the goal object of each level
export default class implements ScriptBehavior {
  nextScene = "Level2";        // string field → editable per instance
  onCollisionEnter(api: ScriptApi, other: Entity) {
    if (api.world.getName(other) !== "Player") return;
    api.state.level = (api.state.level as number ?? 1) + 1;
    api.save("furthestLevel", api.state.level); // remember for next session
    api.loadScene(this.nextScene);
  }
}
ts
// title.ts - on the menu scene
export default class implements ScriptBehavior {
  onStart(api: ScriptApi) {
    const furthest = (api.load("furthestLevel") as number) ?? 1;
    api.ui.setText("continueLabel", `Continue - Level ${furthest}`);
    api.ui.onClick("continueButton", () => {
      api.state.level = furthest;              // seed session state
      api.loadScene(`Level${furthest}`);
    });
    api.ui.onClick("newGameButton", () => {
      api.state.level = 1;
      api.loadScene("Level1");
    });
  }
}

state.level carries the current level through the run; save("furthestLevel", …) remembers the best reached across runs so the title screen can offer Continue.

See also

Awaken — browser-native WebGPU game engine.