Signals: GDScript's Event System
Learn Godot's built-in signal system — declaring custom signals, connecting to built-in ones, and using signals to decouple nodes.
Introduction
Signals are Godot's built-in observer/event system — a way for one node to announce "something happened" without needing to know or care which other nodes, if any, are listening. They are one of the features that make GDScript feel purpose-built for games rather than a general-purpose language with game bindings bolted on.
- What a signal conceptually is, and the problem it solves.
- How to declare, emit, and connect to a custom signal.
- How to connect to signals nodes already provide, like a Button's pressed signal.
- Why signals keep nodes loosely coupled and easier to reuse.
What a Signal Actually Is
A signal is a named event a node can "emit," optionally carrying data along with it. Any number of other nodes (or none at all) can "connect" a function to that signal, and Godot calls every connected function automatically whenever the signal fires — the emitting node never needs a direct reference to whoever is listening.
Declaring and Emitting a Custom Signal
extends Node
signal health_changed(new_health: int)
var health: int = 100
func take_damage(amount: int) -> void: health -= amount health_changed.emit(health)signal health_changed(new_health: int) declares a signal named health_changed that carries one integer value. Calling health_changed.emit(health) fires it, immediately calling every connected function with health as the argument.
Connecting to a Signal in Code
extends Control
@onready var player = get_node("../Player")@onready var health_label: Label = $HealthLabel
func _ready() -> void: player.health_changed.connect(_on_health_changed)
func _on_health_changed(new_health: int) -> void: health_label.text = "HP: " + str(new_health)Click Run to see what this code prints.
Everything shown here as code can also be wired up visually in the Godot editor's Node panel under the "Signals" tab — many developers use both approaches depending on the situation.
Built-in Signals
Most built-in node types already expose useful signals — a Button emits pressed, a Timer emits timeout, an Area2D emits body_entered when something collides with it. Connecting to these works exactly the same way as connecting to a signal you declared yourself.
extends Button
func _ready() -> void: pressed.connect(_on_pressed)
func _on_pressed() -> void: print("Button was clicked!")Click Run to see what this code prints.
Why Signals Decouple Your Code
Without signals, a Player node that needs to update a health bar would need a direct reference to that specific UI node, and any code change to one risks breaking the other. With signals, the Player node just announces health_changed and moves on — it works identically whether zero UI elements are listening, or five different systems (a health bar, a screen-shake effect, an achievement tracker) are all connected at once.
Common Mistakes
- Connecting the same signal to the same function twice by accident, causing a handler to run multiple times per emission.
- Reaching directly into another node's internals ($OtherNode.some_property = x) when a signal would keep the two nodes properly decoupled.
- Forgetting that a signal's declared parameters (like new_health: int above) must match what connected functions actually expect.
Best Practices
- Reach for a signal whenever a node needs to communicate "something happened" to code outside its own subtree.
- Name signals as past-tense events (health_changed, enemy_defeated) rather than commands, to reflect that they announce something that already occurred.
- Keep signal-connected handler functions prefixed with _on_ (like _on_health_changed) — a strong, common Godot convention that makes their purpose obvious.
Frequently Asked Questions
Yes — declare multiple parameters in the signal, like signal item_collected(item_name: String, quantity: int), and pass them all when calling .emit().
Nothing — emitting a signal with no listeners is completely harmless and simply does nothing further.
Yes — conceptually they're the same pattern as event listeners in JavaScript, delegates in C#, or the observer pattern generally, just with first-class, built-in syntax in GDScript.
Key Takeaways
- A signal lets a node announce an event without knowing who, if anyone, is listening.
- signal name(params) declares one; .emit(args) fires it; .connect(function) subscribes to it.
- Most built-in nodes already expose useful signals, like Button.pressed or Timer.timeout.
- Signals keep nodes loosely coupled, making them easier to reuse and test independently.
Summary
Signals are what let a Godot project stay organized as it grows — nodes announce events instead of reaching directly into each other. Next, you'll cover classes and inheritance, including how to define your own reusable custom node types.