Messaging between components

Problem to solve : Decouple messaging between godot components

Godot signals provide us a good way for components to communicate, but creates coupling between them, and for dynamic created components could be more complicated.

One way to get rid of this coupling is the singleton pattern where we declare all the signals there, and then we subscribe / emit them. This works for small projects but for bigger projects the file starts to get bigger and bigger, you can split into several files but then you have to autoload each one of them.

Here enters the observer pattern

Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.

This allows us to separate the mechanism for passing around events from the content of the events themselves scaling better.

Let's first define a base class for our events

extends Object
class_name Event;
 
var event_id;
 
func _init(id) :
	self.event_id = id

And we can create our custom events that inherit from this class, we can create a FireEvent for example

extends Event
class_name FireEvent
 
const ID = "My_Fire_Event"
var fire_damage: int;
 
func _init(damage: int) :
	super._init(ID)
	self.fire_damage = damage;

const ID = "My_Fire_Event" is a constant that we will use to identify this event

var fire_damage: int; is the data that we want to pass between components

Let's create the event bus that will handle the subscription and emit of the events

extends Node
 
var _events_subscriptions = Dictionary()
 
func subscribe(event_id: StringName, subscriber: Object, function_name: String) -> void:
	pass
 
func unsubscribe(event_id: StringName, subscriber: Object, function_name: StringName) -> void:
	pass
 
func broadcast(event: Event) -> void :
	pass
 
class SubscriberDelegate:
	var method_name: StringName;
	var target: Object;
 
	func _init(target, method):
		self.method_name = method
		self.target = target
 
	func is_equal(event_delegate: SubscriberDelegate) -> bool :
		return event_delegate.method_name == self.method_name && event_delegate.target == self.target;
 
	func is_equal_by_values(subscriber: Object, function_name: StringName) -> bool :
		return function_name == self.method_name && subscriber == self.target;
 
	func fire(event: Event):
		target.call(method_name, event)

_events_subscriptions is a dictionary that will hold the event id and the subscribers for that event

subscribe will add a subscriber to an event, the function_name is the method that will be called when the event is broadcasted

unsubscribe will remove a subscriber from an event

broadcast will emit the event to all subscribers

Implementing the subscribe method

func subscribe(event_id: StringName, subscriber: Object, function_name: String) -> void:
	if not subscriber.has_method(function_name):
		return;
 
	var subscriber_delegate = SubscriberDelegate.new(subscriber, function_name)
 
	if not event_id in _events_subscriptions:
		var subscriptions : Array[SubscriberDelegate]  = [subscriber_delegate];
		_events_subscriptions[event_id] = subscriptions
	else:
		var subscriptions : Array[SubscriberDelegate]  = _events_subscriptions[event_id]
		for existing_sub : SubscriberDelegate in subscriptions:
			if subscriber_delegate.is_equal(existing_sub):
				return
		subscriptions.append(subscriber_delegate)
		_events_subscriptions[event_id] = subscriptions

We check if the subscriber has the method that we want to call, if not we ignore.

If there is no event id in the dictionary, we create a new array with the subscriber delegate and add to the dictionary,

if there is we check if the subscriber is already subscribed to the event, if not we add it to the array.

For the unsubscribe method

func unsubscribe(event_id: StringName, subscriber: Object, function_name: StringName) -> void:
	if not event_id in _events_subscriptions:
		return
 
	var subscriptions: Array[SubscriberDelegate] = _events_subscriptions[event_id]
	var index_to_remove = -1;
	for n in subscriptions.size():
		var event_delegate: SubscriberDelegate = subscriptions[n];
		if (event_delegate.is_equal_by_values(subscriber, function_name)):
			index_to_remove = n;
			break;
	if (index_to_remove > -1):
		subscriptions.remove_at(index_to_remove);
		_events_subscriptions[event_id] = subscriptions

We check if the event id is in the dictionary, if not we ignore. Iterate over the subscribers array and check if the subscriber is subscribed to the event, if it is we remove it from the array.

For the broadcast method

func broadcast(event: Event) -> void :
	var event_id = event.event_id;
	if not event_id in _events_subscriptions:
		return;
 
	if event_id in _events_subscriptions:
		var subscriptions : Array[SubscriberDelegate] = _events_subscriptions[event_id]
		for subscriber in subscriptions:
			if not is_instance_valid(subscriber.target):
				_remove_invalid_subscriber(event_id, subscriptions, subscriber)
				continue
			subscriber.fire(event)
 
func _remove_invalid_subscriber(event_id, subscribers_array: Array, subscriber_to_remove: SubscriberDelegate):
	var index_to_remove = subscribers_array.find(subscriber_to_remove)
	if index_to_remove >= 0:
		subscribers_array.remove_at(index_to_remove)
	_events_subscriptions[event_id] = subscribers_array

We check if the event id is in the dictionary, if not we ignore. Iterate over the subscribers array and check if the subscriber is still valid, if not we remove it from the array, if is valid we call the fire method to the subscriber.

We need to add the event bus on the autoload settings, so we can access it from any script Let's call it EventBus and add the path to the script.

Godot Autoload



To subscribe to our Fire Event we can do the following

EventBus.subscribe(FireEvent.ID, self, "name_of_method_to_be_called");
 
func name_of_method_to_be_called(event: FireEvent):
	print("Damage taken : "  + str(event.fire_damage))

To unsubscribe

EventBus.unsubscribe(FireEvent.ID, self, "name_of_method_to_be_called");

To fire the event

EventBus.broadcast(FireEvent.new(10))



This is a simple implementation of an event bus, you can add more features like priorities, async events, etc.


Hope this helps on your journey. Happy coding! 🚀