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.