# Tutorial: Play, Edit, and scenes

Editor mode and scene loading are the most common sources of “my script did nothing” / “Stop wiped my level.” This page covers the rules you need day one.

Deep reference: [Application lifecycle](/docs/systems/lifecycle.md), [Session services](/docs/systems/session-services.md), [UI and editor](/docs/systems/ui-and-editor.md) (including optional [game editor SDK](/docs/systems/ui-and-editor.md#game-editor-sdk-hieditor) for menus/panels).

## Play versus Edit

| | Edit | Play |
|---|---|---|
| Scene rendered | Yes | Yes |
| Entity `update` / physics / scripts | Frozen | Running |
| Authoring source | `SceneDocument` | Live `World` (preview only) |
| Stop | — | Switches back to the retained authoring world; unsaved document edits survive |

> [!WARNING]
> Play runs a preview world. Changes made there are not authored scene edits: **Stop** restores the in-memory scene document, not the Play world or the last play-loaded scene. Make changes you want to keep in **Edit** mode.

In-editor **Quit** from game UI stops Play; it does not close the editor window. Standalone games still exit the process.

Asset hot reload is deferred while Play is active.

### Primary camera preview (Edit)

The camera toolbar button previews the primary camera as a complete lens:
transform, projection, depth of field, shutter, vignette, aberration, and flare.
Toggle it again to return to the editor fly-cam exactly where it was parked.
The fly-cam itself keeps neutral optics and samples spatial Visual Zones at its
own position; preview mode samples them at the primary camera. Preview never
moves or otherwise mutates the camera actor.

### Pause and unpossess (editor Play only)

Two **independent** toolbar knobs next to Play/Stop. Policy lives on the session as `HostPolicy` (`session.hostPolicy()`):

| Query | Meaning |
|---|---|
| `isPlaying` | Session is in Play |
| `isSimPaused` | Host freeze — `World.sim_hold` / `shouldAdvanceSim() == false` |
| What freezes | Entity / script / swarm / fixed ticks, physics integrate, session-script `update`; `world.deltaTime()` is 0 |
| What keeps running | Render, UI begin, `GameSubsystem.onTick` (single hook), fly-cam when unpossessed |
| Game clocks | Sim → `world.deltaTime()` (0 while paused). Chrome → `world.unscaledDeltaTime()` / wall time. Never wall-clock for sim |
| `world.timeScale()` | Game-owned multiplier only — does **not** include host pause |
| `isViewDetached` | Editor fly-cam owns presentation; **primary camera stays the game camera** |
| `isPlayPending` / `session.isPlayPending()` | Play was clicked; its world copy is still building |
| `canAuthor` / `isAuthoringReadOnly` | Document + inspector writes / undo when **not** playing and **not** starting |

Toolbar:

| Control | Effect |
|---|---|
| **Pause** | Toggles `isSimPaused` |
| **Unpossess** (camera) | Toggles `isViewDetached` |

You can unpossess while the sim still runs, pause while still looking through the player cam, or both. Repossess / resume are the same buttons toggled off. Stop clears both.

**Dual view (unpossess):** freecam is the **viewport lens** (image VP, TAA) — where you look from. The **game primary** still owns **GPU frustum cull** (so you can orbit and see what the player view drops), plus reaction: HostApi `primaryCamera`, audio listener, visual-zone look (fog/exposure), shadow focus, game LOD. That is the point of unpossess as a cull/reaction debugger.

While detached, freecam navigation may temporarily capture the OS pointer, but **game-facing input is scrubbed**: `Input.pointer_captured()` is false and `mouse_delta` is zero for world/entity/session ticks. Action resolve is also held. That keeps scripts that key look off capture+delta from rotating the primary (and the camera gizmo) with the editor freecam.

While unpossessed, the viewport overlay is Scene-view-style over the live Play world: same show-flags as Edit (grid, cameras, lights, …), live poses each frame. Mass render-only actors stay undecorated. Inspector / document history stay observation-only while `isAuthoringReadOnly`.

---

## Replace scene load

From Zig (session HUD or entity):

```zig
const hi = @import("hikari_game");
const world = hi.world();

const load = world.requestSceneLoad(hi.SceneRef.must("scenes/messaging"), .{}) catch return;
_ = world.releaseSceneLoad(load);
world.requestSceneReload();
const snap = world.sceneLoadSnapshot();
// snap.ready_stage: none → entities → assets → gpu → all
// Reveal the level only when ready_stage == .all (assets + GPU, not just actors spawned).
```

From Kawa:

```kawa
Scene.load("scenes/messaging");
Scene.reload();
// Scene.ready_stage() → 0 none, 1 entities, 2 assets, 3 gpu, 4 all
```

Requests are queued and applied on the next session tick — not mid-`update`. Load-queue `status == ready` only means actors spawned; wait for readiness `all` before showing the scene.

| Stage | Meaning |
|---|---|
| `none` (0) | No layers |
| `entities` (1) | Actors spawned (`on_scene_loaded`) |
| `assets` (2) | Soft-pending meshes/materials resolved |
| `gpu` (3) | Per-layer GPU resident |
| `all` (4) | World — every layer at `gpu` |

Full rules: [Session services — readiness](/docs/systems/session-services.md#readiness-stages-show-the-scene-gate).

The session HUD’s **Tour** buttons cycle the showcases listed in `scene_catalog.zig`. **Browse scenes** exposes all standalone examples under Showcase tour, Tutorials, and Diagnostics. The UI showcase also has an **Examples** button on its title screen. Streaming cells are loaded by their tutorial and are not standalone entries.

---

## Additive layers

```zig
const hi = @import("hikari_game");
const messaging = hi.SceneRef.must("scenes/messaging");
const world = hi.world();

const cell_load = world.requestSceneLoad(messaging, .{ .mode = .additive }) catch return;
_ = world.releaseSceneLoad(cell_load);
world.requestSceneUnloadLayer(messaging.path);
const layer = world.sceneLayerReadiness(messaging.path); // terminal stage = .gpu
```

Kawa: `Scene.load_additive("scenes/messaging")`, `Scene.unload_layer("scenes/messaging")`, `Scene.layer_ready_stage("scenes/messaging")`. Kawa and the host strip optional `.json` / `.shinscene` suffixes; layer keys are extension-free stems.

`Scene.load` and `Scene.load_additive` take the same presentation policy as the
Zig API, as an optional second argument (`"immediate"` / `"gated"` / `"deadline"`
/ `"manual"`), with `deadline` taking its milliseconds third. An unrecognised
name refuses the load rather than falling back to `immediate` — a silently
ignored gate looks like an engine bug, a scene that never loads points at its own
call.

```kawa
let menu_ready = false;

fn start() {
    Scene.load_additive("scenes/menu", "manual");
}

fn on_layer_ready(key) {
    if (key == "scenes/menu") { menu_ready = true; }
}

fn update(dt, total_time) {
    if (menu_ready) {
        Scene.present_layer("scenes/menu");
        menu_ready = false;
    }
}
```

This is a **session script**: attach it through `GameSubsystem` and call `updateSessionScript()` from `onTick`. It presents the layer on the first update after readiness; add your own loading-screen condition if needed.

Rules:

- Layer key is the request path string.
- Entity `"id"` values must be **unique across all loaded layers** or the load fails.
- Unloading a layer destroys entities bound to that layer; loose spawns (`.layer = .loose`) remain until replace unload / termination.
- World `ready_stage == .all` only when **every** loaded layer (base + additive) is at `gpu`.

---

## Stop semantics (editor)

1. Author scene A in Edit (unsaved edits are fine).
2. Play, then load scene C via gameplay.
3. Stop → renderer and tools switch to the retained authoring world for scene A. Unsaved A survives; play-only C does not become the document.

### Both directions are budgeted, and neither blocks the click

The editor keeps its authoring world intact during Play, so Play and Stop are world switches rather than scene reloads. Neither one stalls the frame it happens on:

| Phase | What happens |
|---|---|
| Click Play | Serialize the document, create the Play world + its physics backend, queue its scene job. Returns immediately |
| Starting… | The job advances on the normal scene-load budget while the **authoring world keeps rendering and taking input** |
| Commit | Renderer, physics, HostApi and input rebind to the copy; the first Play frame is that same tick |
| Click Stop / Quit-from-Play | Stop physics/audio, fire `onPlayEnded`, rebind presentation to the retained authoring world before returning |
| After Stop | Retire the invisible Play world in bounded slices; no authored actors are reconstructed |

There is no visible restore window at Stop: `canAuthor` is true again the moment it returns.

There *is* a visible starting window at Play, and it is deliberately visible. While the copy builds:

- the Play button reads **Starting…** and clicking it again withdraws the request
- `canAuthor` is false — the document Play will run was captured at the click, so an edit now would be silently missing from the session about to start
- opening another scene or prefab withdraws the request too

```zig
// Host / tooling only — games use HostPolicy, not a special API.
if (session.isPlayPending()) {
    // The world copy is still building; do not author into the document.
}
```

`onSceneLoaded` runs when the Play copy is built; Stop does not emit it for the already-live editor world. Pressing Play while a *previous* copy is still retiring closes that copy first rather than queueing a hidden intent.

Never author into packaged `bin/.../data/scenes` — use the project assets root.

---

## Verify

1. Edit a transform, do not save, Play, Stop — edit still present.
2. Play → Next Scene → Stop — document scene returns, not the play destination.
3. Additive load messaging → unload layer → pad/doors gone; session HUD still there.
4. Large scene / many actors: Play stays responsive while **Starting…** — the authored viewport still renders and the fly-cam still moves. Stop returns to that viewport immediately, and cleanup continues invisibly without changing editor-world stats.
5. Click Play then click it again before it commits — the request withdraws and the button returns to **Play** with nothing else changed.

## Next

- [First session services](/docs/tutorials/first-session-services.md)
- [Tutorials index](/docs/tutorials.md)
