Lifecycle¶
the order node hooks fire, made visible.
▶ Run in browserTags: basics lifecycle node
Every node runs through the same hooks in the same order: on_enter_tree when
it is attached to the running tree, on_ready right after, then on_update(dt)
every frame, and on_exit_tree when it leaves. The two entry hooks are not the
same shape, and this is where that shows: one child node is added and removed on
a repeating timer, and each hook appends to a shared log, so you can watch
on_enter_tree fire on every entry while on_ready fires exactly once, for the
first one.
What it demonstrates¶
Hook order:
on_enter_tree->on_ready->on_update(dt)(every frame) ->on_exit_tree.on_enter_tree/on_exit_treebracket each stay in the tree, and repeat for as long as the node keeps coming back. Per-entry setup belongs there.on_readyfires once per node, ever. A node that is removed and re-added does not ready again, so the children it spawns there are built once rather than once per entry.on_update(dt)runs once per frame while the node is in the tree.Removing a child (
remove_child) fires itson_exit_tree; adding it back replayson_enter_treealone.The mirror rule that follows: whatever
on_exit_treeundoes has to be redone inon_enter_tree. Put it inon_readyand the node comes back dead the second time it joins a tree.
Source¶
1"""Lifecycle: the order node hooks fire, made visible.
2
3Every node runs through the same hooks in the same order: `on_enter_tree` when
4it is attached to the running tree, `on_ready` right after, then `on_update(dt)`
5every frame, and `on_exit_tree` when it leaves. The two entry hooks are not the
6same shape, and this is where that shows: one child node is added and removed on
7a repeating timer, and each hook appends to a shared log, so you can watch
8`on_enter_tree` fire on every entry while `on_ready` fires exactly once, for the
9first one.
10
11# /// simvx
12# tags = ["basics", "lifecycle", "node"]
13# web = { root = "LifecycleDemo", width = 800, height = 600, responsive = true }
14# ///
15
16## What it demonstrates
17
18- Hook order: `on_enter_tree` -> `on_ready` -> `on_update(dt)` (every frame) -> `on_exit_tree`.
19- `on_enter_tree` / `on_exit_tree` bracket each stay in the tree, and repeat for as long as the node
20 keeps coming back. Per-entry setup belongs there.
21- `on_ready` fires once per node, ever. A node that is removed and re-added does not ready again, so
22 the children it spawns there are built once rather than once per entry.
23- `on_update(dt)` runs once per frame while the node is in the tree.
24- Removing a child (`remove_child`) fires its `on_exit_tree`; adding it back replays `on_enter_tree`
25 alone.
26- The mirror rule that follows: whatever `on_exit_tree` undoes has to be redone in `on_enter_tree`.
27 Put it in `on_ready` and the node comes back dead the second time it joins a tree.
28"""
29
30from simvx.core import Node2D
31from simvx.graphics import App
32
33WIDTH, HEIGHT = 800, 600
34
35SPAWN_AT = 0.3 # seconds into a cycle at which the tracked child is added
36REMOVE_AT = 3.0 # seconds into a cycle at which it is removed again
37CYCLE = 4.0 # length of one add/remove pass, repeated for as long as the demo runs
38
39
40class Tracked(Node2D):
41 """A node that records each lifecycle hook into the demo's shared log."""
42
43 def __init__(self, log, **kwargs):
44 self._log = log
45 self.updates = 0
46 self.entries = 0
47 self.readies = 0
48 super().__init__(**kwargs)
49
50 def _note(self, event):
51 # Keep the log short enough to render; newest entries stay visible.
52 self._log.append(event)
53 del self._log[:-10]
54
55 def on_enter_tree(self):
56 # Per-entry setup belongs here: this node re-enters the tree every cycle,
57 # so anything that must be true for each stay is reset here.
58 self.updates = 0
59 self.entries += 1
60 self._note(f"on_enter_tree (entry #{self.entries})")
61
62 def on_ready(self):
63 # Once per node, ever. Build the things this node owns for its whole
64 # lifetime here -- child nodes, signal connections, cached lookups.
65 self.readies += 1
66 self._note(f"on_ready (#{self.readies}: once and never again)")
67
68 def on_update(self, dt: float):
69 self.updates += 1
70 if self.updates <= 2: # log only the first few; updates recur every frame
71 self._note(f"on_update (#{self.updates})")
72
73 def on_exit_tree(self):
74 self._note("on_exit_tree")
75
76
77class LifecycleDemo(Node2D):
78 # The retained 2D renderer only re-runs on_draw when a node is "dirty"
79 # (a Property changed or queue_redraw() was called). This demo draws a live
80 # elapsed-time readout (self._t) and a log of plain state every frame, none
81 # of which is a Property, so it opts into per-frame redraw with dynamic.
82 dynamic = True
83
84 def on_ready(self):
85 self.log: list[str] = []
86 self._t = 0.0
87 # One node, added and removed over and over, so the entry hooks replay
88 # each cycle. `parent` is the source of truth for "is it in the tree".
89 self.tracked = Tracked(self.log)
90
91 def on_update(self, dt: float):
92 self._t += dt
93 in_tree = self.tracked.parent is not None
94 wants_child = SPAWN_AT <= self._t % CYCLE < REMOVE_AT
95 if wants_child and not in_tree:
96 # Attaching to the running tree triggers enter_tree, and ready only
97 # the first time round.
98 self.add_child(self.tracked)
99 elif in_tree and not wants_child:
100 # Detaching fires the child's on_exit_tree.
101 self.remove_child(self.tracked)
102
103 def on_draw(self, renderer):
104 renderer.draw_text("Node lifecycle hooks (in firing order)", (20, 20), scale=2, colour=(1, 1, 1))
105 state = "child in tree" if self.tracked.parent is not None else "no child"
106 status = f"cycle t = {self._t % CYCLE:4.1f}s of {CYCLE:.1f}s ({state})"
107 renderer.draw_text(status, (20, 56), scale=1, colour=(0.7, 0.7, 0.7))
108 tally = f"entries: {self.tracked.entries} readies: {self.tracked.readies}"
109 renderer.draw_text(tally, (20, 84), scale=1, colour=(0.9, 0.8, 0.4))
110 for i, entry in enumerate(self.log):
111 renderer.draw_text(f"{i + 1:2d}. {entry}", (40, 128 + i * 28), scale=1, colour=(0.5, 0.9, 0.6))
112
113
114if __name__ == "__main__":
115 App(title="Lifecycle", width=WIDTH, height=HEIGHT).run(LifecycleDemo())