9eead719b0
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
5.2 KiB
Markdown
121 lines
5.2 KiB
Markdown
# Editor Play Mode (Stage 8.7)
|
|
|
|
Play mode lets the editor **run the open scene in place** — play, pause, single-
|
|
step, and stop — driving the same fixed-timestep
|
|
[`Schedule`](modules.md) a shipped game uses, so what you see while playing
|
|
behaves like the real runtime. Mutations made while playing (physics moving
|
|
bodies, scripts spawning entities) are reverted on **Stop**, so the authored
|
|
scene is never corrupted.
|
|
|
|
This page covers the play-state model, the snapshot/restore that makes Stop
|
|
safe, and how the host runner drives the engine. The standalone **"Launch"**
|
|
button (running the *real* exported runtime in a separate process) is a Stage 16
|
|
follow-up and is **not** part of play mode.
|
|
|
|
## The play states
|
|
|
|
`oxide_editor::state::PlayState` is a three-state machine held on `EditorState`:
|
|
|
|
| State | Meaning | Schedule ticked? |
|
|
|-------|---------|------------------|
|
|
| `Editing` | Normal authoring | no |
|
|
| `Playing` | Running | every frame (`App::update`) |
|
|
| `Paused` | Frozen, still live | only on **Step** (one fixed tick) |
|
|
|
|
Transitions are methods on `EditorState`:
|
|
|
|
- `enter_play()` — snapshots the scene and switches to `Playing`. No-op if
|
|
already running (re-entering must not clobber the original snapshot).
|
|
- `toggle_pause()` — `Playing` ⇄ `Paused`; no-op while `Editing`.
|
|
- `stop()` — restores the snapshot, clears the selection and any in-flight gizmo
|
|
drag (entity handles change on restore), and returns to `Editing`.
|
|
|
|
The shell wraps these with status messages and undo-history clearing (see
|
|
[Controls](#controls)); `is_in_play()` is the "running or paused" predicate the
|
|
host uses to gate the runtime.
|
|
|
|
## Snapshot / restore
|
|
|
|
Pressing Play captures a [`SceneSnapshot`](scene.md) of the current scene;
|
|
pressing Stop restores it. Unlike `Scene::to_ron` (which records only the node-
|
|
baked `Node`/`Transform`/hierarchy), a snapshot is **registry-aware**: it also
|
|
serializes every reflected component on each entity, plus the engine-intrinsic
|
|
non-reflected components (`Tags` and `DisabledComponents`). That makes Stop a
|
|
**bit-for-bit** revert across the full component set, not just transforms.
|
|
|
|
```rust
|
|
use oxide_engine::scene::SceneSnapshot;
|
|
|
|
let snap = scene.snapshot(®istry); // capture
|
|
// … play-mode mutations …
|
|
let scene = snap.restore(®istry)?; // revert (fresh entity handles)
|
|
```
|
|
|
|
Fidelity is bounded by what the [`TypeRegistry`](reflection.md) knows: a
|
|
component that is neither registered nor one of the two intrinsics is invisible
|
|
to capture. Modules register their components anyway (that is what makes them
|
|
editable), so they survive play mode automatically.
|
|
|
|
`SceneSnapshot` also exposes `to_ron`/`from_ron`, so the same capture format
|
|
seeds full scene files in a later stage.
|
|
|
|
## Driving the schedule
|
|
|
|
The editor holds the live scene in `EditorState.scene`, not in an
|
|
[`App`](modules.md). On Play the host runner (`oxide_editor::main`) builds a play
|
|
`App` (currently just `DefaultModules`; Stage 9's physics module and the
|
|
project's modules will register here too) and, each frame:
|
|
|
|
1. **swaps** `state.scene` into `app.scene` (an O(1) move),
|
|
2. advances the engine, then
|
|
3. **swaps** the scene back out.
|
|
|
|
So the `App` only "holds" the editor scene for the duration of a tick, and
|
|
`state.scene` stays the single source of truth the inspector, hierarchy, and
|
|
viewport read between frames. The `App` persists its `Time` and resources across
|
|
frames (so a physics world accumulates correctly) and is dropped on Stop.
|
|
|
|
How far to advance is decided by a small pure function,
|
|
`oxide_editor::play::tick_for`, kept separate from the (un-testable) GUI runner
|
|
so the contract is pinned in a unit test:
|
|
|
|
```rust
|
|
use oxide_editor::play::{tick_for, Tick};
|
|
|
|
match tick_for(state.play, step_requested) {
|
|
Tick::Frame => app.update(dt), // Playing
|
|
Tick::FixedStep => app.step(), // Paused + Step
|
|
Tick::Idle => {} // Editing, or Paused with no Step
|
|
}
|
|
```
|
|
|
|
`App::step()` (engine) advances **exactly one fixed timestep** — one
|
|
`FixedUpdate` plus the per-frame phases once, bypassing the accumulator — which
|
|
is the Step primitive. `App::update(dt)` runs a normal frame, fixed steps driven
|
|
by the accumulator as usual (see [modules.md](modules.md)).
|
|
|
|
## Controls
|
|
|
|
A toolbar below the menu bar exposes **Play / Pause / Step / Stop**, gated by the
|
|
play state (Pause/Step/Stop enable only while running) with an
|
|
`Editing`/`PLAYING`/`PAUSED` badge. The viewport additionally draws a coloured
|
|
border + corner label while running (green = playing, amber = paused) so
|
|
edit-vs-play is unmistakable.
|
|
|
|
Shortcuts:
|
|
|
|
- **Ctrl+P** — Play when editing; Pause ⇄ Resume while running.
|
|
- **Ctrl+.** — Step one fixed tick (while paused).
|
|
|
|
Entering Play and pressing Stop both **clear the undo history**: the scene is
|
|
restored wholesale on Stop, so play-mode edits are deliberately not part of
|
|
edit-mode undo. The reflection inspector and gizmos stay live while paused (and
|
|
playing), so a field can be tweaked and the result observed immediately — the
|
|
payoff of the Stage-8.5 reflection work.
|
|
|
|
## Not in play mode (Stage 16)
|
|
|
|
A **"Launch standalone"** button that runs the real exported runtime in a
|
|
separate window/process — the truest-to-ship check — is deferred to Stage 16,
|
|
where it reuses the export builder rather than the in-editor loop.
|