Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions content/engine/scene-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ The current scene schema is version 2. Version 1 migrates in memory; future sche

## Editor and runtime ownership

Rust hosts can use [`SceneManager`](/docs/guides/scene-lifecycle) to explicitly
load worlds, switch only to loaded worlds, and drop worlds on unload. Loading
Scripts use [`game.scene`](/docs/guides/scene-lifecycle) to load scenes, switch
to saved scenes with automatic loading, and unload their worlds and runtime resources.
Scenes automatically loaded by switching unload on switching away; explicitly
loaded scenes remain resident. Loading
does not activate a scene; unloading the active scene clears the selection.
This API manages world ownership; hosts coordinate scripting, physics, audio
and rendering separately.
Global scripts remain available across transitions. Rust hosts can also use
`engine_world::SceneManager` for lower-level ECS world ownership.

The editor owns the authored document. Play starts from an immutable staged snapshot and creates a separate runtime world. Stopping Play does not automatically overwrite the authored scene; the runtime-change review/apply operation is explicit and undoable.

Expand Down
6 changes: 4 additions & 2 deletions docs/Scripting/sceneObjects.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ return {
to return to the authored scene, or use the editor's runtime-change workflow
when you want to retain supported changes.

Only the currently loaded scene is accessible. Referring to another scene does
not load it. Names are case-sensitive. Paths are resolved when a call applies;
Only the active scene is accessible through object paths. Other loaded scenes
retain their state until selected with `game.scene.switchTo(name)`. Use the
[scene lifecycle API](/docs/guides/scene-lifecycle) to load and unload them.
Referring to another scene does not load it. Names are case-sensitive. Paths are resolved when a call applies;
if you rename or reparent an object, use its new path in subsequent calls.
These functions edit engine attributes, not another script's public properties
or arbitrary user-defined functions.
Expand Down
241 changes: 191 additions & 50 deletions docs/scene-lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,196 @@
# Load, unload and switch scenes (Rust)

`engine_world::SceneManager` owns explicitly loaded scene worlds. Use this API
in a Rust host with a path dependency on `crates/engine-world`. It is not exposed
as a Lua, JavaScript, C, C++, C# or Python script function. No script attachment
is required. Editor Play still uses its existing single-scene runtime.

```rust
use engine_world::{SceneManager, SceneManagerError};

fn main() -> Result<(), SceneManagerError> {
let mut scenes = SceneManager::new();
let menu = scenes.load_file("assets/menu.rscene")?;
let level = scenes.load_file("assets/level.rscene")?;
scenes.switch(menu)?;
scenes.switch(level)?;
scenes.unload(menu)?;
assert!(!scenes.is_loaded(menu));
assert!(scenes.switch(menu).is_err());
assert_eq!(scenes.active_id(), Some(level));
Ok(())
# Load, unload and switch scenes

Use `game.scene` to manage scenes from Lua 5.4, Luau, JavaScript, Python, C++,
C#, Java, PHP and HTML with inline JavaScript. C uses `Game.scene`.
The scene open when Play starts is already loaded and active.

## Set up and run

1. Save two scenes with unique document names, such as **Menu** and **Level**.
Save Level as `scene/level.scene` (legacy `scenes/*.rscene` also works).
Scene names and scene IDs must both be unique among loaded scenes.
2. Open Menu. Create a **Global Startup Script** through the Programming menu
and select a language. Paste the matching example into its Start/on_start
callback, keeping the generated imports and callback registration.
Object component and scene startup scripts can call these APIs too; they
belong to their scene and end when that scene is unloaded. Global scripts
remain alive and can load a scene even when none is active.
3. Save all target scenes before pressing **Play**. Play snapshots include scene
files from `scene/` and `scenes/`; keeping a file on disk does not load its
world into runtime memory. Edits made on disk after Play starts require a
new Play session. The initially open scene includes its current editor state.
4. Run the example. Level becomes the active scene on the next scene transition
boundary. Menu is unloaded, its scene/object scripts are destroyed, and its
world, physics, decoded audio, capture state and script runtimes are released.
Level must contain a camera to produce a camera view.

```lua
return {
Start = function()
local level = game.scene.load('scene/level.scene')
assert(game.scene.isLoaded(level))
game.scene.switchTo(level)
game.scene.unload('Menu')
assert(not game.scene.isLoaded('Menu'))
end,
}
```

Replace the paths with valid saved scene files. The example leaves the level
active and loaded, and releases the menu world. `load(SceneDocument)` also
accepts an owned document created or loaded by the host.
```javascript
// JavaScript, or inside a <script> in a ui/ HTML asset.
globalThis.behavior = {
Start() {
const level = game.scene.load('scene/level.scene');
game.scene.switchTo(level);
game.scene.unload('Menu');
}
};
```

Use the same Lua example for Luau. Other language callback bodies:

```python
# from rustic import game, run
# Register with run(globals()) after defining callbacks.
def on_start():
level = game.scene.load('scene/level.scene')
game.scene.switchTo(level)
game.scene.unload('Menu')
```

```cpp
// #include "rustic.hpp"; register start with RusticBehavior.on_start.
void start() {
auto level = game.scene.load("scene/level.scene");
game.scene.switchTo(level);
game.scene.unload("Menu");
}
```

```csharp
// using static Rustic; inside the generated on_start callback:
var level = game.scene.load("scene/level.scene");
game.scene.switchTo(level);
game.scene.unload("Menu");
```

```c
// #include "rustic.h"; register start with RusticBehavior.on_start.
void start(void) {
const char *level = Game.scene.load("scene/level.scene");
Game.scene.switchTo(level);
Game.scene.unload("Menu");
}
```

```java
// Inside a Rustic subclass's on_start callback:
String level = game.scene.load("scene/level.scene");
game.scene.switchTo(level);
game.scene.unload("Menu");
```

```php
// require rustic.php and register on_start with rustic_run.
function on_start(): void {
global $game;
$level = $game->scene->load("scene/level.scene");
$game->scene->switchTo($level);
$game->scene->unload("Menu");
}
```

| Operation | Result |
See the [language guides](/docs/guides/api-languages) for complete SDK setup.
`Game.scene` also exposes these methods in each language. CSS alone has no
callbacks; HTML runs the JavaScript API. PHP and HTML remain UI script types.

## Automatic loading when switching

`switchTo` also accepts a saved scene that is not in memory. Use its document
name, or pass its relative file path directly:

```lua
game.scene.switchTo('Level') -- finds the saved document in scene/ or scenes/
assert(game.scene.isLoaded('Level'))
game.scene.switchTo('Menu') -- automatically unloads Level
assert(not game.scene.isLoaded('Level'))
```

The same calls work with native member syntax in the other languages. C uses
`Game.scene.switchTo("scene/level.scene")`. A document name can differ from its
filename; name lookup searches nested scene directories on demand. If multiple
valid saved scenes have the same document name, pass a path to choose one.

A scene loaded automatically by switching is temporary: switching away unloads
it and releases its state after the callback batch. Switching to it again reads
the saved file and starts fresh. Switching to the same scene keeps its state.
Scenes loaded explicitly with `load`, and the initial Play scene, stay loaded
when you switch away. Call `unload` to release those scenes yourself.

## Calls and results

| Call | Result |
| --- | --- |
| `load(document)` / `load_file(path)` | Build a world and return its scene ID, without selecting it. |
| `switch(id)` | Select a loaded world. An unknown or unloaded ID returns `NotLoaded`. |
| `unload(id)` | Drop that world and its entities/components. Unsaved edits are discarded. |
| `is_loaded(id)` / `loaded_ids()` | Inspect the worlds currently owned by the manager. |
| `active_id()` / `active_world()` / `active_world_mut()` | Inspect or edit the selected world; initially `None`. |
| `world(id)` / `world_mut(id)` | Borrow any loaded world by ID. |

Switching retains loaded worlds and their edits. Switching to the current world
is allowed. Unloading the active world clears the selection, even if other worlds
remain loaded. Load or switch another world explicitly. Duplicate loads return
`AlreadyLoaded`; unloading an unknown ID returns `NotLoaded`. Failed operations
preserve the loaded set and selection.

The manager retains no unloaded entry, document, serialized bytes, or world cache.
Reloading reads the file again and builds a fresh world. File loading rejects
incompatible versions, damaged data and checksum mismatches, and does not read
backup files. Inspect `SceneManagerError` for I/O, parsing or world validation
failures; open and save externally edited scenes in the editor to refresh their
checksum before runtime loading.

The host must tick and render the selected world, and coordinate script callbacks,
physics, audio and GPU resources separately. The manager owns ECS world data only;
assets or resources retained elsewhere in the host are outside its ownership.
Loading a document does not execute its startup scripts. Save authored changes
explicitly before unloading if they should survive.
| `game.scene.load(path)` | Read a saved relative scene file, build its world, and return its document name. Does not activate it or run its startup callbacks. |
| `game.scene.switchTo(scene)` | Select a document name or relative scene file path. If needed, load it automatically. Automatically loaded scenes unload on switching away; scene/object startup callbacks run on first activation. |
| `game.scene.unload(scene)` | Remove that document name from the loaded set; discard its unsaved runtime state and release its world and scene-owned runtime resources. |
| `game.scene.isLoaded(scene)` | Return whether the document name is loaded. |
| `game.scene.getLoaded()` | Return loaded document names in deterministic name order. |
| `game.scene.getActive()` | Return the selected document name, or nil/null/None when no scene is selected. |

`load` returns a string in every language. Lua/Luau/JS/Python/C#/Java/PHP return
native lists from `getLoaded`; C returns `RusticList` (`count`, `items`), and
C++ returns a `RusticValue` array. C++ `getActive` returns a nullable `RusticValue`;
check `is_null()` before `string()`. C query strings and lists last until the end
of the callback; copy strings if keeping them across callbacks.

## Lifetime and transitions

Loading allocates only that scene's live world. Switching preserves explicitly
loaded worlds, edits and script state. Inactive explicitly loaded scenes do not
tick or render; switching back resumes them without rerunning Start. Unloading releases a scene
instead of caching it. Reloading reads the saved file and starts fresh.

Calls update the loaded set and selected name synchronously, so loading and then
switching in one callback works. Scene runtime activation, teardown callbacks and
final memory release happen at the runtime boundary after the current callback
batch. Code already running can finish; return after requesting a transition if
it should do no more work on the old scene. A scene selected from a newly started
scene's Start callback is activated at the following boundary.

Unloading the active scene clears selection without choosing a fallback.
Global scripts continue, but the view is empty until a loaded scene is selected.
Scene/object OnDestroy and OnStop run on unload. Global scripts end only when
Play ends or they are disabled. The engine owns shared renderer/asset caches;
those are separate from the scene world and its scene-owned runtime state.

Object-path lookup and rendering use the active scene. Apply Runtime Changes
still compares the originally opened scene if it remains loaded; it does not save
other scenes. Explicitly save authored changes before Play; runtime unload
never writes to scene files.

## Errors and diagnosis

Switching to an unloaded scene tries to load it first. If the destination cannot
be found, is ambiguous, or fails file/world validation, the current scene stays
selected and loaded, including when it was automatically loaded. Loading a duplicate name or scene ID, or unloading an unknown name,
also raises an error. Catch expected errors with `pcall`/try/catch as appropriate.

`load` takes a relative project file path. `switchTo` accepts a document name or
a relative file path; unload/query calls take the **document name returned by load**. Paths cannot escape the Play snapshot.
Missing files, invalid RON, invalid world data, incompatible schema versions and
checksum mismatches raise errors without adding a loaded scene. The loader does
not silently use a backup. Open and save externally edited scene files in the
editor to refresh their checksums, then start Play again.

If a target's script source fails to load at first activation, the Console reports
its startup failure and clears the selection. Fix the source, then reload the
scene. Missing script attachments retain the existing warning-and-skip behavior.
Scene lifecycle mutations are rejected during final Play teardown.

## Rust host API

Rust hosts can also use `engine_world::SceneManager` with `load`, `load_file`,
`switch`, `unload`, `is_loaded`, `loaded_ids`, and active/world borrowing methods.
That lower-level manager owns ECS worlds only; the scripting runtime above also
coordinates callbacks, physics and audio lifetime.
46 changes: 46 additions & 0 deletions lib/api-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,52 @@ const docs: Record<string, ApiDoc> = {
returns: "No value. Lua updates host state directly; JavaScript/Web and external SDKs use their command boundary. The scheduler observes the resulting enabled state for lifecycle transitions. See [execution behavior](/docs/guides/scripting#when-mutations-take-effect).",
examples: simpleMutationExamples("rustic.set_enabled(false)", "rustic.set_enabled(false);"), notes: [protocolNote],
},
"scene-lifecycle": {
"title": "Scene lifecycle API",
"summary": "Load, unload and switch scenes explicitly from every supported scripting language. Only loaded scene worlds stay resident.",
"when": "Save Menu and Level as distinct scene documents, with Level at scene/level.scene. Put one example inside a generated Start/on_start callback, preferably a Global Startup Script, open Menu, then press Play. Keep the language template imports and callback registration. See [Load, unload and switch scenes](/docs/guides/scene-lifecycle) for exact setup, memory lifetime and errors.",
"calls": [
"game.scene.load(path)",
"game.scene.unload(scene)",
"game.scene.switchTo(scene)",
"game.scene.isLoaded(scene)",
"game.scene.getLoaded()",
"game.scene.getActive()"
],
"parameters": [
{
"name": "path",
"type": "relative project path",
"required": true,
"description": "A saved scene file in the Play snapshot, such as scene/level.scene. Parent traversal and paths outside the snapshot are rejected."
},
{
"name": "scene",
"type": "document name string",
"required": true,
"description": "Document name; switchTo also accepts a relative file path and tries to load a missing destination. Names are case-sensitive. Automatic name discovery searches scene/ and scenes/ and requires an unambiguous valid document."
}
],
"returns": "load returns the document name; isLoaded returns a boolean; getLoaded returns loaded names in sorted order; getActive returns the selected name or nil/null/None. switchTo and unload return no value. C uses RusticList for getLoaded; C++ returns a RusticValue array and a nullable RusticValue for getActive. Calls update membership/selection immediately; scene activation, teardown and final release happen after the current callback batch.",
"examples": {
"Lua 5.4": "local level = game.scene.load('scene/level.scene')\nassert(game.scene.isLoaded(level))\ngame.scene.switchTo(level)\ngame.scene.unload('Menu')\nprint(game.scene.getActive(), #game.scene.getLoaded())",
"JavaScript": "const level = game.scene.load('scene/level.scene');\ngame.scene.switchTo(level);\ngame.scene.unload('Menu');\nprint(game.scene.getActive(), game.scene.getLoaded());",
"Python": "from rustic import game\nlevel = game.scene.load('scene/level.scene')\ngame.scene.switchTo(level)\ngame.scene.unload('Menu')\nassert game.scene.isLoaded(level)",
"C++": "auto level = game.scene.load(\"scene/level.scene\");\ngame.scene.switchTo(level);\ngame.scene.unload(\"Menu\");\nif (!game.scene.isLoaded(level)) throw std::runtime_error(\"Level missing\");",
"C#": "var level = game.scene.load(\"scene/level.scene\");\ngame.scene.switchTo(level);\ngame.scene.unload(\"Menu\");\nif (!game.scene.isLoaded(level)) throw new System.Exception(\"Level missing\");",
"Luau": "local level = game.scene.load('scene/level.scene')\ngame.scene.switchTo(level)\ngame.scene.unload('Menu')\nassert(game.scene.isLoaded(level))",
"C": "const char *level = Game.scene.load(\"scene/level.scene\");\nGame.scene.switchTo(level);\nGame.scene.unload(\"Menu\");\nif (!Game.scene.isLoaded(level)) r_fail(\"Level missing\");",
"Java": "String level = game.scene.load(\"scene/level.scene\");\ngame.scene.switchTo(level);\ngame.scene.unload(\"Menu\");\nif (!game.scene.isLoaded(level)) throw new RuntimeException(\"Level missing\");",
"PHP": "global $game;\n$level = $game->scene->load(\"scene/level.scene\");\n$game->scene->switchTo($level);\n$game->scene->unload(\"Menu\");\nif (!$game->scene->isLoaded($level)) throw new Exception(\"Level missing\");",
"HTML / inline JS": "const level = game.scene.load('scene/level.scene');\ngame.scene.switchTo(level);\ngame.scene.unload('Menu');\nif (!game.scene.isLoaded(level)) throw Error('Level missing');"
},
"notes": [
"Loading creates a world without selecting it. Scene/object startup scripts run on first activation. Explicitly loaded inactive scenes retain state and do not tick or render. Switching back to those scenes does not rerun Start. switchTo automatically loads missing destinations by name or path, and switching away releases scenes loaded this way. Switching to the same scene preserves it.",
"Unloading discards unsaved runtime state, destroys scene/object script instances and releases world, physics, audio and capture state. No unloaded scene cache is retained. Global scripts survive and can load/select a scene when none is active. Shared renderer/asset caches have their own lifetime.",
"A missing or ambiguous saved destination, duplicate loads, unknown unloads, invalid or incompatible files and checksum failures raise errors. Failed load/switch operations preserve the selection. Loading never silently recovers a backup. Lifecycle mutations are unavailable during final Play teardown.",
"The example requires Menu to be the initially open document name. Save Level before starting Play; scene files are staged from scene/ and scenes/. C uses Game.scene; PHP uses $game->scene. CSS is styling only; HTML uses inline JavaScript."
]
},
environment: {
title: "Scene environment API", summary: "Read and update all loaded-scene sky, ambient light, sun light and haze settings in every scripting language.",
when: "Call during Start or a later gameplay callback. Attach a scene startup or object component script, copy panoramas into the project before Play, and enable the environment. See [Sky & atmosphere](/docs/guides/scene-environment) for complete setup and all 13 fields.",
Expand Down
Loading
Loading