Riding the Event Bus in Godot
Tags: godot, gamedev, opensource, designpatterns
Published: 2024-02-11 22:01:56

One of the most popular pieces of advice when working in Godot is to:  

> "Call down and signal up"

It's catchy and helpful and _exactly_ what you should be thinking about when communicating between nodes that live close to one another. Like parent-child nodes or even sibling nodes.

But what if I told you there was a secret(?), **third option**?
<br>


## I'm listening
Do you know what **Events** are? Or the **Observer Pattern**?

If you do, great! You might not need the rest of this. But you might still want a ready-to-go **Event Bus** implementation for Godot, which you can [find on my Github](https://github.com/BajaTheFrog/godot-event-bus).

If you _don't know_ or _aren't sure_ you need it in Godot, then read on!

---

There are a lot of good resources you can find on the topic of an **[Event Bus](https://en.wikipedia.org/wiki/Event-driven_architecture)** or the **[Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern#:~:text=The%20observer%20pattern%20addresses%20the,can%20notify%20multiple%20other%20objects.)**. I've linked some at the bottom of the article. 

**But the short version is**: Events follow an observer or publisher/subscriber structure that allow one entity in the game to tell anyone who cares that something just happened. 

Something changes with one thing and everyone who wants to know about it is automatically updated. 

This pattern is _great_ for reducing brittle, overly-coupled code. 

**Signals** help a lot with this actually at the "local" level, but on their own, they still require directly connecting from one node to another. 

This doesn't scale well as lots of things enter the fray or you want information to cross boundaries of systems.
<br>

## So what's the issue?
As more and more nodes and scenes enter your tree, you are asking for a headache if you are...

1. Directly calling down to some arbitrary scene 
2. Connecting `signals` between dynamically added scenes and lots of possible systems

<br>

### A classic example
You are making a game. 
There is a `hero`. 
There is combat. 

You need the UI to display a health bar that communicates how much health the `hero` has left. 
<br>

#### Attempt 1: Call down
The health bar UI has to get, or be given, a reference to the `hero` and then check every frame to see what their health is. 

```gdscript
# ui.gd

func _process(delta):
    # how do we get the hero to begin with?
    var latest_health = hero.health.value 
    _update_bar(latest_health)
```

This is calling `_update_bar` more frequently than we need AND it makes it harder to build and debug the UI on its own because it _requires_ a `hero` to work. No good.
<br>


#### Attempt 2: Signal up
The health bar UI has to get, or be given, a reference to the `hero` and connects itself to a `signal` about health changes. This is an improvement as we no longer are checking every frame but the fact remains we still have to rely on a `hero` reference.

```gdscript
# hero.gd

signal health_changed(latest_health)


func take_damage(dmg):
    health.value -= dmg
    emit_signal("health_changed", health.value)
```


```gdscript
# ui.gd

func _ready():
    # Again, how do we get the hero to begin with?
    hero.connect("health_changed", self, "_on_health_changed")


func _on_health_changed(latest_health):
    _update_bar(latest_health)
``` 

And if we flipped the dependencies we would have the same problem where the `hero` would demand some access to the UI.

So in either case - the UI and `hero` need a direct reference to one or the other. 

Not the end of the world on its own but as things grow and scale you'll suddenly find lots of systems needs lots of direct references to each other and you will question your life (trust me). 
<br>

#### Attempt 3: Using simple, signal-based events
You can actually make a huge improvement with very little work by just using an `Events` autoload (so that it's globally accessible) that has a bunch of `signals` defined. 

> This is what I did for my game [JOUNCER PX](https://bajathefrog.itch.io/jouncer-px) and while it started to feel precarious towards the end, the game was ultimately small enough to make it work.

Here's the script `events.gd` that we'll make an autoload.
```gdscript
# events.gd
extends Node

signal hero_health_changed(new_value)
```

Now, we can _broadcast_ this change from the player like so:
```gdscript
# hero.gd

func take_damage(dmg):
    health.value -= dmg
    Events.emit_signal("hero_health_changed", health.value)
```

And _subscribe_ to that change in the UI:
```gdscript
# ui.gd

func _ready():
    Events.connect("hero_health_changed", self, "_on_hero_health_changed")


func _on_hero_health_changed(new_value):
    _update_bar(new_value)
```

See that? Now the UI never needs a direct reference to the `hero`. It just needs to hook up to the `Event` it cares about and doesn't need to know _where_ it was triggered from. 
<br>

#### Attempt 4: Using an `EventBus`
By using an `EventBus`, a `hero` can broadcast an `Event` much like they would trigger a `signal`. 

But the advantage of an `EventBus` is you can more easily pass around `Events` as more complex objects and are not constrained to defining every possible `Event` in one file (`events.gd`).   

Let's take a look:
```gdscript
# hero.gd

func take_damage(dmg):
    health.value -= dmg
    var health_event = HealthEvent.new(health.value)
    EventBus.service().broadcast(event)


# Defining our custom Hero.HealthEvent
class HealthEvent extends Event
    var new_health: float
    # you could include other information like 
    # the previous health or health as a %

    const ID = "HERO_HEALTH_EVENT_ID"

    # this passes in ID to the base `Event` _init
    func _init(new_value: float).(ID):
        self.new_health = new_value

```

And then on the UI side of things:
```gdscript
# ui.gd

func _ready():
    EventBus.service().subscribe(Hero.HealthEvent.ID,
            self, 
            "_on_hero_health_event")


func _on_hero_health_event(event: Event):
    var player_health_event = event as Hero.HealthEvent
    if player_health_event:
        print(str(player_health_event.new_value))
```

This gives us the flexibility to separate the _mechanism_ for passing around events from the _content_ of the events themselves. This scales better and allows you to define events closer to the source (such as in the same file as the event "owner"). 
<br>

### In both cases...
![A hyper-realistic rendering of how events can pass information without breaking boundaries between systems](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apyb9miu9bfe6opowmi2.png)
Using either events as `signals` or the via the  `EventBus` removes the need for direct references or  complicated dependency injection and opens up the possibility of simulating events through hotkeys and debug tools. 

As an example - if you want to test the UI a great way to do it is a hotkey that simulates a player health event rather than requiring your player to go out and take damage!

And it's just as easy for an arbitrary number of other nodes and systems to listen to health events too. 

- Want an enemy that gets stronger when the `hero` is low on health? 
- A chest that only unlocks when `hero` health is full?
- Want an achievement manager that counts how many times the `hero` dies?

All easily implemented by subscribing without needing to guarantee a `hero` is ever in the scene, let alone how to access it. 
<br>

## So what's the catch?
Well, don't turn everything into a nail just because you have a hammer! 

Like any tool or pattern, you can over-do it. 

The main risk with events is creating too many that are overly granular and can spawn off their own events. 

This can make debugging really complicated and make it harder to reason about what is happening and why.

So generally, communicate direct interactions between nodes when you can. And if you feel yourself needing to let something waayyy out in the world or UI know about something that has happened, that's usually a good sign you want an event! 
<br>


## Wrapping up!
So let's go back to that sage Godot wisdom:

> "Call down, signal up"

Excellent advice and should always be the starting point when dealing with close-proximity node relationships. 

But when you are working with a different kind of relationship, one that crosses scenes and system boundaries -  events are your friend! 

They remove explicit references from the equation and make it easy for lots of different entities and systems to respond to things happening in the game. 

Just be intentional about designing your events. 

If you want an `EventBus` system that's ready to be dropped into your Godot project, [check out my repo](https://github.com/BajaTheFrog/godot-event-bus)! 
<br>

## More Reading
To read more on the pattern in general, I think the best source is Robert Nystrom's book - [_Game Programming Patterns_](https://gameprogrammingpatterns.com/) - for all sorts of game programming advice, but it has 2 chapters on this topic in particular:
- [Observer](https://gameprogrammingpatterns.com/observer.html)
- [Event Queue](https://gameprogrammingpatterns.com/event-queue.html)

And here are a handful of other pieces I found on the web, but there's lots!
- [GDQuest - The Event Bus Singleton](https://www.gdquest.com/tutorial/godot/design-patterns/event-bus-singleton/)
- [Tutemic - The good and bad of events (video)](https://www.youtube.com/watch?v=A7jJq7O7u7A)
- [Refactoring Guru - Observer Pattern](https://refactoring.guru/design-patterns/observer?source)
- [GMGStudio - Observer Pattern](https://medium.com/@GMGStudio/observer-pattern-how-to-use-this-crucial-design-pattern-in-game-development-with-unity-8967dffcde57)

Thanks for reading and leave a comment if you have any questions or thoughts!