simvx.core.node

Node: Base node class with tree hierarchy, groups, and coroutine support.

Module Contents

Classes

Node

Base node with tree hierarchy, groups, and coroutine support.

Timer

Node that emits :attr:timeout after duration seconds.

Data

log

T

D

API

simvx.core.node.log

‘getLogger(…)’

simvx.core.node.T

‘TypeVar(…)’

simvx.core.node.D

‘TypeVar(…)’

class simvx.core.node.Node(name: str = '', **kwargs)[source]

Base node with tree hierarchy, groups, and coroutine support.

Attributes: name: Unique name within the parent’s children. Defaults to the class name. parent: The parent Node, or None if this is the root. children: Ordered collection of child nodes, accessible by name or index. visible: Whether this node (and its descendants) should be drawn. update_mode: Controls processing behaviour during pause (INHERIT, PAUSABLE, PAUSED_ONLY, ALWAYS, DISABLED). unique_name: When True, the node is registered in the tree for fast lookup via SceneTree.get_unique_node().

Example::

root = Node(name="Root")
child = Node(name="Child")
root.add_child(child)
assert child.parent is root
assert root.children["Child"] is child

Initialization

strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)[source]
property name: str[source]

This node’s name, which is never empty.

The empty string is not a name a node can hold: it cannot be found by

Meth:

find, it cannot appear in a node path, and a scene that saved one would load back a node named after its type. Assigning it raises; pass no name to the constructor to get the type name instead.

property visible_in_tree: bool[source]

Whether this node is effectively visible: its own flag AND every ancestor’s.

visible answers only for this node, so a visible node under a hidden parent still reports True there. This is the answer the draw walk uses, and it is the one to test before skipping work for a hidden subtree.

Maintained on every visibility change and every reparent, so reading it is a cached lookup rather than a walk up the parents. A node with no parent reports its own visible.

reset_error() None[source]

Clear script error flag to re-enable processing.

add_child(node: simvx.core.node.T) simvx.core.node.T[source]

Add a node as a child, reparenting it if already in a tree.

Returns node itself, at the type it was passed, so the self.hero = self.add_child(Sprite2D(...)) the tutorials teach keeps the child’s own type rather than widening it to Node.

Args: node: The node to add. Removed from its current parent first.

Raises: ValueError: node is self or one of self’s ancestors – either would create a cycle in the scene tree – or :meth:destroy has been called on node, which is final.

remove_child(node: simvx.core.node.Node) None[source]

Remove a child node from this node’s children.

Immediate: node has left the tree by the time this returns, so its on_exit_tree has already run. Prefer :meth:destroy from inside a signal handler or a lifecycle hook, which runs the same teardown at the end of the frame instead of in the middle of a dispatch.

reparent(new_parent: simvx.core.node.Node)[source]

Remove from current parent and add to new_parent.

node_at(path, default=_NO_DEFAULT)[source]

Navigate the tree by path: 'Child/GrandChild' or '../Sibling'.

A leading / starts from the tree root, which the path may name as its first segment ('/Root/Player').

default works as it does on :func:getattr: without one, a path that names no such child raises :class:NodeNotFound; with one, that miss returns the default instead, so node_at("HUD/Minimap", None) is how an optional node is read. A malformed path – '..' past the root – is a bug in the caller rather than an absent node, and raises either way.

find(target, *, direct: bool = False)[source]

First descendant matching target, or None.

target may be:

  • a :class:Node subclass: matches the first isinstance descendant. The result is typed as that subclass (find(Player) -> Player | None), so no cast is needed.

  • a str: matches the first descendant whose name equals it.

  • a predicate (Node) -> bool: matches the first descendant it accepts.

Search is depth-first, pre-order, and recursive by default. Pass direct=True to consider only this node’s direct children.

find_all(target, *, direct: bool = False)[source]

All descendants matching target (same matcher rules as :meth:find), in depth-first pre-order. Recursive by default; direct=True limits the search to direct children. Returns [] when nothing matches.

expect(target, *, direct: bool = False)[source]
Meth:

find, but a miss is an error rather than a None.

Same matcher rules and the same typed result. Use it wherever the scene is expected to contain the node and a missing one is a bug in the scene: it fails at the lookup, naming what was asked for, instead of handing back a None that raises somewhere later with no clue why.

Raises: NodeNotFound: nothing under this node matches target.

ancestor(target)[source]

Nearest ANCESTOR matching target, or None.

The upward counterpart of :meth:find, with the same matcher rules and the same typed result, walking parents from this node outwards. self is never a candidate. This is how a node reaches the container it lives under (self.ancestor(Inventory)) without hard-coding how deep it sits.

walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node][source]

Iterate this node and all descendants in DFS pre-order.

property path: str[source]
property is_scene_root: bool[source]

Whether this node is the top of its scene.

True for a node with no parent, and for the current scene root of a

Class:

~simvx.core.scene_tree.SceneTree, whose parent is the tree’s own top node rather than another scene node. This is the test to make before offering to delete, duplicate or reparent a node: a scene root can do none of the three.

add_to_group(group: str)[source]

Add this node to a named group.

remove_from_group(group: str)[source]

Remove this node from a named group.

is_in_group(group: str) bool[source]

Check if this node belongs to a named group.

on_ready() None[source]

Called once, ever, after the node and all its children enter the tree.

Override to perform initialisation that requires the scene tree – finding sibling nodes, connecting signals, spawning children. The tree property is available. Called after on_enter_tree() and after all children’s on_ready().

Decorate other methods with @on_ready to register additional ready handlers; they fire after the override in declaration order.

Once per node instance: a node that leaves the tree and comes back does not ready again, so the children spawned here are built once rather than once per entry, and the signals connected here are connected once rather than stacking up a duplicate per entry.

Which of the two entry hooks::

on_enter_tree()     every entry, for as long as the node keeps
                    coming back -- per-stay setup
on_ready()          once per node instance, ever -- one-time setup

The mirror rule follows from that: whatever on_exit_tree() undoes has to be redone in on_enter_tree(). Putting it here leaves the node dead the second time it joins a tree.

Example::

def on_ready(self):
    self.sprite = self.node_at("Sprite")
    self.health_changed.connect(self._update_hud)
on_enter_tree() None[source]

Called on every entry into the scene tree, before on_ready().

Override for the setup that belongs to each stay in the tree: claiming a slot on the tree, subscribing to a tree signal, restarting whatever on_exit_tree() stopped. Unlike on_ready(), which fires once per node instance and never again, this fires as often as the node is added.

The whole incoming subtree is bound to the tree before any of its entry hooks run, so a hook may look up nodes the walk has not reached yet – by group, by unique name or by path – and every one of them already answers node.tree. Their own on_enter_tree may not have run yet, though, so read their state rather than depending on their setup; on_ready() is where the subtree is fully built.

Example::

def on_enter_tree(self):
    self.add_to_group("enemies")
    self.tree.screen_resized.connect(self._relayout)
on_exit_tree() None[source]

Called when the node is about to leave the scene tree.

Override to clean up resources, disconnect external signals, or persist state. Children have already exited by the time this fires on the parent.

Example::

def on_exit_tree(self):
    self.save_progress()
    self.remove_from_group("enemies")
on_update(dt: float) None[source]

Called every frame for game logic.

Args: dt: Seconds elapsed since the previous frame (variable timestep).

Override for movement, AI, animation triggers, or any per-frame update. Obeys update_mode – disabled or paused nodes are skipped automatically.

Decorate other methods with @on_update to register additional per-frame handlers; they fire after the override in declaration order. For state held while a button is pressed, poll Input.is_action_pressed("name") from inside on_update.

Example::

def on_update(self, dt):
    self.position += self.velocity * dt
on_fixed_update(dt: float) None[source]

Called at a fixed timestep (default 60 Hz) for physics logic.

Args: dt: Fixed time step in seconds (e.g. 1/60).

Override for deterministic physics updates – forces, collision responses, rigid-body integration. Runs independently of the render frame rate.

Example::

def on_fixed_update(self, dt):
    self.velocity += self.gravity * dt
    self.move_and_slide(dt)
on_draw(renderer) None[source]

Called each frame for custom 2D drawing.

Args: renderer: The active draw-command recorder (e.g. Draw2D).

Override to issue immediate-mode draw calls such as draw_line, draw_rect, or draw_text. Called only when visible is True.

The 2D renderer is retained (“build once”): output is re-collected only when a Property changes. If on_draw reads non-Property state (a plain attribute updated by a signal or timer, tree.now animation), call :meth:queue_redraw when that state changes so the new frame is collected. This is identical on live and headless: a body that mutates without queue_redraw freezes on both.

Example::

def on_draw(self, renderer):
    renderer.draw_circle(self.world_position, 10, colour=(1, 0, 0, 1))
on_picked(event: simvx.core.events.InputEvent) None[source]

Called when a 3D mouse-pick event hits this node’s collision shape.

Args: event: The input event containing click position, camera ray, etc.

Override to react to direct interaction with this 3D object – selection, dragging, context menus.

Example::

def on_picked(self, event):
    if event.button == MouseButton.LEFT:
        self.selected = True
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None[source]

Called for input events that no @on_input handler consumed.

Args: event: The unhandled input event.

Use for catch-all bindings such as global debug toggles or pause menus that should only fire when no other handler returned a truthy value to consume the event. For most input handling use @on_input(...) decorators with explicit filters; the dispatch tables route them directly without walking the tree.

Example::

def on_unhandled_input(self, event):
    if event.key == Key.F3:
        self.toggle_debug_overlay()
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle[source]

Register a generator coroutine to run each frame. Returns a cancellable handle.

stop_coroutine(gen_or_handle)[source]

Stop and remove a running coroutine (accepts generator or CoroutineHandle).

queue_redraw() None[source]

Mark this node’s on_draw output stale (re-capture it next frame).

The manual escape hatch for an on_draw body that reads non-Property state and changes ONCE (a signal/timer poke). For per-frame animation set

Attr:

dynamic instead. Idempotent and cheap (a no-op once already dirty).

Drawable2D (every Node2D / Control / CanvasLayer) also gets this called automatically by the blanket Property.__set__ hook on any changed Property, so drawing from Property state never needs it. A plain Node HUD/menu (_render_auto_dirty is False) calls it by hand.

property render_dirty: bool[source]

Whether on_draw output changed since the last upload (introspection).

clear_children()[source]

Destroy all children of this node.

destroy()[source]

Schedule this node for removal at the end of the current frame.

This is the engine’s deferred removal, and the only spelling of it: a node marked here stays alive, in the tree, for the rest of the frame, and its subtree is carried out in one piece at the end-of-frame sync point. That is what makes it safe to call from a signal handler, a collision callback or a lifecycle hook, where tearing a node down on the spot would mutate a structure the caller is still walking.

Meth:

remove_child is the immediate counterpart, for the cases that genuinely need the node gone before the call returns (reparenting).

Removal does not cost a node the events it was still owed, and that is true of remove_child too: the physics seam reports the contacts a destroyed body was in on the step after it goes, and separated / body_exited carry that node. The seam holds it for that step itself, so the naming does not depend on which removal path was taken.

Always final. A node that is not in a tree has no frame to defer to, so it is finished on the spot rather than ignoring the call; one that is in a tree is queued for the end of the frame. Either way the node is spent afterwards, and :meth:add_child refuses to put it back in a tree.

Idempotent: a second call does nothing, before or after the removal has run, so the same enemy can be destroyed by two handlers in one frame.

Attr:

destroying reports the pending state.

Signal connections made through this node’s bound methods are proactively disconnected so emitters stop dispatching to it on the next emit. Lazy weak-ref cleanup in Signal.__call__ covers nodes that are GC’d without destroy().

property destroying: bool[source]

Whether :meth:destroy has been called and the removal has not run yet.

True from the destroy() call until the end-of-frame drain carries the node out of the tree, and False again afterwards (the node object itself stays valid; Python frees it when the last reference goes). Use it to skip a node that is already on its way out::

for enemy in self.tree.group("enemies"):
    if enemy.destroying:
        continue
    enemy.take_damage(1)

Per node, not per subtree: a child of a destroying node reports False until it is itself queued. Only nodes queued while in a tree are ever marked: a detached node has no frame to defer to, so destroy() on one finishes it immediately and this never turns True for it.

call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None[source]

Escape hatch: run method(*args) at the end of this frame, outside tree traversal, instead of now.

Discouraged: prefer a safe-by-default path when one exists. SimVX already makes the common cases safe without deferring: the process loop and signal dispatch iterate snapshots (so adding/removing nodes mid-loop does not corrupt iteration), :meth:destroy is already a deferred delete, Property(coalesce=True) collapses repeated writes, and tree.events.publish_deferred(...) decouples event delivery. Reach for call_deferred only when you must mutate from a context none of those cover, and document why at the call site.

method is a bound method or any callable (type-safe; never a string method name). Calls run once, in queue order, at the end-of-frame sync point; anything queued during that drain runs on the next frame. A call bound to this node is dropped if the node has left the tree by the time the queue drains, and runs through :meth:_safe_call so a failure obeys the same strict/release policy as any other lifecycle hook.

property app[source]

The App running this node’s scene tree. Available after enter_tree().

property tree: simvx.core.scene_tree.SceneTree[source]

The SceneTree this node belongs to.

property physics[source]

Spatial-query accessor bound to this node’s physics world, or None.

Mirrors :attr:app / :attr:tree: available once in-tree. Returns a PhysicsQuery scoped to the same world the node’s body lives in (resolved via the nearest PhysicsRoot ancestor, else the tree default), exposing raycast / raycast_all / shapecast / overlap with typed results and mask / exclude filters. Built fresh per access (not cached): the resolved world can change across re-parent / change_scene, and the wrapper is a thin two-reference object on the cold query path. Bind it locally if a hot loop wants to reuse it.

property physics_2d[source]

2D spatial-query accessor bound to this node’s 2D physics world, or None.

The 2D sibling of :attr:physics: available once in-tree, returns a PhysicsQuery2D scoped to the same 2D world the node’s body lives in (resolved via the nearest PhysicsRoot2D ancestor, else the tree’s 2D default), exposing raycast / raycast_all / shapecast / overlap with typed 2D results and mask / exclude filters. Built fresh per access (cold query path).

__getitem__(key: str)[source]

Shorthand for node_at: self["Child/Path"].

classmethod get_properties() dict[str, simvx.core.descriptors.Property][source]

Return all Property descriptors declared on this node class and its bases.

__repr__()[source]
class simvx.core.node.Timer(duration: float = 1.0, one_shot: bool = True, autostart: bool = False, **kwargs)[source]

Bases: simvx.core.node.Node

Node that emits :attr:timeout after duration seconds.

A Timer counts down in on_update, so it runs on frame time, not on the fixed-step physics clock, and it obeys update_mode: a PAUSABLE Timer stops counting while the tree is paused. Nothing happens until

Meth:

start is called or autostart is passed to the constructor.

With one_shot (the default) the timer fires once and stops. With one_shot=False it repeats, and the leftover time from the frame that crossed zero is carried into the next cycle, so a repeating timer does not drift at low frame rates.

Connect to it like any other signal::

timer = Timer(duration=0.5, one_shot=False, autostart=True)
timer.timeout.connect(self.spawn_enemy)
self.add_child(timer)
Attr:

time_left reports the seconds remaining and :attr:stopped whether the countdown is idle; :meth:stop resets both without emitting.

Initialization

duration

‘Property(…)’

one_shot

‘Property(…)’

autostart

‘Property(…)’

start(duration: float = 0)[source]

Start or restart the timer, optionally overriding duration.

stop()[source]

Stop the timer and reset time_left to zero.

property stopped: bool[source]
property time_left: float[source]
on_update(dt: float)[source]
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_enter_tree() None
on_exit_tree() None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()