Why you should use the Observer Pattern in your Unity Games

Also called Event Listener Pattern, this pattern can be used to decouple components from each other in your game. Robert Nystrom has a dedicated chapter in his book Game Programming Patterns book about it. I suggest you read it first if you're not familiar with the pattern.

This tutorial gives one example of how this pattern can be used in Unity, but keep in mind that there are many other ways to do it.

Example

This repo has an basic working example using the following code.

Scenario

If you're fine tuning your game you'll notice that adding juicy effects can make a huge difference to improve boring, lifeless hits into beautiful and satisfying ones. So you add a PlayKillAnimation method to call effects, things like particles, sounds, time stoppers and even camera shake if you're feeling fancy.

void PlayKillAnimations() 
{ 
    GetComponent<KillParticles>().Instantiate();
    GetComponent<KillSound>().Play(); 
    GetComponent<TimeStopper>().StopTime();
    GetComponent<CameraShaker>().Shake();
}

This code works, but now this component has a reference to four other components and is subject to breaking if any of those change. Also, when adding a new effect to this animation you'll need to add a reference to another component, coupling things even more.

There are many articles out there explaining the problem with coupling components, I recommend that you read as many as possible to draw your own conclusions. You can start with this one (Coupling and Cohesion)

Event Listener and Event Sender

One strategy to fix the problem is by using the Observer Pattern.

You'll need to create an interface for your event listener, the component that will receive the event and react to it.

public interface IEventListener<T> 
{ 
    void OnEvent(T event);
}

Then you can create the base Event Sender class that will be responsible for holding a list of every listener for that specific event.

I prefer to create it as an abstract class and extend MonoBehaviour since I still didn't find a scenario where I had to emit two different types of events in the same component, but if you do, you can use an interface for the EventSender as well.

public abstract class EventSender<T> : MonoBehaviour
{
    protected List<IEventListener<Event>> Listeners { get; set; } 
          = new List<IEventListener<Event>>();

    public void SubscribeToEvent(IEventListener<Event> listener)
          => Listeners.Add(listener);

    public void UnsubscribeFromEvent(IEventListener<Event> listener)
           => Listeners.Remove(listener);

    public void NotifyListeners(Event e)
    {
        foreach (var listener in Listeners)
           listener.Notify(e);
    }
}

Now create an enum to define which events might be emitted from the sender and extend your own component with the abstract class.

public enum EnemyKillerEvent
{
    EnemyKilled,
    EnemyStunned
}
public class EnemyKiller : EventSender<EnemyKillerEffect>
{
}

Then create event listeners for each effect you want to play for this specific event. To make things easier, I'll show only the time stopper effect.

public class TimeStopperOnEnemyDeath : MonoBehaviour, IEventListener<EnemyKillerEffect> 
{
    public void OnEvent(EnemyKillerEffect e)
    {
        if (e == EnemyKillerEffect.EnemyKilled)
            // stop time here
    }
}

Now comes the tricky part. There're are many ways you can populate the Listeners list inside your Event Sender class, the best way that worked out for me was using the GetComponentsInChildren method while making sure that all event listener components were there to be added. In the following image, for example, all events are in a separated game object as children from the event listener component.

image.png

If you're setting your scene like this you can just add this awake method in your implementation of the EventSender component:

void Awake()
{
    Listeners = GetComponentsInChildren<IEventListener<EnemyKillerEffect>>();
}

Finally, instead of calling all those components as the first example, the PlayKillAnimations will call only the OnEvent method sending the event that happened.

void PlayKillAnimations() 
{
    NotifyListeners(EnemyKillerEffects.EnemyKilled); 
}

Done. Now your EnemyKiller component does not know anything about the effects that it plays when killing an enemy. You can easily add new effects or remove old ones just by adding a new independent components as children to the Event Sender.

Some drawbacks

There are some drawbacks when implementing this pattern, but I believe most of them can be tackled gracefully depending on your case.

Too many events

If you're using a time stopper effect for multiple porpuses, let's say, when you both kill and gets hit by an enemy, you'll need to create two event listeners, since each one of those events might be sent by different sources. This might increase the amount of code you write, but if you separrate the logic that stops the time from the event listener things might be better. Having a TimeStopper component and a StopTimeOnEnemyKill event, for example. Then, every other event that will stop the time can call the TimeStopper.

Unsubscribing from senders

Sometimes there might be effects that will only be played once, or that will be assigned to an object who might be destroyed in game time, when doing this, make sure to call the UnsubscribeFromEvent method in the sender, otherwise you'll have a list that references already destroyed components.

Possible Improvements

Instead of implementing all those interfaces and abstract classes, you can a framework for events that already handles all those things, like UniRX. Personally, I prefer to create my own classes to have more control, but this can be a valid option too.

Since we're using generics, you don't need to limit yourself to using enums, you can use every other type as data to emit an event too, like a class with more data in it.

References