Overview
As you progress with Godot development, nodes need to communicate with each other. Scenarios like "when the player touches a coin, the coin disappears and the UI score increases." Beginners tend to directly reference coin or UI nodes from the player's script (using get_node() or $).
# Bad example: Player directly knows about UI and sound manager
func _on_area_entered(area):
if area.is_in_group("enemy_attack"):
get_node("/root/Game/UI/HealthBar").update_health(health)
get_node("/root/Game/SoundManager").play_sfx("player_hurt")
This tight coupling design is extremely fragile to scene structure changes, prevents node reuse, and makes debugging difficult. Signals are Godot's fundamental mechanism for solving this problem and keeping nodes "loosely coupled."
What Are Signals? Broadcasting "An Event Happened!"
A signal is a mechanism for one node to broadcast to other nodes that "a specific event occurred." The signal sender (Emitter) doesn't need to know who's listening. It simply shouts "The player took damage!"
Meanwhile, nodes interested in that broadcast (Receivers) subscribe (Connect) to specific signals. When the signal is emitted, their designated method (Callback) is automatically called.
This broadcaster-listener relationship allows Emitters and Receivers to collaborate without directly knowing each other. This is the fundamental principle of good software design known as "separation of concerns."
How to Use Signals
There are two main ways to connect signals: "via editor" and "via code."
1. Editor Connection (Easiest)
The most visual and straightforward method. Let's see an example of executing processing when a Button is pressed.
- Select the signal-emitting node: Select the
Buttonnode in the scene tree. - Open the "Node" tab: Open the "Node" tab next to the inspector, then select the "Signals" tab. You'll see a list of built-in signals like
pressed()thatButtonhas. - Connect the signal: Double-click the
pressed()signal (or select it and click "Connect"). - Select receiver and method: The "Connect Signal" dialog opens. Select the node to receive the signal and determine the method name to call. Click "Connect" to complete.
This automatically generates a method with the specified name in the receiver node's script.
# Auto-generated in receiver's script
func _on_button_pressed():
print("Button was pressed!")
# Write processing you want to execute here
2. Code (GDScript) Connection
There are many situations requiring code connections, like when dynamically generating nodes. Use the connect() method.
# Player.gd
func _ready():
var hud = get_node("/root/Game/UI/HUD")
health_changed.connect(hud._on_player_health_changed)
In Godot 4, using Callable allows the editor to detect errors when methods don't exist, making it safer.
Practice: Linking Components with Custom Signals
The key to signals' true value is defining your own custom signals beyond built-in ones. Let's refactor a scenario where the player takes damage.
Player.gd (Emitter side)
Declare custom signals with the signal keyword and emit them with emit() when health changes.
extends CharacterBody2D
# Signal to notify health changed
signal health_changed(current_health, max_health)
# Signal to notify death
signal died
@export var max_health: int = 100
var current_health: int
func _ready():
current_health = max_health
func take_damage(amount: int):
current_health = max(0, current_health - amount)
health_changed.emit(current_health, max_health)
if current_health <= 0:
died.emit()
queue_free()
HUD.gd (Receiver side)
Subscribe to the player's health_changed signal and update UI display.
extends CanvasLayer
@onready var health_bar: TextureProgressBar = $HealthBar
func _on_player_health_changed(current_health, max_health):
health_bar.max_value = max_health
health_bar.value = current_health
GameManager.gd (Another Receiver)
Subscribe to the player's died signal and handle game over processing.
extends Node
func _on_player_died():
print("Player died. Transitioning to game over screen.")
get_tree().change_scene_to_file("res://game_over_screen.tscn")
This way, Player just broadcasts its state changes, while UI and game manager listen and fulfill their own roles.
Common Mistakes and Best Practices
| Common Mistake | Best Practice |
|---|---|
| Connecting everything with signals | Be mindful of separation of concerns. For closely related cases like parents directly controlling children, calling methods directly is often more natural. |
| Forgetting to disconnect signals | When the Emitter is deleted with queue_free(), disconnection is automatic. However, manual disconnect() is needed when the Receiver is deleted first or when dynamically disconnecting (see code example below). |
| Passing node references as arguments | Pass only data like IDs, numbers, or strings. If receivers need node references, have them search by ID to maintain loose coupling. |
| Signal name typos | Using Godot 4's Callable or await syntax detects errors at compile time, preventing typos. |
| Using for global events | For game-wide events (e.g., game pause), the "event bus" pattern with signals on a singleton (Autoload) is easier to manage. |
Signal Disconnection (disconnect) Example
# Example of dynamically connecting/disconnecting signals
var callback: Callable
func _ready():
callback = _on_player_health_changed
player.health_changed.connect(callback)
func _exit_tree():
# Disconnect before node is removed from scene tree
if player and player.health_changed.is_connected(callback):
player.health_changed.disconnect(callback)
func _on_player_health_changed(current_health, max_health):
# Processing...
pass
Performance and Alternative Patterns
-
Performance: Signal calls have slight overhead compared to direct function calls. This is because it loops through a list of
Callables internally, but this difference rarely becomes a bottleneck in most games. The benefits of design clarity and improved maintainability far outweigh this minor cost. -
Alternative Pattern (Event Bus): Centralizing signals in a singleton (implemented via Autoload) to function as a global "event bus" is also powerful. This enables sending and receiving events from anywhere in the scene tree, allowing communication between completely independent systems.
Summary
Signals are the cleanest and most recommended way for inter-node communication in Godot.
- Sender: Just shouts "an event happened." Doesn't care who's listening.
- Receiver: Registers for signals they want to hear, and when called, just does their job.
By designing components with this loose coupling in mind, you increase scene reusability, reduce bugs, and build projects resilient to changes. When you find yourself writing code that directly references parents or siblings with get_node(), make it a habit to stop and think "Could this be achieved with signals?" That's the first step to becoming a Godot master.