Omar Bazzi Game Dev
(Global) Event Bus in Godot

(Global) Event Bus in Godot

Design Patterns programming Godot

Those of us who program video games know that one of the most intricate things in art is the communication between objects and/or systems, and that the holy grail is to achieve the famous “decoupled code”. In this fight, one of our main allies is the observer design pattern, a behavioral pattern that allows us to send messages from an object and only those who are subscribed to receive that message do so and behave as they wish in response to this “event”. If there are 0 subscriber objects in the scene, nothing happens (the message is sent, no one receives it, and no problem), and if there are subscribers, but the sender is not there either (the message is never sent, everything remains the same). Many dependency/coupling problems are avoided.

However, this pattern, just like that, has a drawback: we need to explicitly “connect” the sender with the receiver/s. This may not be a problem until we find ourselves in the scenario of what do I do if I need to connect 2 nodes that are “far” from each other in the hierarchy of the scene (here I am talking specifically about Godot and its jargon, in each engine with its language it will have its issues).

A good solution to this problem is to extend this pattern so that everything goes through an intermediary, a “bus”. That is, the events will be defined in this bus, which will be an Autoload/Singleton, so that any object can access the class and tell it to emit its event, just as any other object can access the class and subscribe to an event. Something like Class EventBus —> Where all the events are defined Class A —> accesses EventBus to emit an event X Class B —> accesses EventBus to subscribe to an event X

A and B do not know each other, EventBus does not know A or B, but both can emit and subscribe to signals in such a way that they are “communicated” with each other.

Let’s see in practice:

#event_bus.gd

signal coin_consumed(value: int)
#class_a.gd

func foo() -> void:
EventBus.emit.coin_consumed(100)
#class_b.gd

func _ready() -> void:
EventBus.connect.coin_consumed(_on_coin_consumed)

func _on_coin_consumed(value: int) -> void:
print("coin consumed")
print(value)
print("##############")

So simple that it seems hard to believe, when implemented, all the power it gives.

Now then… Is it perfect? ​​Of course not, like all things, there is a trade-off.

The first and most obvious problem is that we quickly lose the mental record of “what is connected to what”, this can lead to situations where we even introduce bugs because we emit the signal connected to the bus before the object with the class that subscribes to it exists. Another problem, inherent to the Godot signal system, is that we are quite “unprotected” regarding the passing of signal arguments. In the example I gave you, you can try emitting the signal without a value, or connecting it to the target method without declaring it with the corresponding parameter.

Lastly (and perhaps the worst) is that it is a very good pattern to turn into a “golden hammer” and use in situations that are completely meaningless (such as, for example, to send a signal from a node B to a node A where B is a hierarchical child of said A, it has happened to me).

The only partially controllable one is the 2nd one, here the code is slightly modified to at least be able to make sure that we don’t fail in the arguments that we send (in the declaration of the target method we remain the same)

#event_bus.gd

signal coin_consumed(value: int)

func emit_coin_consumed(value: int) -> void:
coin_consumed.emit(value)
#class_a.gd

func foo() -> void:
EventBus.emit_coin_consumed(100)
#class_b.gd

func _ready() -> void:
EventBus.connect.coin_consumed(_on_coin_consumed)

func _on_coin_consumed(value: int) -> void:
print("coin consumed") 
print(value)
print("#############")

As you can see, what we do is create an event inside the bus that is the one that actually emits the signal. We have to pass the corresponding argument to this event, which reduces the margin of error a little.

This same pattern is easily replicable in engines like Unity and Unreal (well, not so much the latter, you have to surf the Unreal C++ API syntax, but you can). Conceptually, you do exactly the same thing, an intermediary Singleton script that contains the signals and, on the other hand, scripts that access to emit and others that access to subscribe, and in the latter you have to declare/implement and connect to the target methods.

As always and with all design patterns: none is perfect, all must be implemented responding to a need (and not force the pattern just because) and in everything there is, at least, a trade-off.