【Godot】Node Communication with Signals

Created: 2025-12-06Last updated: 2025-12-16

Learn Godot's signal feature from basics to practical applications and best practices, enabling loosely coupled component design while avoiding direct references.

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.

  1. Select the signal-emitting node: Select the Button node in the scene tree.
  2. 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() that Button has.
  3. Connect the signal: Double-click the pressed() signal (or select it and click "Connect").
  4. 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 MistakeBest Practice
Connecting everything with signalsBe mindful of separation of concerns. For closely related cases like parents directly controlling children, calling methods directly is often more natural.
Forgetting to disconnect signalsWhen 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 argumentsPass only data like IDs, numbers, or strings. If receivers need node references, have them search by ID to maintain loose coupling.
Signal name typosUsing Godot 4's Callable or await syntax detects errors at compile time, preventing typos.
Using for global eventsFor 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.