Event Bus¶
tree.events is a typed publish and subscribe channel for events that anyone in the tree might
care about. A producer publishes a dataclass instance describing what happened; a handler subscribes
to that dataclass type. Every SceneTree has one, with nothing to set up.
self.tree.events.subscribe(PlayerDied, self._on_player_died)
self.tree.events.publish(PlayerDied(player=self))
Signals or the event bus¶
Use a |
Use |
|---|---|
the receiver already holds the emitter (a parent watching a child) |
producer and consumer are far apart (different branches, singletons, plugins) |
the connection lives and dies with one instance |
subscribers come and go independently of any publisher |
you want a light per-instance callback list |
you want a typed payload that the IDE can follow |
Pick one per event. A signal is emitted with emit (self.died.emit()); the bus uses subscribe,
publish and unsubscribe.
Defining events¶
An event is a dataclass instance; frozen is recommended, since handlers should not change the
payload. publish raises TypeError for anything else, so every event is a type you can search
for.
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .nodes.player import Player
@dataclass(frozen=True)
class PlayerDied:
player: Player
@dataclass(frozen=True)
class PlayerLevelledUp:
player: Player
new_level: int
Give each situation one event class, and let handlers read related state from the payload or the node it references.
Subscribing¶
class HUD(Node):
def on_ready(self) -> None:
self.tree.events.subscribe(PlayerDied, self._on_player_died)
self.tree.events.subscribe(PlayerLevelledUp, self._on_level_up)
def _on_player_died(self, evt: PlayerDied) -> None:
self.show_game_over_for(evt.player)
def _on_level_up(self, evt: PlayerLevelledUp) -> None:
self.flash_level_banner(evt.new_level)
Subscribe in on_ready(), when self.tree is available, not in __init__.
A node’s handlers end with the node. A bound method of a Node is held weakly and its
subscription ends when the node is destroyed with destroy() or garbage-collected, and a scene
replaced by change_scene is destroyed, so every subscription its nodes held ends with the swap.
Leaving the tree without being destroyed, through remove_child, does not end it. Every other handler, a lambda, a
closure, a functools.partial, a free function or a method of a plain object, is held until you call
unsubscribe with it, even a lambda that captures a node which has since been destroyed:
on_died = lambda evt: print(evt.player)
self.tree.events.subscribe(PlayerDied, on_died)
...
self.tree.events.unsubscribe(PlayerDied, on_died)
Handlers run in the order they subscribed, and subscribing the same handler twice registers it once.
Exact-type dispatch¶
A handler hears only the exact class it subscribed to, never subclasses, so adding an event subclass cannot change what existing handlers see, and dispatch is one lookup:
@dataclass(frozen=True)
class EnemyDied:
enemy: Enemy
@dataclass(frozen=True)
class BossDefeated(EnemyDied):
boss: Boss
bus.subscribe(EnemyDied, on_any_enemy_died) # not called for BossDefeated
bus.subscribe(BossDefeated, on_boss_defeated)
bus.publish(BossDefeated(enemy=b, boss=b)) # only on_boss_defeated runs
To reach both, publish both:
def die(self) -> None:
self.tree.events.publish(BossDefeated(enemy=self, boss=self))
self.tree.events.publish(EnemyDied(enemy=self))
Immediate and deferred dispatch¶
bus.publish(event) # handlers run before publish returns
bus.publish_deferred(event) # handlers run at the next frame's flush
The tree flushes deferred events once per frame, before the scene’s on_update. Defer when:
you are in
on_fixed_updateand the UI should react on the next frame, not mid-physics;a handler publishes further events, to keep recursion shallow;
many producers publish in one frame and you want the UI to update once.
Events queued during a flush are dispatched at the next one.
The bus is not thread-safe: publish and subscribe only from the main thread.
Lifetime¶
The bus survives change_scene(): subscriptions held by singletons and other long-lived objects
keep firing across scenes, and those held by the replaced scene’s nodes end as it is destroyed.
The singleton name events is reserved, so tree.events can never be shadowed:
self.tree.add_singleton("events", ...) raises ValueError.
Example: Dungeon Explorer¶
examples/demos/dungeon_explorer/ uses the bus for everything that crosses node boundaries. Its
events live in events.py; the player publishes them:
class Player(CharacterBody2D):
def take_damage(self, amount: float) -> None:
if self.hp <= 0:
return
self.hp = max(0, self.hp - amount)
if self.hp == 0:
self.tree.events.publish(PlayerDied(player=self))
and the HUD subscribes, knowing only the event classes, never Player:
class PlayerHUD(Control):
def on_ready(self) -> None:
self.tree.events.subscribe(PlayerDied, self._on_player_died)
self.tree.events.subscribe(BossDefeated, self._on_boss_defeated)
A save screen can publish SaveSucceeded or SaveFailed the same way so an achievement tracker
reacts without knowing about the button; see Save system.