Patterns¶
Cross-cutting idioms that don’t belong to one class. Each entry shows the canonical API for a pattern Godot and other engine users will search for.
Autoloads — persistent singletons¶
An autoload is a node registered on the SceneTree itself. It lives outside the scene root, so change_scene() leaves it untouched — perfect for global state (score, settings, audio manager).
class GameState(Node):
lives = Property(3)
score = Property(0)
# Register once, at app start
app.tree.add_autoload("GameState", GameState())
# Reach it from anywhere in the scene
gs = self.tree.autoloads["GameState"]
gs.score += 100
SceneTree.add_autoload(name, node) calls _enter_tree() and ready() immediately. Remove with remove_autoload(name). Autoloads preserve their groups and unique-name registrations across change_scene.
Scene transitions¶
Swap the active root to move between screens. Autoloads persist; scene-local groups and unique nodes are rebuilt for the new tree.
class TitleScreen(Node):
def ready(self):
self["PlayButton"].pressed.connect(self._on_play)
def _on_play(self):
self.tree.change_scene(GameScene())
class GameScene(Node):
def ready(self):
# ... gameplay setup ...
self.player.died.connect(self._on_death)
def _on_death(self):
self.tree.change_scene(GameOverScreen(score=self.score))
change_scene(new_root) runs _exit_tree() on the old root and _ready_recursive() on the new one — exactly the same path as the initial root. Works from any node via self.tree.
Signals — decoupled events¶
Declare as class attributes; connect and emit on instances:
class Enemy(Node):
died = Signal() # no args
damaged = Signal(int) # typed: emits one int
hit_by = Signal(int, str) # typed: (amount, source)
# Connect
enemy.died.connect(self._on_enemy_died)
enemy.damaged.connect(lambda hp: self.show_damage(hp))
enemy.hit_by.connect(self._log_hit, once=True) # auto-disconnects after first fire
# Emit
enemy.died() # or enemy.died.emit()
enemy.damaged(42)
connect() returns a Connection handle; call .disconnect() on it, or pass the callback back to signal.disconnect(fn). once=True is a one-shot connection.
Typed signals — arity validation¶
When a signal declares types, connect() inspects the callback’s signature and warns if it can’t accept the emitted arguments:
health_changed = Signal(int, int) # old_hp, new_hp
def bad(hp): # only accepts one arg
print(hp)
health_changed.connect(bad) # logs: accepts at most 1 arg (signal emits 2)
The check fires at connect time, not emit time, so bugs surface without needing to run the emitting code path. Callables with *args are always accepted. Types themselves are advisory — nothing enforces runtime type checks on the emitted values.
Signal[int, str] bracket syntax works the same as Signal(int, str).
Input actions in ready()¶
Register input bindings inside the root node’s ready(), never at module scope:
class Game(Node):
def ready(self):
InputMap.add_action("jump", [Key.SPACE])
InputMap.add_action("move", [Key.A, Key.D, Key.LEFT, Key.RIGHT])
Reason: the web exporter skips the module’s if __name__ == "__main__" block and module-level statements, so InputMap.add_action(...) calls written there are silently dropped. Actions registered during ready() run regardless of entry point and work on desktop and web builds.
Query actions with Input.is_action_pressed("jump"), Input.is_action_just_pressed("jump"), Input.get_strength("jump") (0.0–1.0), or Input.get_vector("left", "right", "up", "down") for a normalised 2D direction.