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>
5.2 KiB
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 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 toPlaying. No-op if already running (re-entering must not clobber the original snapshot).toggle_pause()—Playing⇄Paused; no-op whileEditing.stop()— restores the snapshot, clears the selection and any in-flight gizmo drag (entity handles change on restore), and returns toEditing.
The shell wraps these with status messages and undo-history clearing (see
Controls); is_in_play() is the "running or paused" predicate the
host uses to gate the runtime.
Snapshot / restore
Pressing Play captures a SceneSnapshot 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.
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 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. 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:
- swaps
state.sceneintoapp.scene(an O(1) move), - advances the engine, then
- 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:
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).
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.