Import Oxide engine (Stages 0–10) under MIT license
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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user