---
title: "Scenes"
description: "A scene is a loadable arrangement of a world — its entities (with their components, transforms, and lighting), plus a startup script — saved as a .scene asset. A scene is the unit of \"level\" or…"
section: "Core"
slug: "core-scenes"
canonical: "https://origozero.ai/docs/core-scenes"
updated: "2026-09-03T15:00:41.722195231+00:00"
tags: ["documentation", "guide"]
---

# Scenes

## Loading a scene

`layers.load(ref, opts?)` loads a scene; `ref` is a scene AssetRef or its identity string:

```lua
layers.load("@builtin::_templates.blank_app.blank_app")   -- by identity
layers.load(asset.ref("@my_world::lobby"))                  -- by ref
```

Loading a scene as the **root** replaces the current root (despawning its non-persistent entities), instantiates the scene's entities, applies its lighting / camera / player setup, and runs its `entrypoint.luau` once. The current root is `layers.active`.

## Root + additive layers

A scene loads either as the **root** (the main scene) or as an **additive overlay** on top of it:

```lua
layers.load(asset.ref("@my_world::hud"), { additive = true })   -- overlay, rides on the root
```

There is exactly one root at a time, plus any number of additive overlays (a HUD, a pause menu). Additives ride on the root and unload with it. `layers.list()` enumerates what's loaded; `layers.onLoad(cb)` reacts to loads.

The root is special: it owns the live players registry and the play-mode camera below. Additive overlays don't.

## The player intent — how a scene handles players

A scene declares **who plays it** with a single string `player` intent in its `scene.json`:

- **`"spawns"`** — the scene spawns a player for each joining user from the **PlayerSpawns** you author into it. The canonical player scene.
- **`"none"`** — the scene has no player. Author a **Camera** entity for its play-mode view. Use this for UI-only screens, menus, cutscenes, and dashboards.

Everything about players and cameras lives **as authored entities** in the scene's scenegraph. A scene that wants a player says so with `"spawns"` and carries the PlayerPrototype + PlayerSpawn structure below; a scene that doesn't says `"none"` and authors whatever Camera it needs.

## Authoring a player: PlayerPrototype + PlayerSpawn

A `"spawns"` scene is built from three authored pieces:

- A **PlayerPrototype** — an entity (with the `PlayerPrototype` component) that roots a player-setup subtree. It's a **template**, not a live player: in edit mode it's the live authored original; in play mode the engine clones it for each joining user.
- A **body** — the entity named by the prototype's `PlayerPrototype.body` ref (a child of the prototype). It's whatever the author placed there: a bundle-backed entity (an `Asset` pointing at an avatar bundle like `@builtin::avatars.humanoid`), a hand-authored tree, or an empty entity whose own component builds the visual. On spawn the clone's body is adopted as the joining user's avatar.
- A **CameraRig** — a `Camera`-component entity under the prototype, scoped **OwnerOnly** so each user gets their own view. On spawn it follows the user's bound avatar.

A **PlayerSpawn** — an entity with the `PlayerSpawn` component — defines *where* players spawn and *which* prototype to clone. Its fields:

```
prototype     -- an entityRef to the PlayerPrototype entity to clone
team / role   -- optional labels for the spawned player
maxPlayers    -- cap on players from this spawn
spawnPolicy   -- how repeat spawns are handled
placement     -- "at_spawn_transform" places the clone at the PlayerSpawn's transform
```

On join, the PlayerSpawn clones its prototype; the clone root becomes the joining user's internal identity (it folds into the players registry below), the clone's body (named by `PlayerPrototype.body`) is adopted as the user's avatar, and the prototype's CameraRig follows that avatar. The engine wires this to the connected-user join event automatically for any `"spawns"` root scene — you author the structure, the join flow runs itself.

The built-in `_canonical` scenes are the worked shapes to copy: `@builtin::_canonical.static_player` is a complete `"spawns"` scene (PlayerSetups → DefaultSocialPlayer prototype with a humanoid body + CameraRig, Spawns → DefaultSpawn), and `@builtin::_canonical.no_player` is the `"none"` shape.

## The players registry

Reach the live players through the root scene's registry:

```lua
local me = layers.active.players.localPlayer        -- the player THIS client controls
layers.active.players:count() / :list()             -- how many / all of them
layers.active.players:onLocalReady(function(p) end) -- fires once the local player is ready
layers.active.players:onJoin(function(p) end)       -- :onLeave too
```

`localPlayer` is the multiplayer-safe way to get "this client's player"; the rest are remote. The registry lives on the **root** (`layers.active.players`) — additive overlays don't have one.

A player's body — both how it *looks* and how it *moves* — is its **avatar**: a live world entity carrying a visual mesh together with a movement controller. It comes from the scene's **`PlayerPrototype.body`** — every connecting player gets a clone of that authored body (see `man scene`). You build a body from an **`avatar`** asset; the built-in ones are `@builtin::avatars.{humanoid, minimal_player, editor_pill, first_person, controller_only}` (`humanoid` is a full character + walk/run controller; `editor_pill` is a capsule with a free-fly mover).

`localPlayer.avatar` is this client's live body entity (a ref). To swap it at runtime, spawn a body and bind the entity — a `.bundle` / `.avatar` asset is not a world entity, so compose it onto an entity first:

```lua
local body = entity.spawn("hero")
body.component.add("Asset", { source = asset.ref("@builtin::avatars.humanoid") })
localPlayer.avatar = body   -- bind the live entity (assign nil to clear)
```

To make **your own** imported character the body every player spawns with —
without editing any built-in content — wrap it in an **`avatar`** asset and point
the scene's `PlayerPrototype.body` at a body entity whose `Asset` source is that
avatar. After importing a rigged character (the importer leaves a `.bundle`), one
call composes a playable avatar from it:

```lua
local body = asset.resolve("my_character", "bundle")     -- the imported character bundle
local hero = asset.create("avatar", "my_hero", { body = body })
-- in the scene, set the PlayerPrototype's body entity Asset.source = hero
```

A **humanoid** body automatically gets the shared retargeting locomotion (any
humanoid clip set plays on it, retargeted onto its rig) plus the standard
character controller — no per-body setup. A **non-humanoid** body (a vehicle, a
custom shape) is just as valid: it takes the controller and animation you supply.
The `avatar` asset is the adapt-the-base-character primitive — see `man avatar`.

## The camera & its controller

The play-mode camera is the prototype's **CameraRig** — a `Camera`-component entity scoped **OwnerOnly**, so each peer owns its own (a camera is never synced). It's authored into the PlayerPrototype and follows the user's avatar on spawn. A `"none"` scene has no player, so it carries a Camera entity of its own for its play-mode view.

Reach the active play-mode camera as `layers.active.camera`. It is a handle on
the camera's settings, and `cam.entity` is the entity ref for the camera itself
— the same shape `layers.active.players.localPlayer.avatar` hands back for the
player's body:

```lua
local cam = layers.active.camera
cam.fov = 70
cam.behavior = cam.behaviors.orbitalFollow    -- how the camera moves

cam.ready                                     -- is a camera loaded yet
cam.entity                                    -- the camera's entity ref
cam.entity.position                           -- where it is
cam.entityId                                  -- that entity's id string
```

How the camera moves is its **controller**. Two ways to choose one:

- **Quick** — `cam.behavior = cam.behaviors.X`, where the common set is `orbitalFollow, thirdPersonFollow, firstPerson, topDown, menu, free, orbit`.
- **Full catalog** — the camera-controller components `@builtin::controller.*` (`orbital_follow, third_person_follow, first_person, free, orbit, birds_eye, chase, cinematic, isometric, rts, side_scroller, menu`).

A follow-style controller tracks the local player's avatar by default; `cam.follow` makes it follow a specific entity.

## Entity lifecycle & replication properties

Two persisted entity-proxy properties control how a player-setup subtree behaves across modes and across peers. They sit alongside `temporary` / `internal` as entity-level properties — they are entity **modes**, not components:

```lua
entity(id).participation                        -- "WorldEntity" (default) | "PrototypeOnly" | "EditorOnly" | "RuntimeOnly"
entity(id):setParticipation("PrototypeOnly")    -- a template: live in edit, cloned (not live) in play
entity(id).networkScope                         -- "Replicated" (default) | "OwnerOnly" | "AuthorityOnly"
entity(id):setNetworkScope("OwnerOnly")         -- clone reaches only the owning peer
```

- **`participation`** is the lifecycle axis. A **PrototypeOnly** entity is a template — live while you author in edit, deactivated in play (the engine clones it per joining user instead of running the original). **EditorOnly** entities exist only in edit; **RuntimeOnly** entities exist only at runtime and are never saved. A PlayerPrototype root is authored as `PrototypeOnly`.
- **`networkScope`** decides what replicates when a prototype is cloned on join: **OwnerOnly** nodes (like the CameraRig) reach only the peer that owns that player; **AuthorityOnly** reach only the simulation authority; **Replicated** (the default) reach everyone. The clone is pruned by each node's scope against the joining user's role.

## The entrypoint

Each scene has an `entrypoint.luau` that runs when the scene loads. It's where per-scene wiring lives: dynamic spawns, event listeners, hooking up the local player. Top-level functions in it are picked up as **scene hooks**, several of them mode-specific:

- `onLoad()` — fires on load **in play mode** (gameplay start).
- `onEditLoad()` — fires on load **in edit mode** (authoring / editor setup).
- `onUnload()` — fires on unload, always.
- `update(dt)` — per frame in play mode, while the gameplay clock runs; `editorUpdate(dt)` — per frame in edit mode, running or paused. Play-paused ticks neither. This is the scene loader's own dispatch and it partitions strictly on mode; a *component's* `update` reads the clock rather than the mode, so the two rules differ in edit-running and in play-paused (`core/components` has that table).
- `localPlayerReady(player)` — once this scene's local player is ready (the place to position it or set its avatar); `playerJoined(player)` / `playerLeft(id)` cover remote players. (Root scenes only.)

**Entities the entrypoint spawns are automatically marked `temporary`, so they're never written into the scene** — the entrypoint recreates them on every load, and baking them in too would double them up. This holds however you spawn them — directly via `entity.spawn`, indirectly through a module helper the entrypoint calls, or from a deferred `task.spawn` the entrypoint starts: the marker is inherited from the entrypoint down the call stack at spawn time, not snapshotted in a window. So an entrypoint is the right home for spawned-on-load content: it stays out of `scene.json` for free. Pass `entity.spawn(name, { temporary = false })` to opt one entity out of that default.

## Saving and editing

A scene is content you edit live: load it, arrange entities, then `layers.active:save()` writes the current scenegraph back into the scene's `scene.json`. While you edit, the entities you spawn and move **are** part of the scene — they're tracked live and synced to everyone — and `:save()` is what consolidates that live scenegraph into the scene file so it loads back the same way. Saving captures entity **data**, not your `entrypoint.luau` (that's preserved).

Mark an entity **temporary** to keep it out of the saved scene:

```lua
entity(id).setTemporary(true)   -- this entity (and its subtree) is excluded from :save()
```

Use it for runtime helpers you don't want baked into the scene (debug gizmos, transient props). Entities your `entrypoint.luau` spawns already get this **automatically** (see the entrypoint section) — so reach for `setTemporary` mainly for things you spawn outside the entrypoint. Everything else you place is saved.

## Authoring a scene

```lua
asset.create("scene", "Lobby")   -- → /zero/source/Lobby.scene/ (scene.json + entrypoint.luau + README.md)
```

A new scene starts as a `"spawns"` scene with the canonical PlayerPrototype + PlayerSpawn structure already in place, so it boots into a playable player + camera. For a UI-only screen, set its `player` intent to `"none"` and author a Camera entity instead. Set the world's startup scene in `.world_settings` so the world boots into it.

Scenes authored against an earlier format still load unchanged. To bring one you own up to the current model, run the user-initiated migration:

```lua
tools.use("scene", "migrate_v6_to_v7", { path = "/source/MyOldScene.scene/scene.json" })   -- or { ref = "@my_world::MyOldScene" }
```

It bumps the version, sets the `player` intent, and — for a scene that had a player — injects the PlayerSetups / Spawns structure, leaving every existing entity and its lighting untouched.

## Finding the rest

`asset.list("scene")` lists every scene; `asset.inspect("<name>")` summarises one; the built-in scenes and demos under `@builtin` are worked examples to load and read — `@builtin::_canonical.static_player` (a `"spawns"` scene) and `@builtin::_canonical.no_player` (a `"none"` scene) are the reference shapes. `layers`, `players`, and `camera` are authored modules under `modules/api/engine/` — read them for the full surface. The player/avatar and edit-vs-play model are covered in the components and engine guides.
