This module implements a Global Event Bus (Signal Bus) pattern for Godot 4.x. It serves as a central nervous system for your game, allowing distant nodes (e.g., a Trap in a level and a HealthBar in the UI) to communicate without Topology Coupling or Location Dependency.
By using this module, you eliminate the need for brittle get_node() paths that span across scene boundaries, significantly reducing Change Amplification when refactoring scene hierarchies.
- Deep Module Design: Encapsulates the complexity of global state management behind the native, simple interface of Godot signals.
- Strict Typing: Rejects string-based event lookups in favor of first-class signals to prevent "Unknown Unknowns" (silent failures due to typos).
- Zero-Lazy-Code: Includes a built-in debug monitor to visualize signal traffic without requiring manual print statements.
To integrate this module into your Godot project, you must register it as an Autoload (Singleton). This ensures the bus is accessible from every script in your game.
- Save the Script:
Save the provided code as events.gd in a logical location, such as res://scripts/globals/events.gd. Note: file names must use snake_case. - Open Project Settings:
Go to Project -> Project Settings... in the top menu. - Navigate to Autoload:
Click the Globals tab (labeled Autoload in some versions). - Add the Script:
- Path: Click the folder icon and select your events.gd file.
- Node Name: Ensure the name is set to Events (PascalCase). This is the global variable name you will use in code.
- Global Variable: Ensure the "Enable" checkbox is checked.
- Click Add.
Unlike generic plugins that use loose strings, this strategic approach requires you to explicitly define your signals in events.gd. This serves as self-documenting code for your game's architecture.
Open events.gd and add signals at the top:
# events.gd
# Define your project-specific events here
signal player_health_changed(new_health: int, max_health: int)
signal item_collected(item_type: StringName)
signal level_completed
Any node can shout a message to the bus. The sender does not need to know who is listening.
# player.gd
func take_damage(amount: int) -> void:
health -= amount
# Strategic: The player doesn't know the UI exists. It just reports the data.
Events.player_health_changed.emit(health, max_health)
Any node can listen for messages. The receiver does not need to know where the sender is located.
# health_bar.gd
func _ready() -> void:
# Strategic: Connection is type-safe. If the signal name changes,
# the editor will throw an error (Preventing Unknown Unknowns).
Events.player_health_changed.connect(_update_display)
func _update_display(new_val: int, max_val: int) -> void:
value = new_val
The module includes a toggleable debug logger. When enabled, it prints all global signal emissions to the Output console with rich text formatting.
To enable/disable:
- Open events.gd.
- Change the const _VERBOSE_DEBUG to true or false.
Output Example:
[EventBus] Signal Emitted: player_health_changed | Payload: [80, 100]
[EventBus] Signal Emitted: item_collected | Payload: ["KeyCard"]
- Do not use for local communication: If a parent node needs to talk to its direct child, use get_node() or an @onready reference. If a child needs to talk to its direct parent, use a standard signal. Only use Events for nodes that are "strangers" or distant in the Scene Tree.
- Maintain Naming Conventions: Signals should use snake_case and past tense (e.g., door_opened, not DoorOpen) to clearly indicate an event that has already happened.
- Strict Types: Always strictly type your signal parameters in the events.gd definition to ensure autocomplete works correctly.