Files
Oxide/docs/play-mode.md
T
Homer Simpson 9eead719b0 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>
2026-07-05 20:41:02 +02:00

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 to Playing. No-op if already running (re-entering must not clobber the original snapshot).
  • toggle_pause()PlayingPaused; 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); 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(&registry);   // capture
// … play-mode mutations …
let scene = snap.restore(&registry)?;   // 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:

  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:

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.