simvx.core.scene_tree¶
SceneTree: Central manager for the node tree, groups, input routing, and UI focus.
Module Contents¶
Classes¶
How a scene’s space relates to the render target it is presented in. |
|
A transient one-shot timer owned by the :class: |
|
Central manager for the node tree, groups, input routing, and UI focus. |
Data¶
API¶
- simvx.core.scene_tree.RESERVED_SINGLETON_EVENTS¶
‘events’
- simvx.core.scene_tree.log¶
‘getLogger(…)’
- class simvx.core.scene_tree.ContentScaleMode[source]¶
Bases:
enum.StrEnumHow a scene’s space relates to the render target it is presented in.
Set through :attr:
SceneTree.content_scale_mode, which documents what each one does to a layout and to the three sizes a game can read.Initialization
Initialize self. See help(type(self)) for accurate signature.
- EXPAND¶
‘expand’
- FIT¶
‘fit’
- __new__(*values)¶
- __add__()¶
- __contains__()¶
- __delattr__()¶
- __dir__()¶
- __eq__()¶
- __format__()¶
- __ge__()¶
- __getattribute__()¶
- __getitem__()¶
- __getnewargs__()¶
- __getstate__()¶
- __gt__()¶
- __hash__()¶
- __iter__()¶
- __le__()¶
- __len__()¶
- __lt__()¶
- __mod__()¶
- __mul__()¶
- __ne__()¶
- __reduce__()¶
- __reduce_ex__()¶
- __repr__()¶
- __rmod__()¶
- __rmul__()¶
- __setattr__()¶
- __sizeof__()¶
- __str__()¶
- __subclasshook__()¶
- capitalize()¶
- casefold()¶
- center()¶
- count()¶
- encode()¶
- endswith()¶
- expandtabs()¶
- find()¶
- format()¶
- format_map()¶
- index()¶
- isalnum()¶
- isalpha()¶
- isascii()¶
- isdecimal()¶
- isdigit()¶
- isidentifier()¶
- islower()¶
- isnumeric()¶
- isprintable()¶
- isspace()¶
- istitle()¶
- isupper()¶
- join()¶
- ljust()¶
- lower()¶
- lstrip()¶
- partition()¶
- removeprefix()¶
- removesuffix()¶
- replace()¶
- rfind()¶
- rindex()¶
- rjust()¶
- rpartition()¶
- rsplit()¶
- rstrip()¶
- split()¶
- splitlines()¶
- startswith()¶
- strip()¶
- swapcase()¶
- title()¶
- translate()¶
- upper()¶
- zfill()¶
- __deepcopy__(memo)¶
- __copy__()¶
- name()¶
- value()¶
- class simvx.core.scene_tree.SceneTreeTimer(seconds: float)[source]¶
A transient one-shot timer owned by the :class:
SceneTree.Created via :meth:
SceneTree.create_timer. Emitstimeoutonce after its delay elapses, then the tree drops it. Lighter than a :class:~simvx.core.Timernode: it lives only on the tree’s timer list, not in the scene graph.Initialization
- __slots__¶
(‘timeout’, ‘_time_left’)
- class simvx.core.scene_tree.SceneTree(screen_size=None, *, input_base: simvx.core.scene_tree.SceneTree | None = None)[source]¶
Central manager for the node tree, groups, input routing, and UI focus.
Owns the root node and drives per-frame
process/physics_process/drawtraversals. Also manages pause state, scene changes, and the UI input pipeline (mouse, keyboard, popups).Initialization
Build a tree.
Args: screen_size: The render target’s size,
(800, 600)by default. input_base: Another tree whose input entries (its actions, device aliases and redefinitions of engine entries) this one is built on, read live, as a branch inherits the entries above it. None, the default, starts from the engine’s entries and the process-wide map alone, which is what an editor hosting a game wants: the choice is made here and never inferred.A tree owns its input. It has its own
- Class:
~simvx.core.input.map.InputMap, its own contexts, its own focus, and its own delivered record of the device elements that actually reached it, which is the only thing its actions resolve from. So an editor and the game it is running never share action state, and neither has to ask for that.
Two things are underneath it and shared. The process-wide device record says what is physically down:
Input.is_key_pressed,Input.capabilities,Input.controllersandInput.input_methoddescribe the hardware under the player and say the same thing in every tree. And the process-wideInputMapis the bottom layer of this tree’s, read live, so an action registered before any tree existed, which is what a game’smain()does, is one this tree resolves.A test that needs the machine clean calls
- Func:
simvx.core.input.devices.resetand- Func:
simvx.core.input.capabilities.reset, orInput._reset(), which covers the first.
- verify_input_order: bool¶
False
- input_delivered¶
‘Signal(…)’
- script_error¶
‘Signal(…)’
- 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 byInputSimulatorto deliver scene-tree + UI-tree events without requiring callers to thread a tree reference through.Held weakly: returns
Noneonce the tree has no remaining strong references (e.g. a test dropped it), which is the correct “no active tree” semantics.
- property events: simvx.core.event_bus.EventBus[source]¶
Engine-provided typed event bus.
Use
tree.events.subscribe(EventCls, handler)to register a handler andtree.events.publish(event)(orpublish_deferred) to dispatch. The bus surviveschange_scene(): subscriptions held by singletons or other long-lived objects keep firing across scene swaps, and those the replaced scene’s nodes held end when it is destroyed. Deferred events queued during a frame are dispatched at the start of the nextprocess()tick, before any node_processruns.
- property audio_backend: simvx.core.audio_protocol.AudioBackend | None[source]¶
The active audio backend, or
Noneif none was initialised.Returns the union :class:
AudioBackendtype for backwards compatibility: callers that only need one facet should prefer the narrowed :attr:audio_playback/ :attr:audio_streaming/- Attr:
audio_busesproperties 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.runviamake_backend. Tests and headless harnesses that don’t initialise audio seeNone.
- 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_backendslot is no longer part of the contract. Used byApp.run/WebAppat startup and by tests that inject a- Class:
NullAudioBackendor a mock.
Semantics mirror startup driver selection in other engines (Godot’s
--audio-driver, pyglet’saudiooption): 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_busesaccessors narrow viaisinstanceand returnNonefor a backend that doesn’t implement that facet, and callers raise- Class:
AudioCapabilityErroronNone. Gating here as well would duplicate that boundary and reject legitimate partial backends.
A backend that paces itself by the engine rather than by a device clock declares :class:
~simvx.core.audio_protocol.TreeBoundAudioBackend, and installing hands it this tree: several trees can be alive at once, so it has to be told which one’s ticks are its own.
- property audio_playback: simvx.core.audio_protocol.AudioPlaybackBackend | None[source]¶
The active backend narrowed to :class:
AudioPlaybackBackend, orNone.Always equals :attr:
audio_backendcast to the playback facet when a backend is present: every shipped backend implements playback, including the silent :class:NullAudioBackend.
- property audio_streaming: simvx.core.audio_protocol.AudioStreamingBackend | None[source]¶
The active backend narrowed to :class:
AudioStreamingBackend, orNone.Returns
Nonewhen the active backend doesn’t implement streaming (the Null backend, any future no-device test stub). Callers that need streaming (:class:AudioSynthdriver, AudioWorklet feeds) should raise :class:AudioCapabilityErroronNone.
- property audio_buses: simvx.core.audio_protocol.AudioBusBackend | None[source]¶
The active backend narrowed to :class:
AudioBusBackend, orNone.Always equals :attr:
audio_backendcast to the bus facet when a backend is present: every shipped backend implements bus + capability advertisement.
- audio_listener_3d() simvx.core.audio_listener.AudioListener3D | None[source]¶
The active 3D audio listener, lazy-creating one if none exists.
Returns the most recently entered :class:
AudioListener3Din the scene. If none has been added, auto-creates a fallback parented to the activeCamera3Dwith a one-time warning. ReturnsNoneonly 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() simvx.core.audio_listener.AudioListener2D | None[source]¶
The active 2D audio listener, lazy-creating one if none exists.
Same contract as :meth:
audio_listener_3dbut for 2D.
- activate_input()[source]¶
Return a context manager making this tree’s Input/InputMap the active ones.
Wrap any engine-driven tree work in this. Node code that runs inside the span sees this tree, so
Input.pressed(...)reads this tree’s state andInputMap.add_action(...)lands in this tree’s own map rather than the process-wide default. The span is also howInputMap.add_actiontells such a registration from a genuinely out-of-band one made while the scene is running.Every tree entry point that runs this tree’s node code opens the span, so the rule holds without the caller doing anything: a tick, a physics step, a mount (
set_root,change_scene,add_singleton,Node.add_child), a teardown (change_scene,remove_singleton,Node.remove_child),@on_inputdispatch, UI, multi-touch and 3D pick dispatch, a focus or hover change, a layout flush, drawing (on_draw), and the tree’s ownscreen_resizedandquit_requestedemits.What that does not cover is code reaching node methods without going through the tree at all: calling a hook by hand, or emitting a signal the tree owns from outside it. Such a caller wraps the work itself.
_input_span_depthis a re-entrant depth, so spans may nest freely (achange_scenecalled fromon_updateor from an@on_inputhandler mounts inside the span that is already open).
- property paused: bool[source]¶
Whether the scene is suspended: a node runs only if it says so.
A node’s
update_modedecides what the pause means for it:PAUSABLEstops,ALWAYScarries on,PAUSED_ONLYruns only while this is True, which is what a pause menu is. The switch reaches input as well as processing, so a paused player neither polls nor hears, and aninput_contexton a paused node stops declaring and stops hiding until the node wakes.
- 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 appquit()), signalling the main loop to exit at the end of the current frame.
- property now: float[source]¶
Monotonically-increasing scene time, in seconds.
Accumulates
dtat the start of every :meth:tickcall (after any :attr:simvx.graphics.App.time_scalescaling has been applied), so slow-motion and hitstop also slow this clock. Frozen while- Attr:
pausedis True or any inert overlay is open. Resets to0.0only by constructing a freshSceneTree.
Use for time-based animation, slow-mo gating, and any “how long has the scene been running” query: preferable to per-node
self._time += dtaccumulators because every consumer reads the same monotonic value.
- quit() None[source]¶
Request a clean shutdown of the running tree.
Emits :attr:
quit_requestedand flips :attr:is_runningto 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]¶
The size of the space this scene is laid out in, in engine units.
Under :attr:
content_scale_modeEXPAND, the default, that is the render target: a wider window is a wider scene and shows more. UnderFITit is :attr:content_scale_size, the design resolution, whatever the window is doing, because under FIT the scene’s space is design units all the way through.Assigning says the render target is now this big, which is what a backend does on a resize. Under EXPAND that is the same statement as the one above; under FIT it changes the letterbox and leaves what this reads back alone. The three sizes and which question each answers are set out in :attr:
content_scale_mode.An
on_drawthat read it is re-captured when it changes.
- property window_size: tuple[float, float][source]¶
The render target this scene is presented in, in engine units.
The glass, as opposed to :attr:
screen_size, which is the space the scene is laid out in. They are the same underEXPANDand differ underFIT.
- property content_scale_mode: simvx.core.scene_tree.ContentScaleMode[source]¶
How the scene’s space relates to the render target it is shown in.
EXPAND(the default) means an engine unit is a logical pixel of the render target: the scene’s space is the window, a wider window shows more world, and a HUD anchored to a screen edge is on the glass. This is what anything with a HUD wants and it is what every backend does.FITpreserves a design resolution for a fixed playfield: the scene is always :attr:content_scale_sizeunits across, that rect is scaled up uniformly to the largest that fits the window and centred, and the leftover bars belong to no viewport. Under FIT the root viewport’s space is design units: the letterbox is applied once, where input enters, soPointerEvent.position,Input.mouse_position,- Attr:
screen_sizeand everyControlrect are already design units and no consumer transforms anything. A pointer over a bar has a design coordinate outside the design rect and therefore hits nothing, by ordinary geometry rather than by a special case.
What each size answers, which is the thing to be careful about. These three agree under EXPAND and deliberately differ under FIT:
========================== ==========================================
app.widththe window. The glass, always, because a game placing an OS dialog or remembering a window size needs the real onetree.screen_sizethe space the scene is laid out in: design units under FITnode.viewport_sizethe target that node draws into, which isscreen_sizeunless aSubViewportancestor renders it offscreen ========================== ==========================================A game that wants a screen-edge HUD wants EXPAND. Under FIT a HUD anchored to an edge lands on the edge of the design rect, not of the glass, which is right for a playfield and wrong for a HUD that wants the whole screen: FIT buys one coherent space at the price of the bars.
- property content_scale_size: tuple[float, float][source]¶
The design resolution
FITpreserves, in engine units.Defaults to the size the tree was built at, which is the size the game asked its window for, so a game that switches to FIT and says nothing else keeps the layout it authored. Ignored under
EXPAND.
- property content_scale: float[source]¶
Render-target units per engine unit: 1.0 under
EXPAND.Under
FITthis is how much the design rect is magnified to fill the window, which is what a delta measured on the glass has to be divided by to become a delta in the scene’s space. :meth:window_to_viewportdoes that for a position; this is for everything else.
- property presentation_rect: tuple[float, float, float, float][source]¶
(x, y, w, h)of the glass the scene’s space is presented into.In engine units, measured on :attr:
window_size. The whole window underEXPAND; underFITthe design rect scaled uniformly and centred, with the letterbox bars in the margin.The exact inverse of :meth:
window_to_viewport, and computed from the same placement, so what a renderer draws and what a pointer hits cannot drift: a point at the rect’s top-left corner converts to(0, 0)in the scene’s space, a point at its bottom-right to- Attr:
screen_size, and a point in a bar to a coordinate outside both.
- property screen_insets: tuple[float, float, float, float][source]¶
Edges of the scene’s space something is covering, as
(left, top, right, bottom).In screen pixels, measured inward from the edges of :attr:
screen_size, and all zero unless a backend says otherwise. A soft keyboard sliding up is a bottom inset, a notch is a top or side one, and a home indicator is a small bottom one.A node keeping something out of them reads
node.viewport_insetsinstead, which answers in the pixels of the space that node draws in: aControlunder :attr:ui_scalegets UI pixels it can use as margins directly, where these would be too large by the factor.Only a backend that can actually ask the platform writes this, and one that can declares
Input.capabilities.screen_insetswhen it does. A backend that cannot declares nothing and this stays zero, which is the honest answer rather than a gap: zero insets is exactly what a desktop window has.Assigning takes insets measured on the glass, which is what a platform reports, and is the backend’s door in the way :attr:
screen_sizeis. UnderFITthey are converted: the letterbox bars already sit between the window edge and the scene’s, so an inset that only reaches into a bar occludes nothing and arrives here as zero.A change, and only a change, re-captures every
on_drawthat read the insets and then firesscreen_insets_changedwith the new value, which is where code outsideon_drawthat keeps something clear of them listens. A backend may therefore write them every frame.
- property ui_scale: float[source]¶
How big the interface is drawn, as the player chose it. 1.0 is as authored.
Every
Controlin this tree is laid out and drawn at this size; the game world is not. A settings slider assigns it and the interface resizes on the next frame, the slider included::def on_ready(self): slider = self.add_child(Slider(1.0, 2.0, value=self.tree.ui_scale)) slider.value_changed.connect(self._set_ui_scale) def _set_ui_scale(self, value): self.tree.ui_scale = valueThe scale lives in the space rather than in the widgets: the outermost control of each interface lays out against
screen_size / ui_scaleand carries the factor in its transform, so fonts, paddings, minimum sizes and a game’s own hard-coded40.0all grow together, and a click finds the control it lands on because hit testing goes through the same transform. Text is laid out at the size it is drawn at rather than laid out small and magnified, so glyphs stay on the pixel grid at fractional factors.Control.viewport_sizeanswers in the divided space, which is the space a control’son_drawworks in.It is a preference, not a density: an engine unit is already one density-independent pixel, so a 44-unit button is the same physical size on every screen at 1.0 and nothing here should be multiplied by a display’s pixel ratio. It belongs to this tree, so an editor and the game running inside it are scaled independently, and it survives
- Meth:
change_scenebecause the tree does. Keeping it across runs is the game’s job: load it and assign it in the root’son_ready.
A world node’s
on_drawthat read it is re-captured when it changes, as every control’s is.Raises: ValueError: the value is not a finite number above zero.
- window_to_viewport(x: float, y: float) tuple[float, float][source]¶
A point on the glass, as a point in the space the scene is laid out in.
The identity under
EXPAND. UnderFITit undoes the letterbox, and a point in a bar comes back outside the design rect rather than clamped to its edge: the bars belong to no viewport, so a pointer over one has to hit nothing, and a coordinate that is simply out of range does that without anything having to check for it.
- property root: simvx.core.node.Node | None[source]¶
The current scene’s root node.
A sibling of this tree’s singletons under the tree’s own top node, and the node every scene-facing walk starts from: rendering, 3D submission, saving, hot reload and the editor’s scene panel all begin here, so a singleton is ticked but is not part of the scene.
Assigning re-links the child and runs no lifecycle, which is what assigning to it has always done. Use :meth:
set_rootto mount a scene (enter + ready) and :meth:change_sceneto swap one.
- set_root(root: simvx.core.node.Node)[source]¶
Mount
rootas the current scene.The root’s
input_context, like any node’s, comes into force as it enters, and goes with it when another scene replaces it.A scene already mounted is carried out first, so its
on_exit_treeruns, but it is not destroyed.change_sceneremains the call for a swap: it destroys the old scene and also clears popup state and the active 2D camera.The mount itself runs inside this tree’s input span, so
on_enter_tree/on_readycode sees this tree’sInputandInputMap, and any action it registers lands in this tree’s own map.
- change_scene(new_root: simvx.core.node.Node)[source]¶
End the current scene and mount
new_rootin its place.The old scene is destroyed: every node in it runs
on_exit_tree, then is finished exactly as :meth:Node.destroyfinishes a node, so its signal connections andEventBussubscriptions end there and then, and none of its nodes can be added to a tree again. A lambda or other strongly held callable it subscribed still fires until it is unsubscribed, as it would afterdestroy(). To carry state into the next scene, pass values tonew_root, not nodes of the old one.new_rootthen runs the full_enter_tree/_ready_recursivepath, identical to the initial root. Singletons the tree owns are left in place, keeping their groups and unique names; a singleton registered on a node the old scene owned leaves with that scene, and its name binding is dropped. UI popup state and the active 2D camera are cleared.Called from the scene’s own code, the call does not return. A hook, an input handler, a signal a control emitted or a contact handler that the tree is running for the scene ends that scene where it stands: the code that asked unwinds (its
finallyblocks run, with the old scene still in place), nothing further in the old scene runs that frame, and the tree then carries the old scene out and mountsnew_rootfrom its own frame, never from inside the old scene’s walk. So the first scene change a scene makes is the one that happens; a second node that would have decided otherwise later in the same walk is never reached. A scene changed fromon_updatestarts on the next frame; one changed from an input handler oron_fixed_updateruns the rest of the frame.Called from anywhere else (a singleton, a deferred call, a timer, code outside the frame), it swaps at once and returns with
tree.rootalready the new scene.The new scene is given no edge for anything already held when it arrived, because that press was delivered to the scene that ended: a held key reads at rest there until it is let go and pressed again.
A change asked for while the old scene is being carried out (from one of its
on_exit_treehooks) is ignored: the change already under way is the one that happens.Use this for title → gameplay → game-over navigation. See
- Doc:
../patternsfor a full example.
Raises: ValueError:
new_rootis the current scene or a node inside it, or has been destroyed or is about to be (a scene an earlierchange_scenereplaced, kept to come back to). Nothing has changed when it raises.
- flush_layout() None[source]¶
Reflow every dirty container now, root-first, and expire the rect cache.
Synchronous layout settle: after this returns, container children have their final positions/sizes for this frame, so
get_global_rectis valid immediately (no one-tick lag). Called at end-of-tick and, crucially, right when an overlay/popup opens so a freshly-shown dialog can be hit-tested and focus-elected on the same tick. Idempotent and O(1) when no container is dirty.The reflow itself runs inside this tree’s input span:
_do_update_layoutis an overridable hook, so a custom container laying itself out sees this tree’sInputandInputMap.
- tick(dt: float)[source]¶
Run process callbacks and coroutines on all nodes for one frame.
Order: singletons’
_processfirst, in registration order (global nodes, before the scene), thenself.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. Anythingemit_deferred’d during this frame’s singleton pass is also drained in the same flush, keeping the scene’s view of the world consistent.Singletons and the current scene are siblings under the tree’s own top node, and the scene is always its last child, so every node the tree ticks is reached through exactly one containment path and processes exactly once. A node registered as a singleton and parented into the scene is a scene node that happens to have a name: it processes in its scene position, not ahead of the scene.
- property physics_world: simvx.core.physics.world.PhysicsWorld[source]¶
The tree’s default PhysicsWorld, created lazily on first access.
Bodies with no PhysicsRoot ancestor resolve here. Lazy so non-physics scenes never allocate a world. Backend follows the selection precedence (no node override -> project
physics_backendsetting > auto-discovered native > Builtin), gravity -Y. Tree-scoped: it persists acrosschange_scene(likeevents) and is released only byquit()/ GC.
- property has_physics_world: bool[source]¶
True iff a default world has been lazily created (no allocation).
- property physics_world_2d: simvx.core.physics.world2d.Physics2DWorld[source]¶
The tree’s default 2D PhysicsWorld, created lazily on first access.
The 2D sibling of :attr:
physics_world. 2D bodies with noPhysicsRoot2Dancestor resolve here. Lazy so non-2D-physics scenes never allocate it. Backend follows the selection precedence (no node override -> projectphysics_backendsetting > auto-discovered native > Builtin), gravityVec2(0, -9.81)(Y-up). Tree-scoped: persists acrosschange_scene, released byquit()/ GC.
- register_physics_world(world: simvx.core.physics.world.PhysicsWorld) None[source]¶
Register a PhysicsRoot’s isolated world so physics_tick steps it.
Idempotent: a world already present is not added twice (defends against re-entrant enter_tree). Called from PhysicsRoot.on_enter_tree.
- unregister_physics_world(world: simvx.core.physics.world.PhysicsWorld) None[source]¶
Unregister a PhysicsRoot’s world (PhysicsRoot.on_exit_tree). No-op if absent.
The world will not be stepped by this tree again, so any body still in its retirement grace period is released here rather than left holding a node alive (see :meth:
retire_physics_node).
- register_physics_body(world: simvx.core.physics.world.PhysicsWorld, handle: simvx.core.physics.world.BodyHandle, node: simvx.core.node.Node) None[source]¶
Register a body node for post-step transform read-back.
Called by
PhysicsBody3D.on_enter_treefor non-static bodies. Marks the world’s membershipdirtyso the next sync rebuilds the bulk order + buffer. Weak reference: a GC’d node auto-drops.
- unregister_physics_body(world: simvx.core.physics.world.PhysicsWorld, handle: simvx.core.physics.world.BodyHandle) None[source]¶
Unregister a body node (
PhysicsBody3D.on_exit_tree). No-op if absent.Drops the whole per-world
_BodySynconce empty so idle worlds cost nothing.
- register_physics_node(world: simvx.core.physics.world.PhysicsWorld, handle: simvx.core.physics.world.BodyHandle, node: simvx.core.node.Node) None[source]¶
Register a body node in the collision-event handle->node map.
Called by
PhysicsBody3D.on_enter_treefor ALL modes (static included), so :meth:_dispatch_contact_eventscan resolve both sides of a contact pair. Mode-independent: unlikeregister_physics_bodythis map never changes on a mode flip. Weak reference: a GC’d node auto-drops.
- retire_physics_node(world: simvx.core.scene_tree.AnyPhysicsWorld, handle: simvx.core.physics.world.BodyHandle) None[source]¶
Move a body node out of the live map and into the retirement grace period.
Called by
PhysicsObject3D.on_exit_treeimmediately before the backend body is destroyed. Destroying a body ENDS every contact and sensor overlap it was in, and the seam reports those closingEXITs on the first drain after the NEXT step (see- Meth:
~simvx.core.physics.world.PhysicsWorld.destroy_body). Dropping the handle->node entry here would leave that dispatch unable to name the peer that went away, so the entry is retired instead: held, with a STRONG reference to the node (the live map is weak, and the game has usually let go of a node it destroyed), until the events it is owed have been dispatched.
The grace period is one step, measured on this tree’s per-world step count, and :meth:
_release_retired_physics_nodesends it. Retirement is never consulted before the live map, so a backend that recycles handles resolves a reused handle to its NEW body.One STEP, not one frame: a tree that stops stepping keeps the entry until it steps again, so pausing (or clearing
auto_physics) immediately after a kill holds that one node for the duration of the pause. It is released on the next step regardless, and bychange_sceneand by unregistering the world, so it cannot outlive the scene either way.No-op if the handle is not registered.
- interpolate_physics(alpha: float) None[source]¶
Write the render pose of every captured new-world body: lerp(prev, cur, alpha).
alphais the fixed-step fractionphysics_accum / physics_dtin [0, 1). Position lerps linearly; rotation slerps (shortest-arc, Quat.slerp). A sleeping body has prev == cur, so it lerps to a constant (correct, cheap). No-op whenphysics_interpolationis False (driver still calls it; the raw write already happened in _sync_physics_world during physics_tick).
- physics_tick(dt: float, *, tick_input: simvx.core.input.tick.TickInput | None = None)[source]¶
Run physics_process callbacks on all nodes, then auto-step physics.
The node walk runs against one :class:
~.input.tick.TickInput, which is whatInputanswers from insideon_fixed_update: held state as it was when the tick started, and the frame’s edges on the frame’s first tick and no other. Pass tick_input to drive a tick from a recorded or hand-built snapshot instead of from the live record; the physics step below is outside the span, since the solver asks nothing of input.
- capture_input(event: simvx.core.input.events.InputEvent) simvx.core.node.Node | None[source]¶
The capture phase: offer event root-first to the
capture=Truehandlers, and return who claimed it.Runs ahead of every control, so a handler here sees a key before the focused text field does, and a claim is the whole of the event: no control, no action and no other handler hears it, and its release is claimed with it. It is how an editor keeps F5 and Ctrl+S whatever has focus and whatever the game it hosts is doing. Root-first, the reverse of the ordinary deepest-first phase, so an outer scope decides before the scopes inside it. Free in a tree where no capture handler was ever registered.
- render(renderer)[source]¶
Draw the scene, then the overlay layer, into
renderer.Runs inside this tree’s input span:
on_drawis game code like any other hook, so a handler that readsInputor registers an action sees this tree rather than the process-wide default. That matters for an isolated tree, which is how the editor renders a game in play mode.
- input_cast(screen_pos: tuple[float, float] | numpy.ndarray, button: simvx.core.input.controls.DeviceButton = Pointer.PRIMARY, *, source: simvx.core.input.events.InputEvent | None = None, pressed: bool = True) simvx.core.node.Node | None[source]¶
Cast a ray from screen_pos through the camera and deliver the hit.
The ray leaves the camera the tree renders through (see
- Meth:
_active_camera_3d). The nearest pickable- Class:
CollisionShape3Dunder it wins, and its owner receives a- Class:
~simvx.core.input.events.PickEventcarrying the whole hit: the node, the world point, the surface normal there and the distance along the ray. Returns that node, orNonewhen the ray reached nothing.
A pickable shape whose owner is a body in a physics world is hit on its real collider, through that world’s raycast. A shape with no body in a world (a free collider, or one on a body whose collision layer is empty and so answers no query) is tested against its bounding sphere,
- Attr:
~CollisionShape3D.pick_radius.
pressed says which edge the pick is: a press’s pick reaches the node’s
@on_pickedhandlers for presses, then itson_pickedoverride; a release’s pick (pressed=False) reaches only the handlers declared withedge="release"or"both".The pointer pipeline calls this in its targeted phase, after the UI has declined the pointer, and passes the pointer event as source: for every press, and for a release only while a node in the tree declares a release pick handler. A game or a tool can cast from anywhere else, which is why it stays public.
- group(name, of=None, /)[source]¶
The members of group
name, in the order they joined it, as a new list.A node joins a group when
add_to_groupruns while it is in this tree, or when it enters the tree already carrying the group; leaving the group or the tree drops it, and joining again puts it last.- Meth:
Node.destroydrops the node from its groups at once, although it stays in the tree until the end of the frame.
of, a class or a tuple of classes, keeps only the members that are instances of it, and the list is typed as that class, so a group may hold several types and a loop calls the right one’s methods::for enemy in tree.group("enemies", Enemy): enemy.stun()A
runtime_checkableProtocol class asofmatches by methods, not by inheritance; mypy refuses one wheretype[T]is expected, so such a call carries# type: ignore[type-abstract]under mypy (pyright accepts it).The list is a snapshot: a member an earlier iteration destroys is still visited, as with any copy.
- first(name, of=None, /)[source]¶
The earliest-joined member of
namethat is an instance ofof(any type if omitted), orNone.
- create_timer(seconds: float) simvx.core.scene_tree.SceneTreeTimer[source]¶
Create a one-shot timer that emits
timeoutafterseconds.Returns immediately with a :class:
SceneTreeTimer; connect itstimeoutsignal to run code after the delay. The timer is owned and ticked by the tree (right after singletons) and discarded once it fires, so it needs no Node in the scene and no manual cleanup. Honourstree.paused. For delays inside a coroutine, preferwait(seconds).
- call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None[source]¶
Queue
method(*args)to run at the end of this frame (escape hatch).The tree-level counterpart to :meth:
Node.call_deferred, for code that has no node to defer from (plugins, singleton helpers, non-node systems). Same discouraged-escape-hatch guidance applies: prefer a safe-by-default path (destroy,Property(coalesce=True),events.publish_deferred) when one exists.methodis a callable, never a string.
- property singletons: dict[str, simvx.core.node.Node][source]¶
Read-only view of registered singletons.
- add_singleton(name: str, node: simvx.core.node.Node)[source]¶
Register
nodeas a persistent singleton attached to the tree.An unparented node is mounted as a child of the tree’s own top node: it enters the tree, runs
on_ready()immediately, processes ahead of the scene in registration order, and surviveschange_scene(). Reach for it when a node must outlive the scene that started it: music that keeps playing across a scene change, or a save file held open. Retrieve one viatree.singletons[name]; it is not reachable from the scene root.It is not a home for the game’s own state. A score belongs to whatever owns the run, and reaching it through a name from anywhere makes every reader of that name a writer of it.
A node the scene already owns may also be given a name. It stays where it is, so it processes in its scene position rather than ahead of the scene, and it leaves with its scene: after a
change_scenethe name is gone.A singleton is ticked but not drawn – drawing walks the scene. A global HUD is either a child of the scene or a full-screen overlay.
The name
"events"is reserved for the engine-provided- Class:
~simvx.core.event_bus.EventBus; usetree.eventsinstead.
Raises: ValueError: the name is
"events"; the name is already bound to a different node;nodealready holds a different name; ornodeis a child of some other tree.
- remove_singleton(name: str)[source]¶
Drop a singleton’s name binding, and unmount it if the tree owns it.
A singleton the tree mounted (registered while unparented) is carried out of the tree, so its
on_exit_treeruns and anything it registered on the way in is released. A singleton the scene owns keeps its place in the scene: dropping the name is the whole of the operation, because the node is a scene node that happens to have a name.
- unique(name: str) simvx.core.node.Node | None[source]¶
Get a unique node by name. Returns None if not found.
- clear_delete_queue() None[source]¶
Abandon every pending :meth:
Node.destroywithout running it.For a teardown that is carrying the whole tree out anyway (a harness dropping its tree): the queued nodes go with it, so the removal they were waiting for is moot. Clears their pending state as well, since a node left marked as destroying would report a removal that is never coming, and puts each one still in this tree back in its groups.
- listen_input(event: simvx.core.input.events.InputEvent) simvx.core.node.Node | None[source]¶
Offer event to the listener that is hearing input now, before anything else.
An :class:
~.input.listener.InputListenerin the tree hears every control event of the device types it listens to ahead of the UI and every handler, which is what lets a rebind screen take the next press without the game playing it. The newest listener hears first.Returns the listener that took the event, or
None.
- ui_key(event: simvx.core.input.events.KeyEvent) simvx.core.node.Node | None[source]¶
Route a key press or release to the UI.
The UI is handed the object the tree received, so a widget reads the physical key and the modifier flags off the same event a broadcast handler sees. Typed characters take :meth:
ui_text, pointers- Meth:
ui_pointerand the wheel :meth:ui_scroll.
Returns the node that took the key, or
Nonewhen the UI let it through. A focused text field swallowingWis what stops the character behind it walking.Routing runs inside this tree’s input span:
on_gui_input, focus signals and thepressed/releasedhandlers reached from here are arbitrary game code, and they see this tree’sInputandInputMap.
- ui_text(event: simvx.core.input.events.TextEvent) simvx.core.node.Node | None[source]¶
Route a typed character to the UI.
Returns the node that took it, or
None. Runs inside this tree’s input span for the same reason :meth:ui_keydoes.
- ui_pointer(event: simvx.core.input.events.PointerEvent) simvx.core.node.Node | None[source]¶
Route a pointer to the controls under it.
The UI is handed the object the tree received rather than a flattened copy of it, so a widget reads the phase, the button, the pressure and the pointer id off the same event a broadcast handler sees.
Returns the node that took the pointer, or
Nonewhen nothing in the UI stops it.
- ui_pad(event) simvx.core.node.Node | None[source]¶
Route a controller button or axis to UI navigation.
The pad’s half of
ui_key: the UI resolves it through the same navigation actions a key goes through, so a controller can move focus, activate what it lands on and dismiss a dialog. Runs inside this tree’s input span for the same reason :meth:ui_keydoes.Returns the node the pad’s press moved, activated or dismissed, or
Nonewhen navigation did nothing with it.
- ui_scroll(event: simvx.core.input.events.ScrollEvent) simvx.core.node.Node | None[source]¶
Route a wheel notch to the controls under it.
The UI receives the event the tree does, rather than a key named after it: a notch carries a position and a magnitude, and a key carries neither. Routing runs inside this tree’s input span for the same reason
- Meth:
ui_keydoes.
Returns the control that took the notch, or
None.
- flush_pointer_motion() None[source]¶
Deliver the frame’s accumulated pointer motion, from every router.
Motion is coalesced into one event per frame carrying the frame’s total (:meth:
InputRouter.flush_motion <simvx.core.input.router.InputRouter.flush_motion>), and this is where the frame spends it: at the top of the input phase, before physics, so a fixed tick reads a pointer position that is this frame’s. Several routers can feed one tree, and each holds its own device’s motion.
- cancel_capture(pointer_id: int, event: simvx.core.input.events.PointerEvent)[source]¶
Take a captured pointer back, telling whoever held it it was taken.
- input_handover(*, pointers: bool = True, keys: bool = True) None[source]¶
Input is leaving this tree. Close what it was holding, press nothing.
Called by whatever stops feeding the tree: a :class:
SceneHostthat stops forwarding, is hidden, leaves its tree or loses the focus, and an editor pausing the game it runs. Leaving a tree is a handover rather than a switch, and the two differ in what they leave behind. A switch simply stops delivering, and everything the tree was holding stays held for ever: the filed form of that is a game paused in the editor whose character walks into a wall on resume with nothing touching the keyboard.So the pointers in flight are cancelled, whoever holds one is told the gesture is over, the hover is left, and every open action closes with a real release edge. Nothing is pressed on arrival either: what the player is still physically holding does not count again until they let go and press.
Per kind: pointers is the mouse, the fingers and the pens; keys is the keyboard, the text session and the controllers. A host losing focus hands over the keys and leaves a drag in flight alone. The handover reaches every host inside this tree too, and never touches the device record.
Deliberately not :meth:
InputRouter.focus_lost <simvx.core.input.router.InputRouter.focus_lost>, which replays device releases. A window that lost focus will not see the key come up, so replaying it is the truth; here the key is still down under the player’s finger and device state must go on saying so.
- property focus_active: bool[source]¶
Whether the keyboard reaches this tree now.
A window’s tree has it while the window has the operating system’s focus; a hosted tree while its :class:
SceneHostholds focus in a tree that has it and is forwarding. A tree whose keyboard went elsewhere keeps its focused widget, drawn as it was, with this False.
- hit_test(point: tuple[float, float], *, pointer_type: simvx.core.input.enums.PointerType | str = PointerType.MOUSE, source_id: int = 0) simvx.core.node.Node | None[source]¶
What point would hit: the nearest control under it, else the pickable body its ray meets.
Writes no state: no hover, no focus, no pick delivered. The same answer the pointer pipeline acts on, so code that crosses a host boundary (a drag session, a tooltip) asks the tree on the far side the question it would ask its own. pointer_type matters because a finger hits a touch target anywhere in its grown area.
- property drag_threshold: dict[simvx.core.input.enums.PointerType, float][source]¶
How far a pointer travels from its press before the motion is a drag, per pointer type.
The default every node inherits;
node.drag_thresholdoverrides it for a subtree. Assigning a map replaces the types it names and keeps the rest.
- property focus_owner: simvx.core.node.Node | None[source]¶
The node holding keyboard focus, or None. Read-only:
grab_focus()andrelease_focus()move it.
- property focus_reason: simvx.core.input.enums.FocusReason[source]¶
Why keyboard focus last moved: a click, Tab, a direction, hover, an overlay, code, or the owner leaving.