How to properly communicate between game objects with the observer pattern


One of the most common issues you’ll run into in your game development journey is how to let two unrelated objects communicate with one another in a way that doesn’t completely ruin the organization and flexibility of your code. Maybe we want to increase the player’s score when an enemy is killed, or want the UI to show a message when the player gains a level. We don’t want to go crawling around the application, connecting unrelated nodes as we please, and creating deep, fragile couplings (such as having our game crash because the player object tried to call a now-renamed function in our UI code), but we need a way for these separate objects to talk to one another. That’s what today’s post is about.

Our example

Say we’ve got an action-adventure game and when the player enters the arena where a boss fight will occur, we want to lock a gate behind them and display a special UI for the boss and their health. We’d like to implement this using a collision body somewhere in the level that can detect when the player enters the arena and use that detector to trigger these events. The problem is that a specific collision body in a specific level is a long ways away from our UI in the application hierarchy, and while our gate may be closer to our collision object, at best it’s parallel to it in the hierarchy amongst all other level objects, so we still shouldn’t access it directly as we want each object in our game to be self-sustaining and independent of the application around it.

Let’s just whip up a quick Elden Ring, as an example.

What we really want is a way to shout “Hey, the player has entered the boss arena!” and let all interested parties do whatever they want with that information. Letting them choose how to respond would also let an object choose to not react to an event if it made sense to do so. If the boss is dead, the gate no longer needs to lock itself and we don’t need to show any special UI.

The solution is the observer pattern.

The observer pattern

This is such a common design pattern that most programming languages and game engines have native, or at least standardized, support for it. Therefore, I’m not going to go too into weeds on how it works before talking about how to use it since you most likely have no need to implement it yourself. In the observer pattern, objects register themselves as wanting to be notified when a certain event has occurred. This registration is usually done by providing what is essentially a callback function. When the event occurs, the subject (the thing the observers are… observing) will go through the list of all registered observer functions and call them one by one. Here’s what the code for a subject might look like in JavaScript:

class Subject {
  constructor() {
    this.observers = [];
  }

  register(observer) {
    this.observers.push(observer);
  }

  deregister(observer) {
    const index = this.observers.indexOf(observer);
    this.observers.splice(index, 1);
  }

  emit() {
    this.observers.forEach(observer => observer());
  }
}

Just a simple container to hold references to all observers, call their registered functions when the even is emitted, and let them come and go from the list as they please. So if we had a subject for when the player enters the arena, our gate code could do something like:

function lockGate() {
  // Lock the gate
}

// Included at some point in our code, maybe during initialization
PlayerEnteredArenaSubject.register(lockGate);

// After the boss is defeated, deregister from the subject so the gate no longer locks
function onBossDefeated() {
  PlayerEnteredArenaSubject.deregister(lockGate);
}

And now it will always lock when the player enters. Once the boss has been defeated, we can just deregister from the subject and not worry about what the player is doing anymore.

If this looks familiar to you, it probably is. For you Godot users, this is called a signal. If you’re using C#, such as with Unity, the language has built-in “events”. And observables are all over the place in the world of web development. If you’re using something else, look up how your language or engine handles “events” or “observables”. Odds are it’s already taken care of.

How to actually use it

So that’s the basic theory out of the way, but what would its usage actually look like in a full game? Well, if your subjects and observers are close to each other in the tree, you can get away with connecting them directly by passing around a reference to the subject. Let’s go back to our example of locking a gate when the player enters the arena and let’s assume our level layout and hierarchy is like the following, where we have an Arena object that houses everything that goes inside the arena:

Level
|-- Arena
    |-- Arena Gate
    |-- Arena Entrance Detector

In this instance, the observer pattern is a great fit for notifying the gate of the player entering the arena. We can do this by:

1) Defining a subject on Arena Entrance Detector that fires when it detects the player

2) Having Arena access and store a reference to this subject during level initialization

3) Passing this reference to Arena Gate so it can subscribe to the subject and lock itself when the event is emitted

With this technique, all of our logic is self-contained. Arena Entrance Detector can fire off its event anytime, whether or not anyone is listening and regardless of what the layout of the world around it is like, which also has the benefit of letting us turn this into a generic Player Detector that we can use for triggering cutscenes, tutorials, and more. Arena Gate is similarly isolated and reusable since now it just has an event it listens to for locking itself, once again opening it up to potentially becoming a generic Event Lockable Gate. And lastly, Arena is there to just facilitate a bit of communication between the two during initialization, but then is absent afterwards, meaning it’s doing its job of providing light organization and management help to its immediate children without getting too into the weeds of their implementation. Overall a lot better than crawling the application tree directly!

Event buses

But what about when the objects we want to connect are far away from each other? For instance, UI elements aren’t anywhere near the objects in our level. An expanded view of the game’s hierarchy might look like this:

Game
|-- Level
    |-- Arena
        |-- Arena Gate
        |-- Arena Entrance Detector
|-- UI
    |-- Boss UI
        |-- Health Bar

We don’t want to have to crawl up several levels, passing references at each stop, to connect the UI to the Arena Entrance Detector. Our Game and Level objects have more important things to worry about than connecting lower level events and objects like that. Plus, this would still add some significant coupling and make it a pain if we want to change where or how our UI or gate is implemented later.

Tracking the player’s score is another example of distant objects needing to talk to one another.

What we need in this case is a way to have our subjects be accessible from anywhere, and that’s where an event bus comes in. An event bus is a globally available object that can let distant objects communicate events with one another without cluttering up the code of the entire application tree each time a connection needs to occur. We do this by making a global object, oftentimes as a singleton, which holds the subjects we want. When an object wants to subscribe to a subject, it can access it directly from the event bus rather than from the object that will trigger it. Similarly, the objects that are responsible for emitting an event can connect to the bus and use those subjects to make their events known globally. Ignoring any sort of global or singleton setup, a basic event bus for our action-adventure game could be as simple as this:

class EventBus {
    constructor() {
        this.playerEnteredArena = new Subject();
    }
}

Then we can connect our special boss UI to it the same way we did above, just using the event bus as an intermediary step:

EventBus.playerEnteredArena.register(showBossUI)

To emit our signal, the Area Entrance Detector can just do:

EventBus.playerEnteredArena.emit()

This technique is also a great way to keep the UI updated. For instance, we could add another subject for boss health to our event bus and let the boss emit an update every time it takes damage:

EventBus.bossHealthUpdated(newValue)

And if singletons and global objects don’t bother you too much, you can even organize your code further by using multiple busses geared towards different uses, such as an AudioEventBus, UiEventBus, and so on.

A brief detour to Godot and closing thoughts

Since a lot of readers here use Godot, including myself, I’ll take a moment to quickly point out what this flow looks like in that engine. As previously mentioned, signals are the implementation of the observer pattern in Godot. The way you make an event bus is to simply put your signals in an Autoload and use that to access everything. Godot takes care of all the other implementation work for you. Simple enough, right?

And lastly, as is usually the case with a good design pattern, Game Programming Patterns has a much more in-depth look at this pattern in case you want to learn more.