Node System¶
Nodes are the building blocks of every SimVX game. They form a tree hierarchy managed by the SceneTree.
Lifecycle¶
Every node goes through these stages. All lifecycle hooks use the on_ prefix to avoid shadowing Python builtins (input) and common method names (process, draw).
Construction –
__init__()sets up propertiesEnter tree – Node is added via
add_child();on_enter_tree()firesReady –
on_ready()is called once, after all children are ready (bottom-up)Process –
on_process(dt)runs every framePhysics process –
on_physics_process(dt)runs at fixed intervals (default 60 Hz)Input –
on_input(event)thenon_unhandled_input(event)for events not consumed by UI / earlier handlersDraw –
on_draw(renderer)is called every frame for 2D draw commandsExit tree –
destroy()triggerson_exit_tree(); children are recursively removed at the end of the frame
Multiple handlers for the same hook can be registered with decorators (@on_process, @on_input(action="jump"), @on_input(key=Key.ESCAPE, ctrl=True)). Decorated handlers run in declaration order; a truthy return from an @on_input handler consumes the event.
import math
class Enemy(CharacterBody3D):
health = Property(100, range=(0, 200))
def on_ready(self):
self.health = 100
def on_process(self, dt):
self.rotate((0, 1, 0), math.radians(45) * dt) # 45°/sec: rotate() takes radians
def on_physics_process(self, dt):
self.move_and_slide(dt)
Properties¶
Property declares editor-visible, serializable values with optional validation:
from simvx.core import Node3D, Property
class Tank(Node3D):
speed = Property(5.0, range=(0, 20), hint="Movement speed")
armor = Property("heavy", enum=["light", "medium", "heavy"])
active = Property(True)
health = Property(100, on_change="_on_health_changed", persist=True)
def _on_health_changed(self, old, new):
if new <= 0:
self.died.emit()
Constructor arguments:
Arg |
Purpose |
|---|---|
|
initial value (use |
|
numeric clamp + inspector slider range |
|
string enum + inspector dropdown |
|
tooltip text shown in the editor inspector |
|
name of a zero-arg bound method invoked after each value change. Hooks fired while |
|
child’s resolved value is offset from the parent’s same-named property (cumulative scale, accumulated tint, etc.) |
|
bool/enum properties inherit disabling values from parents (e.g. |
|
included in |
|
schema version recorded with the persisted value |
Query all properties on a class with get_properties():
Tank.get_properties() # {"speed": Property(...), "armor": Property(...), ...}
Hierarchy¶
Nodes have a single parent and any number of children:
root = Node(name="Root")
player = Node3D(name="Player")
camera = Camera3D(name="Camera", position=(0, 5, 10))
root.add_child(player)
player.add_child(camera)
# Access children by name or index
root.children["Player"] # by name
root.children[0] # by index
# Path-based access on the node itself
root["Player/Camera"] # slash-separated path
camera.get_node("../Player") # relative path (sibling)
camera.get_node("/Root") # absolute path
# Node properties
camera.parent # direct parent
camera.path # "/Root/Player/Camera"
camera.tree # the SceneTree
Reordering children¶
The Children collection isn’t a Python list: don’t reach for
children._list, children.remove(...), or children.insert(...).
Use the dedicated reorder methods on the collection. They mutate the
draw / hit-test order in place, are no-ops for a node that isn’t a
child, and run in O(n):
parent.children.move_first(node) # index 0 → drawn first, hit-tested last
parent.children.move_last(node) # last index → drawn last (on top), hit-tested first
Use move_last to bring a UI panel to the foreground when it gains
focus; use move_first to push a background layer behind its siblings.
Signals¶
Signals provide decoupled event communication. connect() returns a Connection handle for later disconnection:
from simvx.core import Signal
class HealthComponent(Node):
def __init__(self):
super().__init__()
self.died = Signal()
self.hp = 100
def take_damage(self, amount):
self.hp -= amount
if self.hp <= 0:
self.died.emit()
# Connect and get a Connection handle
health = HealthComponent()
conn = health.died.connect(lambda: print("Game over"))
# One-shot connection: auto-disconnects after first call
health.died.connect(on_first_death, once=True)
# Disconnect manually
conn.disconnect()
Groups¶
Tag nodes for batch operations:
enemy.add_to_group("enemies")
enemy.is_in_group("enemies") # True
# Get all nodes in a group
all_enemies = scene_tree.get_group("enemies")
Coroutines¶
Generator-based coroutines for sequential async logic:
from simvx.core import wait, parallel, tween
def cutscene(self):
yield from wait(1.0) # pause 1 second
yield from tween(cam, "position", target, 2.0) # animate
yield from wait(0.5)
self.start_dialog()
# Run multiple animations simultaneously
self.start_coroutine(parallel(
tween(a, "position", pos_a, 1.0),
tween(b, "position", pos_b, 1.0),
))
Destroying Nodes¶
Remove nodes safely at the end of the frame:
enemy.destroy()
Finding Nodes¶
# Find first child of type
camera = root.find(Camera3D)
# Find all descendants of type
all_lights = root.find_all(Light3D)
all_meshes = root.find_all(MeshInstance3D, recursive=True)
API Reference¶
See simvx.core.node for the complete Node API.