Engine Module · Runtime Coordination

Scene System

A lifecycle and routing boundary that owns scene objects, defers transitions to safe frame boundaries, and coordinates input, physics, cameras, rendering, and UI.

Implemented Lifecycle Routing Ownership Runtime Context

01 · Overview

One boundary for scene lifetime and frame work

The system separates route decisions from lifecycle execution. Scenes request a destination; SceneManager validates, stores, and applies that request after input or update completes.

SceneManager

Registry and transition authority

Maps SceneKey values to providers, owns the active-scene pointer, binds runtime context, and enforces transition rules.

SceneFactory

Typed instance cache

Caches scenes by concrete C++ type and supports reuse, reset, or full reconstruction without exposing allocation to callers.

SceneRoute

A value-owned transition request

Carries the target key, type-erased payload, and reload policy. The manager copies it before processing.

SceneRuntimeContext

Runtime services at the lifecycle boundary

Provides the renderer, content registry, logical dimensions, font resolver, and engine-assist services before on_enter runs.

02 · Boundaries

What the scene owns—and what it delegates

Scene owns

  • GameObject instances grouped by depth layer and UI root ownership.
  • Input, update, physics, collision, camera, and render dispatch order.
  • Interface registration and frame-end removal of destroyed objects.

Scene delegates

  • Registration, route validation, caching, and active-scene replacement to SceneManager.
  • Shared cameras, resources, fonts, and engine services through focused service objects.
  • Application termination through an observer request instead of closing the app directly.

03 · Architecture

Composition and safe transitions

Two views show how scenes enter the application and how a runtime request becomes a lifecycle transition.

Application scene composition

Engine-owned, testbed, and game scenes share one registry before the initial route starts.

Application registers engine, testbed, and game scenes, then starts the configured initial route.

Deferred transition boundary

A scene emits intent during input or update; replacement happens only after that phase returns.

Scene request → pending value copy → frame boundary → exit and reload policy → context binding → enter.

04 · Design evolution

How the scene boundary became explicit

Three changes turned scene switching from a convenient manager call into a predictable runtime contract.

01

From direct switching to a deferred request boundary

The important change was not how a destination is named, but when replacing the active scene is allowed to happen.

The first useful version

SceneManager::switch_to<T>() asked the factory for a typed instance and replaced the active scene immediately. It was compact and sufficient while navigation remained an external manager operation.

The pressure pointOnce scenes initiated navigation from input or update callbacks, immediate replacement could enter exit and destruction work while the old scene was still on the call stack.

The boundary now

A Scene emits a value-owned request. SceneManager copies it into a pending slot and handles it only after on_input or on_update returns.

  • The executing callback finishes against a stable scene.
  • Only one request may be queued in a processing cycle.
  • Duplicate and reentrant requests fail with logic_error.
02

From separate operations to explicit route semantics

A transition should describe the destination, transferred data, and instance-lifetime policy as one decision.

Earlier surface

Switching, resetting, and destroying scenes were separate manager operations. The first request value also exposed target, payload, and reload mode as parallel fields.

Current contract

SceneRoute keeps those three facts together, so registration, validation, request copying, and lifecycle execution all receive the same route value.

Three deliberate lifetime choices

Reuse
Keep the cached instance and its state.
Reset
Keep the allocation, run reset, then enter again.
Recreate
Destroy the cached instance and construct a fresh one.
03

From implicit services to a bounded runtime context

Cached construction and active execution are different lifetimes, so runtime-only dependencies belong at the entry boundary.

The early Scene lifecycle had no unified runtime dependency carrier. The manager and scenes reached renderer, camera, and other services through separate paths, even though a cached scene might be constructed long before it becomes active.

The design ruleRenderer, content registry, logical dimensions, font resolution, and engine-assist services describe the active runtime—not factory construction.
  1. BindSceneManager attaches SceneRuntimeContext before on_enter.
  2. UseThe scene borrows the same context throughout normal execution and exit.
  3. ClearRecreate and shutdown remove the binding before destroying cached instances.

Lifecycle behavior is covered by the Scene core tests. View scene_core_tests.cpp ↗

05 · Selected source

The contracts behind the flow

These snapshots were selected from the current local Moonline working tree. The source links point to their canonical files on GitHub.

Contract

Scene lifecycle and routing surface

engine/scene/scene.h

The base class exposes lifecycle hooks while keeping route emission and runtime services behind the protected boundary.

virtual void on_enter(const ScenePayload& payload) = 0;
virtual void on_exit() = 0;
virtual void reset() = 0;

void request_scene_switch(const SceneRoute& route);
[[nodiscard]] const SceneRuntimeContext& runtime_context() const;

Routing

Value-owned route data

engine/scene/routing/scene_route.h

The route keeps destination, payload, and lifetime policy together, while SceneRequest distinguishes switching from quitting.

enum class SceneReloadMode
{
    Reuse,
    Reset,
    Recreate
};

struct SceneRoute
{
    SceneKey target = SceneKeys::Invalid;
    ScenePayload payload{};
    SceneReloadMode reload_mode = SceneReloadMode::Reuse;
};

Transition

Pending request and lifecycle execution

engine/scene/scene_manager.cpp

The manager rejects ambiguous requests, takes a value copy, and applies exit, reset, context binding, and enter in a controlled order.

const SceneRequest request = _pending_request;
_pending_request = SceneRequest{};
_has_pending_request = false;

ProcessingRequestGuard processing_guard(_is_processing_request);

switch (request.type)
{
case SceneRequestType::Switch:
    switch_to_registered_scene(request.route);
    break;
}

06 · Limits and next steps

Keep the boundary explicit as the engine grows

Current constraints

Requests are intentionally limited to normal input and update execution, and only one request may be queued per processing cycle.

Natural extensions

Future work can add transition effects or asynchronous scene preparation while preserving the same route and lifecycle boundary.