simvx.core.scene_tree

SceneTree: Central manager for the node tree, groups, input routing, and UI focus.

Module Contents

Classes

SceneTree

Central manager for the node tree, groups, input routing, and UI focus.

Data

API

simvx.core.scene_tree.RESERVED_AUTOLOAD_EVENTS

‘events’

simvx.core.scene_tree.log

‘getLogger(…)’

class simvx.core.scene_tree.SceneTree(screen_size=None, *, isolated_input: bool = False)[source]

Central manager for the node tree, groups, input routing, and UI focus.

Owns the root node and drives per-frame process / physics_process / draw traversals. Also manages pause state, scene changes, and the UI input pipeline (mouse, keyboard, popups).

Initialization

classmethod current() simvx.core.scene_tree.SceneTree | None[source]

Return the most recently activated SceneTree, or None.

Activation happens automatically on set_root / change_scene. Used by InputSimulator to deliver scene-tree + UI-tree events without requiring callers to thread a tree reference through.

property app[source]

The App instance running this tree (set by graphics backend).

property events: simvx.core.event_bus.EventBus[source]

Engine-provided typed event bus.

Use tree.events.subscribe(EventCls, handler) to register a handler and tree.events.publish(event) (or publish_deferred) to dispatch. The bus survives change_scene() – subscriptions held by autoloads or other long-lived objects keep firing across scene swaps. Deferred events queued during a frame are dispatched at the start of the next process() tick, before any node _process runs.

property audio_backend: AudioBackend | None[source]

The active audio backend, or None if none was initialised.

Returns the union :class:AudioBackend type for backwards compatibility: callers that only need one facet should prefer the narrowed :attr:audio_playback / :attr:audio_streaming /

Attr:

audio_buses properties so the type checker enforces the boundary. Reaching for the underscore-prefixed attribute is engine-private and may change.

The engine sets this during App.run via make_backend. Tests and headless harnesses that don’t initialise audio see None.

install_audio_backend(backend: simvx.core.audio_protocol.AudioPlaybackBackend) None[source]

Install the audio backend for this tree: the one canonical path.

This is the only supported way to attach a backend; assigning the private _audio_backend slot is no longer part of the contract. Used by App.run / WebApp at startup and by tests that inject a

Class:

NullAudioBackend or a mock.

Semantics mirror startup driver selection in other engines (Godot’s --audio-driver, pyglet’s audio option): a backend is installed once, before the tree starts producing sound. Re-installing shuts the previous backend down first so device handles don’t leak.

Facet conformance is enforced where it is consumed, not here: the

Attr:

audio_playback / :attr:audio_streaming / :attr:audio_buses accessors narrow via isinstance and return None for a backend that doesn’t implement that facet, and callers raise

Class:

AudioCapabilityError on None. Gating here as well would duplicate that boundary and reject legitimate partial backends.

property audio_playback: AudioPlaybackBackend | None[source]

The active backend narrowed to :class:AudioPlaybackBackend, or None.

Always equals :attr:audio_backend cast to the playback facet when a backend is present: every shipped backend implements playback, including the silent :class:NullAudioBackend.

property audio_streaming: AudioStreamingBackend | None[source]

The active backend narrowed to :class:AudioStreamingBackend, or None.

Returns None when the active backend doesn’t implement streaming (the Null backend, any future no-device test stub). Callers that need streaming (:class:AudioSynth driver, AudioWorklet feeds) should raise :class:AudioCapabilityError on None.

property audio_buses: AudioBusBackend | None[source]

The active backend narrowed to :class:AudioBusBackend, or None.

Always equals :attr:audio_backend cast to the bus facet when a backend is present: every shipped backend implements bus + capability advertisement.

audio_listener_3d() AudioListener3D | None[source]

The active 3D audio listener, lazy-creating one if none exists.

Returns the most recently entered :class:AudioListener3D in the scene. If none has been added, auto-creates a fallback parented to the active Camera3D with a one-time warning. Returns None only if there’s no camera either.

Audio players call this every frame, so the auto-creation is cheap once it’s happened (the cached listener is returned).

audio_listener_2d() AudioListener2D | None[source]

The active 2D audio listener, lazy-creating one if none exists.

Same contract as :meth:audio_listener_3d but for 2D.

property input[source]

The Input instance for this tree (per-tree isolation).

property input_map[source]

The InputMap instance for this tree (per-tree isolation).

activate_input()[source]

Context manager to make this tree’s Input/InputMap the active ones.

Use this when processing the tree so that game code calling Input.is_action_pressed(...) sees this tree’s input state.

property is_running: bool[source]

Whether the tree is actively being ticked by its driving app.

Set to False by :meth:quit (or the graphics backend’s app quit()), signalling the main loop to exit at the end of the current frame.

property now: float[source]

Monotonically-increasing scene time, in seconds.

Accumulates dt at the start of every :meth:tick call (after any :attr:simvx.graphics.App.time_scale scaling has been applied), so slow-motion and hitstop also slow this clock. Frozen while

Attr:

paused is True. Resets to 0.0 only by constructing a fresh SceneTree.

Use for time-based animation, slow-mo gating, and any “how long has the scene been running” query: preferable to per-node self._time += dt accumulators because every consumer reads the same monotonic value.

quit() None[source]

Request a clean shutdown of the running tree.

Emits :attr:quit_requested and flips :attr:is_running to False. The driving app polls this state and exits its loop at the end of the current frame. Safe to call from node callbacks or signal handlers.

property screen_size: tuple[float, float][source]
set_root(root: simvx.core.node.Node)[source]

Set the root node of the scene tree.

Before the root enters the tree, any input_actions declared on the root (class- or instance-level dict[str, list]) is bulk- registered with this tree’s InputMap. This is the canonical replacement for the wrapper-class + on_ready boilerplate and survives change_scene swaps – every new root’s actions are re-registered automatically.

change_scene(new_root: simvx.core.node.Node)[source]

Swap the active root with new_root.

The old root receives _exit_tree; new_root then runs the full _enter_tree / _ready_recursive path, identical to the initial root. Autoloads are left in place and their groups and unique-name entries are re-registered on the rebuilt tree. Pending deletes, UI popup state, and the active 2D camera are cleared.

Use this for title → gameplay → game-over navigation. See

Doc:

../patterns for a full example.

tick(dt: float)[source]

Run process callbacks and coroutines on all nodes for one frame.

Order: autoloads’ _process first (Godot-style global singletons), then self.events.flush_deferred() so deferred events queued during the previous frame (process, physics, or input) reach all subscribers before scene logic runs, then the scene root’s _process. Anything emit_deferred’d during this frame’s autoload pass is also drained in the same flush, keeping the scene’s view of the world consistent.

physics_tick(dt: float)[source]

Run physics_process callbacks on all nodes, then auto-step physics.

propagate_input(event: simvx.core.events.TreeInputEvent) None[source]

Dispatch an input event to registered @on_input handlers.

Looks up handlers via the typed dispatch tables (built when nodes carrying @on_input-decorated methods enter the tree). The traversal is O(handlers per event), not O(nodes): nodes without any input handlers cost nothing.

A handler returning a truthy value marks the event consumed: on_unhandled_input only fires if nothing consumed the event. event.handled is also flipped True so callers can short-circuit.

render(renderer)[source]
push_modal(control)[source]

Register a control as an active modal (drawn on top, receives all UI input).

pop_modal(control)[source]

Unregister a modal control.

input_cast(screen_pos: tuple[float, float] | numpy.ndarray, button: simvx.core.input.enums.MouseButton = MouseButton.LEFT)[source]

Cast a ray from screen_pos through the camera into the scene. Finds the nearest pickable CollisionShape3D and delivers an InputEvent to its parent node.

get_group(name: str) list[simvx.core.node.Node][source]

Get all nodes in a group.

property autoloads: dict[str, simvx.core.node.Node][source]

Read-only view of registered autoloads.

add_autoload(name: str, node: simvx.core.node.Node)[source]

Register node as a persistent singleton attached to the tree.

The node enters the tree and runs on_ready() immediately. Unlike a regular child, it is not reachable via the scene root: retrieve it via tree.autoloads[name]. Autoloads survive change_scene(), making them the canonical home for global state (score, settings, audio manager). See :doc:../patterns.

The name "events" is reserved for the engine-provided

Class:

~simvx.core.event_bus.EventBus; use tree.events instead.

remove_autoload(name: str)[source]

Unregister and tear down an autoload. Calls _exit_tree on the node.

get_unique(name: str) simvx.core.node.Node | None[source]

Get a unique node by name. Returns None if not found.

ui_input(mouse_pos: tuple[float, float] | numpy.ndarray = None, button: simvx.core.input.enums.MouseButton | None = None, pressed: bool = True, key: str = '', char: str = '')[source]

Route UI input events to controls.

button is a MouseButton for press/release, None for keyboard / char / pure mouse-move events.

touch_input(finger_id: int, action: int, x: float, y: float)[source]

Route multi-touch events to controls with touch_mode=’multi’.