Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Decoupled Communication with Godot Signals

Recommended resources

Introduction

Welcome back! In the last lesson, you wrote your first GDScript, giving your Goblin scene properties like health and a take_damage function. The script can successfully manage its own internal state, printing messages as the goblin's health changes.

However, a critical piece is missing. The Goblin script is currently an isolated island of logic. If another part of your game—say, a central GameManager—needs to know when the goblin is defeated, how would it find out? One way is for the GameManager to constantly check the goblin's health, which is inefficient. Another is for the goblin to hold a direct reference to the GameManager and call one of its methods, but this creates a rigid dependency, a pattern known as "tight coupling."

This lesson introduces Godot's elegant solution: the signal system. Your learning outcome is to use Godot's signal system to create decoupled communication between game objects (nodes). We'll explore:

  • The concept of signals as an implementation of the Observer design pattern.
  • Connecting to built-in signals from nodes like buttons.
  • Creating and emitting your own custom signals to announce important events.
  • The "call down, signal up" principle for writing clean, maintainable code.

For someone with your background in systems engineering and Java, you'll likely recognize this as a form of event-driven programming, similar to implementing ActionListener interfaces or using message queues.


The "Why": Decoupling with the Observer Pattern

At its core, Godot's signal system is a powerful implementation of the Observer design pattern. This pattern is all about letting objects (Observers) subscribe to and be notified of events happening in another object (the Subject) without the Subject needing to know anything about its Observers. This creates a "decoupled" architecture, where components can communicate without being directly dependent on one another.

To get a clear overview of this concept and how it relates to Godot, please watch the beginning of the following video.

Game Programming Patterns in Godot: The Observer Pattern (Godot Signals)

This video from GameDev w/ David provides a great explanation of the Observer pattern and how Godot signals are a practical application of it.

Watch from the beginning to 2:32. The video explains why decoupling is important and introduces the roles of 'Subject' and 'Observer' in this pattern.

The key problem that signals solve is the avoidance of tight coupling. To dig a little deeper into why this is so critical from a software design perspective, take a look at the following article.

GDScript Godot Signals In Depth Guide

This article, 'GDScript Godot Signals In Depth Guide' by Adrian Larion, clearly articulates the problems with direct communication between nodes.

Read the section titled 'Code Without GDScript Signals Is Bad'. This section explains the issues of tight coupling and low re-usability, which will be very familiar concepts from your backend development experience.

In summary, by using signals:

  • The emitter (Subject) doesn't need to know who is listening, or if anyone is listening at all.
  • The listener (Observer) doesn't need to be known by the emitter.
  • This makes your scenes and scripts more reusable and easier to maintain.

Using Built-in Signals: A Hands-On Example

Let's build a small test scene to see signals in action. We'll create a simple UI with a button that deals damage to our Goblin when pressed.

  1. Create a new scene. Add a Node2D as the root and name it SignalTest.
  2. Add a Button node as a child of SignalTest. In the Inspector, set its Text property to "Damage Goblin". Resize and position it as you like.
  3. Instantiate your goblin.tscn scene as another child of SignalTest.
  4. Attach a new script to the SignalTest root node and save it as signal_test.gd.

Your scene tree should look like this:

- SignalTest (Node2D, with signal_test.gd)
  - Button
  - Goblin (Instance of goblin.tscn)

Now, we want to connect the button's "press" event to a function.

Connecting a Signal in the Editor

Godot's editor makes this very straightforward.

  1. Select the Button node in the Scene dock.
  2. In the Inspector panel on the right, click the Node tab (next to Inspector).
  3. You'll see a list of signals for the Button node. The one we want is pressed().
  4. Double-click the pressed() signal. A dialog window will appear.
Godot Signal Connection Dialog
The 'Connect a Signal to a Method' dialog lets you visually wire up nodes.
  1. In this window, select the SignalTest node in the list on the left. This is the node that will listen for the signal.
  2. The Receiver Method will be auto-filled to _on_button_pressed. This is the function that will be called in signal_test.gd when the signal is emitted. Leave it as is.
  3. Click Connect.

The script editor for signal_test.gd will open, and Godot will have automatically added the new function for you:




# signal_test.gd
extends Node2D

func _on_button_pressed():
	pass # Replace with function body.

Now, let's make this function do something. We want it to call the take_damage function on our Goblin node.




# signal_test.gd
extends Node2D




# This function is connected to the Button's 'pressed' signal.
func _on_button_pressed():



	# Get a reference to the Goblin node and call its method.
	$Goblin.take_damage(25)
	print("Damage signal sent to Goblin.")

Run this scene (press F6). Every time you click the button, you should see the goblin's health decrease in the output log. We have successfully used a built-in signal to trigger an action!


Creating Custom Signals

While built-in signals are useful, the real power comes from creating your own. Let's modify our Goblin script to announce when it has been defeated.

1. Declaring the Signal

Open your goblin.gd script. At the top, under extends Node2D, declare your new signal using the signal keyword. We'll also have it pass the goblin's name as an argument.




# goblin.gd
extends Node2D

signal defeated(card_name_arg)

var health := 100



# ... rest of your variables

2. Emitting the Signal

Now, find your take_damage function. Inside the if health <= 0 block, after you've set the health to 0, emit the signal. In Godot 4, you call the emit() method directly on the signal object.




# goblin.gd
func take_damage(amount: int):
	health -= amount
	print(card_name + " takes " + str(amount) + " damage.")
	
	if health <= 0:
		health = 0 # Prevent health from going negative
		defeated.emit(card_name) # <-- EMIT THE SIGNAL HERE
		print(card_name + " has been defeated!")
	
	print("Current health: " + str(health))

The Goblin script is now a proper "Subject". It announces a key event but has no idea who, if anyone, is listening.

3. Connecting to the Custom Signal via Code

Let's make our SignalTest script listen for this new defeated signal. While you could connect this in the editor just like before, connecting via code is often more flexible, especially for nodes created dynamically.

We'll connect the signal in the _ready function of signal_test.gd.




# signal_test.gd
extends Node2D

func _ready():



	# Connect our custom function to the Goblin's 'defeated' signal.
	$Goblin.defeated.connect(_on_goblin_defeated)




# This function is connected to the Button's 'pressed' signal.
func _on_button_pressed():



	# Get a reference to the Goblin node and call its method.
	$Goblin.take_damage(40) # Increased damage to reach 0 faster
	print("Damage signal sent to Goblin.")




# This is the function that will run when the 'defeated' signal is received.
func _on_goblin_defeated(name_of_defeated_goblin: String):
	print("SignalTest script received news: " + name_of_defeated_goblin + " was defeated!")



	# We can now disable the button since the goblin is gone.
	$Button.disabled = true

Let's break down the connection line:
$Goblin.defeated.connect(_on_goblin_defeated)

  • $Goblin: The node that emits the signal (the Subject).
  • .defeated: The signal we want to listen to.
  • .connect(): The method to establish the connection.
  • _on_goblin_defeated: A Callable which is the function on self (the SignalTest script) that will be called (the Observer's method).

Now, run the scene with F6. Click the button three times. On the third click, the goblin's health will drop to zero, it will emit the defeated signal, and your SignalTest script will react by printing a message and disabling the button.


Best Practice: Call Down, Signal Up

A fundamental principle for organizing Godot projects is "call down, signal up".

  • Call Down: A parent node can safely get a reference to its children ($ChildNode) and call their functions directly. This is acceptable because a parent is expected to manage its children.
  • Signal Up: A child node should never know about its parent. If it needs to communicate a change or event, it should emit a signal. The parent (or any other interested node) can then choose to listen to that signal.

This one-way flow of dependencies keeps your scenes modular and reusable. A child scene doesn't break if you place it under a different parent, because it never assumes a specific parent exists.

The following video explains this principle well.

Game Programming Patterns in Godot: The Observer Pattern (Godot Signals)

Let's return to the GameDev w/ David video, which has an excellent section on this design philosophy.

Watch from 7:08 to 8:27. This segment visualizes and explains the 'call down, signal up' pattern.

This diagram is a great mental model for this communication flow:

Godot Node Communication: Signals vs. Direct Connections
This diagram shows direct calls (green) flowing down the scene tree from parent to child, while signals (red) flow up from child to parent, enabling decoupled communication.

Conclusion

In this lesson, you've mastered one of Godot's most important and powerful features. You've gone beyond simple, self-contained scripts and learned how to build a flexible communication network between your game objects.

Key Takeaways:

  • Decoupled Communication: Signals, an implementation of the Observer pattern, allow nodes to communicate without direct dependencies, leading to more modular and maintainable code.
  • Connecting Signals: You can connect both built-in and custom signals either visually through the Node tab in the editor or programmatically using the connect() method.
  • Custom Signals: You can define your own signals with the signal keyword and broadcast them with emit(), passing along any relevant data as arguments.
  • Design Principle: Always strive to follow the "call down, signal up" pattern. This ensures your individual scenes remain self-contained and reusable.

Preview of the Next Lesson:
Our SignalTest scene uses a Goblin that is already part of the scene from the start. But in our card game, we'll need to create creatures and other effects on the fly. In the next lesson, we will learn how to create and instantiate scenes programmatically, allowing us to spawn new game elements whenever we need them. Signals will be crucial for managing the lifecycle of these dynamically created objects.