scene/minimal_scene

Goal

Understand the smallest useful SimScene implementation that still demonstrates:

  • scene registration and discovery

  • frame-by-frame tick(input_frame, dt) execution

  • backend-agnostic rendering via RenderPacket

  • runtime telemetry (backend name, frame, dt, viewport)

Why this tutorial exists

This is the base scene pattern used before introducing:

  • action maps and intent systems

  • world/entity simulation

  • queued render systems and draw ops

  • commands and overlays

If this scene is clear, advanced scene architecture is easier to reason about.

Source map

  • Settings profile: examples/settings/scene/minimal_scene.yml

  • Example builder: examples/catalog/scene/minimal_scene/main.py

  • Runtime scene: examples/catalog/scene/minimal_scene/scenes/scene.py

  • Shared runner: examples/_shared/runner.py

Scene lifecycle walkthrough

MinimalScene does this:

  1. @register_scene("minimal_scene") registers scene id in auto registry.

  2. __init__(ctx) initializes local runtime state:

    • _elapsed

    • _frames

    • _last_backend_name

  3. tick(input_frame, dt):

    • increments time/frame counters

    • reads runtime config (self.context.config)

    • reads viewport telemetry (window.get_viewport())

    • computes animation position (sin pulse)

    • returns RenderPacket.from_ops([draw])

  4. draw(backend) callback:

    • records concrete backend class name

    • draws panel + animated rectangle + text diagnostics

This is intentionally one-file, one-scene, no systems.

Code excerpt

Key parts from examples/catalog/scene/minimal_scene/scenes/scene.py:

SCENE_ID = "minimal_scene"


@register_scene(SCENE_ID)
class MinimalScene(SimScene):
    def __init__(self, ctx: RuntimeContext):
        super().__init__(ctx)
        self._elapsed = 0.0
        self._frames = 0

    def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
        self._elapsed += dt
        self._frames += 1
        vp = self.context.services.window.get_viewport()

        def draw(backend: Backend):
            backend.render.draw_rect(24, 24, 640, 390, color=(0, 0, 0, 220))
            backend.text.draw(
                40, 36, f"frame={self._frames} viewport={vp.window_w}x{vp.window_h}"
            )

        return RenderPacket.from_ops([draw])

Why this is important:

  • @register_scene(...) is how scene IDs become discoverable.

  • tick(...) is the per-frame simulation/render hook.

  • self.context.services.* is the runtime service access surface.

  • RenderPacket is the unit consumed by the render pipeline.

Config behavior

From main.py, this example supports:

  • --backend

  • --fps

  • --virtual-width, --virtual-height

  • --window-width, --window-height

  • --enable-profiler

  • --postfx-enabled, --postfx-active

Config precedence:

  1. CLI passthrough overrides

  2. examples/settings/scene/minimal_scene.yml

  3. shared example defaults (examples/settings/settings.yml)

  4. hardcoded fallback values in builder

Run

Default:

mini-arcade run --example scene/minimal_scene

Force pygame:

mini-arcade run --example scene/minimal_scene --pass-through --backend pygame

Force native:

mini-arcade run --example scene/minimal_scene --pass-through --backend native

Override timing/resolution:

mini-arcade run --example scene/minimal_scene --pass-through --fps 72 --virtual-width 960 --virtual-height 540

What to verify

On screen you should see:

  • backend: <class> changes between pygame/native runs

  • frame increments continuously

  • dt fluctuates near expected frame time

  • fps target reflects CLI override

  • viewport values react to resize

Behavior checks:

  1. animated rectangle moves smoothly (tick + dt are active)

  2. F1 toggles debug overlay

  3. ESC exits cleanly

How this scales to real games

When moving from this minimal scene to gameplay scenes:

  • move local state into models.py world dataclass

  • replace inline input handling with action-map intent systems

  • split rendering into draw_ops.py + render system

  • move side effects into commands

Reference next levels:

Common mistakes

  • Missing package discovery entry: scene is registered but never imported/discovered.

  • Forgetting --pass-through: CLI override flags do not reach the example builder.

  • Returning no RenderPacket: scene runs but displays nothing.