Godot Signals Architecture

To make our game elements communicate with each other, like updating an health bar when the player gets hit (and almost any other interaction), we can have a look at a signaling/events system.

This article contains a very short introduction about them, and most importantly some suggested practices to keep everything more stable later in development (or at least: how we’re trying to achieve it).

0:00
/0:09

An example use case we made in Godot. (Art pack by Kenney - Tiny Dungeon)

Quick Overview

We can think of Signals as a way for nodes to communicate with each other. In this article we’re referring to Godot, but keep in mind that the logic is 1:1 with Unity as well (we use both! <3).

One node/scene element would be responsible for telling to "anyone listening" that something specific has just occurred (e.g. "I've been clicked!), and others (if any) would react based on that message.

0:00
/0:04

a button sending a signal to a text label

This is one of the patterns we use the most in any of our games or projects (even outside of UI), to make game systems communicate with each other independently and not worry too much about dependencies. (Read more about the Observers Pattern here.)

Built-in Signals

Many Godot Nodes have built-in signals already available for their specific behavior/events. For example, a Button can tell everyone it has been pressed, focussed and many other things (read more here), so you can use them directly without having to worry about the implementation.

Custom Signals

That said, you can also create signals from scratch super easily, as quick as declaring "signal signal_name" in your script. (You can read more about custom signals on Godot's docs.)

Suggested Workflow

There are two ways to connect signals with external functions/methods in your game, but even if using the Editor is the faster one, we highly suggest connecting them via code specifically - or later in development you might end up not knowing who is connected to what element, and risk of breaking things more easily.

Connecting Signals Via Code

To link signals and methods together via code, you can do something like:

# declaring a new signal in your script
signal custom_signal

func somewhere_in_your_code() -> void:

	# invoking/emitting the signal for anyone who's listening
	custom_signal.emit()
	
# that's it.. really!

and to listen:

@export var target_node # the node we're listening, declared above

func _ready() -> void:
	# connecting a method
	target_node.custom_signal.connect(_on_triggered)
	
# the method you want to fire when the signal is emitted
func _on_triggered():
	print('the signal has been triggered!')
	
# remember to disconnect signal
func _on_exit_tree():
	target_node.custom_signal.disconnect(_on_triggered)

Keeping things independent

The nicest thing about signals (or events, in general) is that they allow you to keep elements independent from each other. This comes really handy in our UI tutorial series (part 1 here: Core UI Concepts in Godot), for example by updating the player's health during gameplay.

# inside Player's class
signal on_health_changed(health : float)

# UI health-bar pseudo code

func _ready() -> void:
	player.on_health_changed.connect(_update_health_bar)
	_update_health_bar(player.health)
	
func _update_health_bar(health : float) -> void:
	health_text.text = str(health)

Instead of having two objects strictly connected with each other (like a “player” script requiring a UI reference to be able to communicate their HP changes), we can simply “hook” different signals and methods together based on our need, and still be able to disconnect them to test elements in isolation.

In this case, we can test the player without the need of having the whole UI loaded in our game, and we can also test our UI by using a “fake player” class and change its health based on our test scenarios.

Testing Missing Signals

Going from here, you could check (before building) if your game contains missing signals references and reduce even more the risk of bugs. If a Node (e.g. UI) needs to listen at something exposed in the inspector, you can check if its reference is null and fix it right away.

We have created an example script that does this and use it in Unit Testing before building our games - you can find it at the end of this article.

Expanding Further

That said, the main issue you might have when following this direction (even if you’re managing everything via code, and probably have a better overview than in Editor) is when you’ll need to connect two or more elements far away from each other (either in structure, on the same “child level”, or nested nodes/scenes), which would make it really hard to track things anyways.

You could definitely look at every node’s child, and “subscribe” to the ones you’re interested to (e.g. if they have a specific type, or belong to specific groups), but it might not scale at all anyways.

Events “Bus”

A possible solution would be creating an “Events bus” (or multiple of them).

You forward everything to “someone in the middle”, that could be available locally or globally for everyone (shared resources or a singleton), and other objects can reference it to retrieve information without needing extra dependencies (like an entire Player script etc.).

It could look like this:

class_name EventsBus

signal on_hp_changed

And then subscribing like:

func something() -> void:
	EventsBus.on_hp_changed.connect(...)
	
	#etc

The main issue here (they never stop…) is that you don’t really know the player’s health until it changes for the first time, so you can really use this method only for doing things when elements change, but not to retrieve their initial information.

Shared Variables

Another concept we really like and that can fix this issue, although it requires some extra care in its initial setup/planning (and later tests), are shared variables.

They follow the same logic of events buses, but instead of forwarding everything via “events”, you share the same variables instead between “setters” (e.g. a player) and “listeners” (e.g. an health bar), which are instead independent and contain a “on_changed” signal to communicate independently with other objects.

This way you can have any element present in the scene in isolation, and test them without the need of extra dependencies/scripts loaded.

An example setup via code:

class_name SharedFloat

signal on_changed_value(value : float)

var _value: float

var value : float:
	get:
		return _value
	set(new_val):
		if _value == new_val:
			return
		_value = new_val
		
		on_changed_value.emit(_value)

Then your player could do something like:

# player.gd

@export var health : SharedFloat

func _ready() -> void:
	health.value = 5

and the listeners:

# ui.gd

@export var player_health : SharedFloat

func _ready() -> void:
	player_health.on_changed_value.connect(_update_health_bar)
	
func _update_health_bar(new_health:float) -> void:
	# stuff here
	pass

The main issue here is that it becomes really easy to have dozens of objects/resources floating around your assets folder (what if you have 10s of NPCs with different stats?), and it doesn’t scale as much with procedural/runtime content anyways (you might need direct references via script anyways, at that point), so… use it as you see fit.

You’d also need to care about referencing them properly (via code?) and name then properly, or you’d end up with the same problem as connecting signals with the Editor: loosing track of who’s connected to what, in the long run.

In Conclusion

No game is the same and unfortunately this architecture-mess never ends, but these are the strategies/architecture we like and use the most in our project!

There is another, more complex step that you could follow (”shared variables” that could be bound to an object or external, chosen via the editor directly), and I suggest having a look at this video in case you want to explore more: The Last Night - Game Architecture Presentation (although it’s for Unity, but the concepts apply 1:1).

That said, please connect signals via code! 🥺

I really hope you liked this post, and looking forward for our next ones (maybe extending signals even more) and also continuing our UI series. Thanks so much for reading through here and hear you soon on the next one!


Resources for Members

  • Godot script to check if all signals are connected properly in Editor

Oh hey - the extra resources (and downloadable content) of this post are available after you login (for free)! No shenanigans, we just need to make sure humans are accessing them (we spend many hours producing these posts in general). Please consider supporting us by creating a free account, it only takes a few seconds from the box below. Thanks so much!! ❤️

Febucci

The rest of this post is available for free!

You can unlock this content after creating an account; it helps us stay independent and keep posting what we love. Thanks! ❤️

Never miss any new post, from games, tools and what we notice in the industry in general
Our Unity, Godot, Game Art and Design learning notes
Help us reach you even if social medias change algorithms, die, AI takes over and suddenly no one knows what's real anymore..
Already have an account? Sign in

Deep dive

Almost five years ago I released a plugin called “Text Animator for Unity”, a tool I made/needed for my own games - and Today it is also getting used in other games like “Dredge, Cult of The Lamb”, “Slime Rancher 2” and many more!! I’d love if you could check it out! you’d also support us with this blog and new assets! ✨

Product callout image

Thanks so much for reading!