Lifecycle

the order node hooks fire, made visible.

▶ Run in browser

Tags: 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_tree bracket each stay in the tree, and repeat for as long as the node keeps coming back. Per-entry setup belongs there.

  • on_ready fires 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 its on_exit_tree; adding it back replays on_enter_tree alone.

  • The mirror rule that follows: whatever on_exit_tree undoes has to be redone in on_enter_tree. Put it in on_ready and 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())