How To Use The Unity Observer Pattern (With Static Classes)

Luis Ramirez Jr
Luis Ramirez Jr
hero image

One of the biggest challenges facing game developers is communication between game objects. Just how do you send data from one game object to another?

Well, the most common solution is to use the Observer pattern.

In this tutorial, you will learn the simplest way of implementing the observer pattern with static classes.

Important: If you want to follow along, check out this repository on GitHub. It contains the completed code from this tutorial.

Also, if you want to learn Unity from scratch or get stuck along the way in this tutorial, take my full Unity Bootcamp course.

With that out of the way, let's dive in!

What is a design pattern?

Throughout time, thousands of developers have constantly faced the same types of problems. Eventually, general solutions were curated for solving these problems.

That's what a design pattern is. It's a specific type of solution to address common problems, and there are literally dozens of design patterns available.

Such as:

  • Factory Pattern - Creates an instance out of classes
  • Singleton Pattern - A class where only one instance can exist during the lifetime of a program
  • Decorator Pattern - A class for adding responsibilities to an object dynamically
  • Proxy Pattern - An object to represent another object
  • Command Pattern - A pattern for processing commands and prioritizing different actions
  • Observer Pattern - A pattern for sending notifications to other classes of events

If you don't know how to use these yet, I highly recommend it. Design patterns can be used across multiple languages and frameworks, and are not tied to a specific language. They can also improve readability and scalability, so the larger your project is, the more helpful a design pattern can be!

However, using a design pattern is not as simple as copying and pasting code ๐Ÿ˜ข.

It's better to think of design patterns as concepts that can be adopted into any project. This means there are less limitations on how you can implement these patterns to fit your needs, but they do take time to set up.

This is why I always recommend that before you implement a design pattern, you should always take the time to determine whether it's worth all the hassle, to begin with.

Reviewing the project

If you downloaded the project linked earlier, you will encounter the following game example:

Review the project

The premise is very simple. The goal of this game is to move the ball around and collect coins (green objects), and movement is handled with the input system.

(If you're unfamiliar with the input system, check out my other article that covers this exact topic, here).

In the mean time, here's a quick review.

You will find the code for moving the ball here: Assets/Scripts/Movement.cs

using UnityEngine;
using UnityEngine.InputSystem;

public class Movement : MonoBehaviour
{
    private Rigidbody rb;
    private Vector3 movementVector;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        rb.AddForce(movementVector);
    }

    public void HandleMovement(InputAction.CallbackContext context)
    {
        Vector2 input = context.ReadValue<Vector2>();
        movementVector = new Vector3(input.x, 0, input.y);
    }
}

Here's whats happening:

  1. We're caching a reference of the RigidBody component in a variable called rb. The reference is then grabbed during the Start() method
  2. The HandleMovement() method is reponsible for listening to input from the player. We're storing the direction the player should move in in a variable called movementVector
  3. In the Update() method, the player has a force applied to them with the AddForce method. The force will be applied in the direction of the movementVector variable

Simple so far right?

Ok, another common feature implemented in video games is scores. This is why, for each coin collected in our example, the player's score should go up.

However, you will usually store this information in a different game object, and in our case, the score is kept track on the Score game object.

Can you see the problem here?

How do we update the score when the player touches a coin? After all, the score is stored on a completely different object, so the Score game object is never notified when the player touches a coin.

Well, good news. This is a common scenario where the observer pattern can help us.

Heres how...

Understanding the Observer Pattern

Excuse the technical jargon but, "the observer pattern is a solution where an object (subject) stores a list of subscribers and notifies them of updates by calling one of their methods".

Huh? Subscribers? Subject? What's with this terminology?

Well, lets break it down with an analogy. Imagine you're at an airport.

An employee may announce that it's time to board your flight, but employees don't specifically walk up to each individual to instruct them to board a flight.

That would be madness.

Instead, this part of the process is broadcasted to all passengers. Passengers listen for an announcement. Then, if their flight is announced, they'll react by standing up and walking up to their respective gate.

The process of broadcasting a message and people reacting to the message is similar to the observer pattern.

In the observer pattern, the subject is an object that broadcasts a message. A subscriber is another object that then listens to the broadcast.

observer pattern

By using the observer pattern, the broadcaster doesn't need to care about what subscribers do after a message has been broadcasted. (Decoupling is the most substantial benefit of using the observer pattern).

We can now use this method to keep score (subscriber), whenever the player touches a coin (broadcast).

So let's show you how.

Creating a Custom Event

To get started with the observer pattern, we must create an event, and the simplest way of implementing the observer pattern is to use static classes.

Static classes don't need an instance, so they're easily accessible to other classes, such as our components.

In the Assets/Scripts folder, there's a file called EventManager.cs.

using UnityEngine.Events;

public static class EventManager
{
    public static event UnityAction<int> OnAddPoints;

    public static void RaiseAddPoints(int points) => OnAddPoints?.Invoke(points);
}

Unity has custom delegates that you can use for creating events. They can be found under the UnityEngine.Events namespace.

Next, we're creating a new property called OnAddPoints annotated with the UnityAction delegate. If you need to send data to the functions registered, you can add generics, and up to 4 types are supported.

Here are examples:

// No arguments
public static event UnityAction SomeEvent;
// 1 argument
public static event UnityAction<int> SomeEvent;
// 2 arguments
public static event UnityAction<int, string> SomeEvent;
// 3 arguments
public static event UnityAction<int, string, bool> SomeEvent;
// 4 arguments
public static event UnityAction<int, string, bool, float> SomeEvent;

Lastly, we have a method called RaiseAddPoints for invoking the methods registered to the property. You may have noticed, but we're using optional chaining.

What does this mean?

The ? character in the OnAddPoints?.Invoke(points) line is the null-conditional operator, and will check if the property has methods registered to it. If not, they're not invoked.

Without this operator, you may receive errors if there aren't any subscribers.

Subscribing to the Event

As mentioned before, the Score game object is responsible for managing the score. I've also added a custom component that will be responsible for listening to the event called ScoreController.

(You can to find this component in the Assets/Scripts folder).

using UnityEngine;

public class ScoreController : MonoBehaviour
{
    private int currentScore = 0;

    private void OnEnable()
    {
        EventManager.OnAddPoints += HandleAddPoints;
    }

    private void OnDisable()
    {
        EventManager.OnAddPoints -= HandleAddPoints;
    }

    private void HandleAddPoints(int points)
    {
        currentScore += points;
        print(currentScore);
    }
}

Ovearll, the component is simple.

We have a property called currentScore for keeping track of the score, while the HandleAddPoints method can be called to update the currentScore.

Note that it has one argument called points to add to the existing score, but we're not going to be rendering the score. To keep it simple, we'll log the new score in the console instead.

The most important parts of the component are the OnEnable and OnDisable methods. In these methods, we're registering/unregistering the HandleAddPoints method to the EventManager.OnAddPoints event.

Essentially, we're 'subscribing' to the broadcast.

Raising the Event

The last step is to raise the event.

Raising the event is the process of broadcasting the message, so the subscriber can recieve it. The score should be then updated whenever the player touches the coin.

On the Coin game object, there's a component called CoinController.cs, which can be found in the Assets/Scripts folder.

using UnityEngine;

public class CoinController : MonoBehaviour
{
    [SerializeField] private int points = 10;

    private void OnTriggerEnter()
    {
        EventManager.RaiseAddPoints(points);
        Destroy(gameObject);
    }
}

All coins have the Collider component with the trigger option. Therefore, we're defining the OnTriggerEnter() method to handle this event.

Invoking this method causes the event to be raised, and the game object is destroyed.

Why do this? Well, if we don't destroy the game object, the player will be able to score unlimited points! Instead, the points will be incremented by 10 whenever a coin is touched.

Here's what the final result looks like.

ztm-unity-observer-3

Boom! And jst like that, the score gets logged in the console. Huzzah!

Add the observer pattern to your own projects!

As you can see, using the observer pattern makes it easy to communicate between game objects - and only takes a little effort to set up.

That being said, we've only covered one way here, but there are multiple different ways of implementing the observer pattern. For example, you could use scriptable objects for advanced cases.

However, if you're developing small-to-mid-sized games, using static classes can be a great way to centralize all your events.

So what are you waiting for? Go ahead and add this pattern into your own games today and let me know how it goes!

P.S.

If you want to learn more about each of those design patterns and how to implement them, as well as get your Unity skills to a point where you could get hired as a full-time game developer, then check out my course on Unity Game Design & Development.

This is the only Unity course you need to go from complete beginner (with absolutely no coding experience) to coding your own 3D games and getting hired as a Game Developer this year!

If you're already dabbling with Unity now, then this course is the fast-track to getting employed, and, you'll get to build a complete game as the project.

Plus by joining today, you'll also get to join me and fellow game devs in the ZTM Discord so you don't have to learn alone and will always have someone to help answer any questions you have!

Check out the first lesson here, totally for free.

More from Zero To Mastery

How To Move Game Objects (With Physics) In Unity preview
How To Move Game Objects (With Physics) In Unity

Want to add movement to your game objects but not sure how to get started? In this tutorial, I share 2 simple methods AND a follow-along practice project!

How To Get Started In Game Design preview
How To Get Started In Game Design

Want to go from playing games to getting paid $100K+ to build them? Our Game Design expert shares the steps to go from total noob to hired as a Game Designer.

Unity's New Input System (+ How To Use It!) preview
Unity's New Input System (+ How To Use It!)

Confused with how to set up the new Unity Input System, or why you should use it vs the Input Manager ๐Ÿ˜•? I cover this and more, in this step-by-step tutorial.