Log inGet started

Scenes

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…

Loading a scene

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

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:

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 hidden 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:

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:

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:

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:

local cam = layers.active.camera
cam.fov = 70
cam.behavior = cam.behaviors.thirdPerson    -- how the camera moves

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

  • Quickcam.behavior = cam.behaviors.X, where the common set is thirdPerson, firstPerson, topDown, menu, free, orbit.
  • Full catalog — the camera-controller components @builtin::controller.* (third_person, 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 / hidden as entity-level properties — they are entity modes, not components:

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) / editorUpdate(dt) — per frame in play / edit respectively.
  • 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:

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

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:

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.

  • documentation
  • guide