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.

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! 🚀