# 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). ```python 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 `on_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. ```python class TitleScreen(Node): def on_ready(self): self["PlayButton"].pressed.connect(self._on_play) def _on_play(self): self.tree.change_scene(GameScene()) class GameScene(Node): def on_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: ```python 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: ```python 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)`. ## Lifecycle hooks via decorators Lifecycle is normally driven by overriding `on_ready`, `on_process(dt)`, `on_physics_process(dt)`, `on_enter_tree()`, `on_exit_tree()`, `on_draw(renderer)`, `on_picked(event)`, `on_input(event)`, and `on_unhandled_input(event)`. Decorators register *additional* handlers under arbitrary method names so concerns can split without one monolithic override: ```python from simvx.core import Node, on_process, on_input, Key class Player(Node): def on_process(self, dt): # primary override: fires first self.position += self.velocity * dt @on_process # extra handler: fires after the override def update_animation(self, dt): self._frame += dt * self.fps @on_input(action="jump") # filter by InputMap action def jump(self, event): self.velocity.y = -300 return True # truthy return consumes the event @on_input(key=Key.S, ctrl=True) # filter by raw key + modifiers def save(self, event): self.tree.save_game() ``` Filters for `@on_input` (mutually exclusive: at most one per decoration; stack decorators for multiple bindings): | Filter | Type | Matches | |--------|------|---------| | `action=` | `str` | active `InputMap` action | | `key=` | `Key` or tuple of `Key` | keyboard key (any-of for tuples) | | `button=` | `MouseButton` | mouse button | | `motion=True` | flag | mouse motion events | | `scroll=True` | flag | scroll events | | `joy_button=` | `JoyButton` | gamepad button | | `joy_axis=` | `JoyAxis` | gamepad axis | State and modifier flags: `released=True` matches release events (default `False` matches presses); `ctrl`/`shift`/`alt`/`meta` are `True` (must be pressed), `False` (must not), or `None` (don't care). Modifier filters apply to `key`/`button`/`joy_button` only. Bare `@on_input` is the catch-all: fires for every event regardless of filter. Hooks are collected once in `Node.__init_subclass__`, so dispatch is a tuple iteration, not a name lookup. ## Typed signal bracket syntax `Signal(int, str)` and `Signal[int, str]` are equivalent: bracket form reads cleanly when types are imported: ```python from simvx.core import Signal class Inventory(Node): item_added = Signal[str, int] # (item_id, quantity) cleared = Signal() ``` Arity validation runs at `connect()` time against the callback's signature; mismatches log a warning. Types themselves are advisory. ## Input actions in `on_ready()` Register input bindings **inside the root node's `on_ready()`**, never at module scope: ```python class Game(Node): def on_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 `on_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.