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, Session services, UI and editor (including optional game editor SDK 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 | — | Re-instantiates from document UTF-8, not disk, not the last play-loaded path |
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 cascade 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):
const hi = @import("hikari_game");
const world = hi.world();
world.requestSceneLoad(hi.SceneRef.must("scenes/messaging"), .{});
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:
Scene.load("scenes/messaging");
Scene.reload();
// Scene.ready_stage() → 0 none, 1 entities, 2 assets, 3 gpu, 4 allRequests 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.
The session HUD’s Tour buttons cycle eight showcases from 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
const hi = @import("hikari_game");
const messaging = hi.SceneRef.must("scenes/messaging");
const world = hi.world();
world.requestSceneLoad(messaging, .{ .mode = .additive });
world.requestSceneUnloadLayer(messaging.path);
const layer = world.sceneLayerReadiness(messaging.path); // terminal stage = .gpuKawa: 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.
Scene.load_additive("scenes/menu", "manual")
fn on_layer_ready(key) # resident and uploaded, deliberately not shown
if key == "scenes/menu" then menu_ready = true end
end
fn on_tick(dt)
if menu_ready and splash_done then
Scene.present_layer("scenes/menu") # opens the gate; fires on_layer_loaded
end
endRules:
- 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 == .allonly when every loaded layer (base + additive) is atgpu.
Stop semantics (editor)
- Author scene A in Edit (unsaved edits are fine).
- Play, then load scene C via gameplay.
- 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
canAuthoris 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
// 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
- Edit a transform, do not save, Play, Stop — edit still present.
- Play → Next Scene → Stop — document scene returns, not the play destination.
- Additive load messaging → unload layer → pad/doors gone; session HUD still there.
- 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.
- Click Play then click it again before it commits — the request withdraws and the button returns to Play with nothing else changed.