GD Patterns: Observer
A continuation of the GD Patterns series, following the Game Programming Patterns book by Robert Nystrom, and applying its insights within the Unity environment.
Observer
Observer, being a behavioral pattern, is one of the most popular and well-known design patterns from the Gang of Four. It has a native implementation in C# using the event
keyword and in the Unity API using the UnityEvent
class.
The Observer pattern is built around a subscription model, allowing the observed subject to notify its subscribers by dispatching events when certain criteria are met. Upon receiving the notification, subscribers (or observers) react accordingly, separating the event-handling logic from the subject.
Example
So, the pattern consists of two entities: the observer and the subject being observed. Let’s break down a basic subscription model.
First, let’s start with the Observer
class. At this stage, it only contains a single OnNotify()
method to be called upon notification by the subject.
public class Observer
{
public void OnNotify()
{
Debug.Log("Notification received.");
}
}
Next, the Subject
class. Its job is to keep track of observers (in this case, in a List
) and notify them when something happens by invoking the OnNotify()
method on each of them.
public class Subject
{
private List<Observer> _observers = new();
private void Notify()
{
foreach (Observer observer in _observers)
{
observer.OnNotify();
}
}
}
The subscription bit comes into play with exposing a public API for adding and removing observers to the subject:
public class Subject
{
//...
public void AddObserver(Observer observer)
{
_observers.Add(observer);
}
public void RemoveObserver(Observer observer)
{
_observers.Remove(observer);
}
}
This setup can lead to a situation where the Observer
is destroyed, but the Subject
still keeps a reference to it. This condition can result in a NullReferenceException
when invoking the event, which is a big no-no.
The way to deal with this is to unsubscribe the observer from the subject right in the observer’s destructor. In the example below, I’ve also added a constructor to demonstrate subscribing and unsubscribing more clearly.
public class Observer
{
private Subject _subject;
public Observer(Subject subject)
{
_subject = subject;
_subject.AddObserver(this);
}
~Observer()
{
_subject.RemoveObserver(this);
}
public void OnNotify()
{
Debug.Log("Notification received.");
}
}
To sum up, here’s a quote from Robert Nystrom’s book:
It’s an observer’s job to unregister itself from any subjects when it gets deleted.
Making it better
Up to this point, our subscription system is very rigid and not very usable, as it’s tightly coupled to specific classes.
We could make it more versatile by using interfaces for both the observer and subject classes, but even then, it wouldn’t be very agile. In our case, making the observer system function-based and using delegates would be a more appropriate fit, as a delegate
is essentially a reference to a method with a predefined signature.
First, we need to define a delegate
, specifying its return type (void
), name (OnNotifyHandler
) and the list of arguments in the brackets (just a Subject
class reference to keep it simple).
public class Subject
{
public delegate void OnNotifyHandler(Subject subject);
}
Then we replace Observer
references with OnNotifyHandler
references everywhere within the Subject
class.
public class Subject
{
public delegate void OnNotifyHandler(Subject subject);
private List<OnNotifyHandler> _observers = new();
private void Notify()
{
foreach (OnNotifyHandler observer in _observers)
{
observer.Invoke(this);
}
}
public void AddObserver(OnNotifyHandler observer)
{
_observers.Add(observer);
}
public void RemoveObserver(OnNotifyHandler observer)
{
_observers.Remove(observer);
}
}
At last, we also need to modify the Observer
class a bit.
public class Observer
{
private Subject _subject;
public Observer(Subject subject)
{
_subject = subject;
_subject.AddObserver(OnNotify);
}
~Observer()
{
_subject?.RemoveObserver(OnNotify);
}
public void OnNotify(Subject subject)
{
Debug.Log($"Notification from {subject.name} received.");
}
}
As seen in the constructor, we can now pass OnNotify()
method of the Observer
to the Subject
directly. Now, the observer of any type can subscribe to the subject by providing a method of the right signature.
Observers in C# and Unity
All of the above was examined to gain a better understanding of the Observer pattern’s underlying mechanisms, but you most probably won’t ever be writing it all yourself, as there are baked-in implementations of it pretty much everywhere.
Firstly, C# has its own implementation of the Observer pattern in the form of the event
keyword, which simplifies things (and improves performance).
Let’s see how the Subject
would change when using event
.
public class Subject
{
public event Action<Subject> OnNotify;
private void Notify()
{
OnNotify.Invoke(this);
}
}
As seen above, only the declaration of the event remains, which is invoked when needed. There’s no need to keep track of observers or write a subscription API—the event
keyword does it all for you. Action
is a delegate with a void
return type, and method arguments are encapsulated within <>
brackets.
The Observer
class remains largely unchanged, except for a slight change in the subscription syntax.
public class Observer
{
private Subject _subject;
public Observer(Subject subject)
{
_subject = subject;
_subject.OnNotify += OnNotify;
}
~Observer()
{
_subject.OnNotify -= OnNotify;
}
public void OnNotify(Subject subject)
{
Debug.Log($"Notification from {subject.name} received.");
}
}
Unity has its own implementation of the Observer pattern—the UnityEvent
class. Here’s how it works.
public class Subject
{
public UnityEvent<Subject> OnNotify;
private void Notify()
{
OnNotify.Invoke(this);
}
}
public class Observer
{
private Subject _subject;
public Observer(Subject subject)
{
_subject = subject;
_subject.OnNotify.AddListener(OnNotify);
}
~Observer()
{
_subject.OnNotify.RemoveListener(OnNotify);
}
public void OnNotify(Subject subject)
{
Debug.Log($"Notification from {subject.name} received.");
}
}
Not much has changed here:event Action<Subject>
has turned into UnityEvent<Subject>
in the Subject
class, and the subscription syntax has been altered in the Observer
class.
C# event vs UnityEvent
So, the two primary options of utilizing the Observer pattern within Unity environment are C# event
keyword and UnityEvent
class. But when should we choose one over the other?
UnityEvent
supports serialization in the Unity editor, allowing subscribers to be added or removed from outside the code. This makes it a useful tool for non-developers.
However, in other cases, there’s no visible benefit to using UnityEvent
over C# event
, as Unity’s implementation is at least 2 times slower and generates more garbage over time. For a detailed examination of the topic, please refer to the article by Jackson Dunstan.
Conclusion
The Observer pattern is dead simple, lightning fast and reliable. At its core, it’s just an object and a bunch of listeners waiting for something to happen to it. There are more advanced techniques built on top of it, like Reactive Programming (see UniRx) and the Event Queue pattern, which I hope to cover here as well. As of today, the Observer is everywhere and won’t go anywhere anytime soon.