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:
| Store | Lives for | Cleared when | Use for |
|---|---|---|---|
Local script variables (this.x) | One behaviour instance | Scene load, restart, or Stop | Per-object working state |
api.state (session bag) | The whole play session | The game reloads, or Stop | Score, lives, current level |
api.save / api.load | Forever (localStorage) | The player clears browser data | High scores, unlocks, options |
Scenes
A game ships a library of named scenes - a menu, levels, a credits screen. Scripts switch between them.
| Member | Signature | Description |
|---|---|---|
loadScene | (name): void | Switch to a named scene from the library. |
reloadScene | (): void | Restart 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 loadThe 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.
| Member | Signature | Description |
|---|---|---|
save | (key, value): void | Persist a JSON-serialisable value under a per-game key. |
load | (key): unknown | Read 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),
savedoes nothing andloadreturnsundefinedrather 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
- ScriptApi Reference -
loadScene,save/load, andstate - Scenes - authoring the scene library
- Events & Timers - what resets on a scene load
- UI from Scripts - menu buttons that switch scenes
- Saving & Opening Projects - the project file vs game save data