The Mediator Pattern In Unity

Posted by : on

Category : Unity   Architecture

What Is The Mediator Pattern

The mediator pattern is a behavioral design pattern that helps us reduce the dependencies between our types. Instead of having dependencies between our classes so that they can communicate with each other, our classes depend on a mediator class that is responsible for the communication.

Our types, with the use of the mediator, can communicate indirectly, as the mediator is now responsible for passing the appropriate calls to each one. This allows us to reduce the number of dependencies that exist and easily add new functionality to our code, because now we only have to couple any new class to the mediator object only.

In this post, I will show how the mediator pattern can be used in Unity, with game objects and scriptable objects. If you are not familiar with the mediator pattern in general, there are some excellent resources on the internet, for example the refactoring guru site.

The Mediator Pattern With Monobehaviours

Let’s suppose that we have six different game objects in our scene, six squares with colors red, blue, yellow, green, purple and pink and each of those has its own script. We want whenever something happens to any of these squares, then something else to happen to some of them.

Colored Squares

For example, all of those squares have the OnMouseOver, OnMouseDown and OnMouseExit event methods and each one has a set of methods that do something to the game object like ChangeColor, Rotate or ChangeSatellitesColor.

We want whenever one of the OnMouse methods gets called, one or more squares to execute one of their methods. One way to do that would be for each game object to have serialized fields in the editor that are the other five game objects. Then we would have to create the logic of what happens in each square separately. This causes some problems:

  • We would have duplicate code. If for example we wanted that whenever one of the squares gets clicked the pink square would rotate, this logic would have to be in every script.

  • Any change we would like to do in our scene would take time as we would need to change every script that depends on the changed object. If for example we remove the pink square, then we have to go to every one of the other five scripts and remove any calls to it.

Instead of having our squares depend on each other, we create a mediator game object (represented with the white circle in the center). Now each square will have a serialized field with the mediator and only the mediator will have serialized fields with references to our squares.

The mediator script will have only one method, called Notify that contains all the logic of what happens when a certain event is true. Each game object will call this method, with an argument that represents the event, and this method will call the behaviors we want to execute on each square.

We can create an enum that has all the possible notifications:

public enum Notification
{
   Click,
   MouseHover,
   Rotate,
   ResetColors
}

And then add our logic to the Mediator class. For example:

public class Mediator : MonoBehaviour
{
   [SerializeField] private Red red;
   [SerializeField] private Green green;
   [SerializeField] private Blue blue;
   [SerializeField] private Yellow yellow;
   [SerializeField] private Purple purple;
   [SerializeField] private Pink pink;
   
   public void Notify(Notification notification)
   {
      switch (notification)
      {
         case Notification.Click:
            purple.ChangeColor();
            pink.Rotate();
            break;
         case Notification.MouseHover:
            blue.ChangeColor();
            green.Rotate();
            break;
         case Notification.Rotate:
            red.ChangeColor();
            yellow.ChangeSatellitesColor();
            break;
         case Notification.ResetColors:
            red.ResetToDefaultColors();
            green.ResetToDefaultColors();
            blue.ResetToDefaultColors();
            yellow.ResetToDefaultColors();
            purple.ResetToDefaultColors();
            pink.ResetToDefaultColors();
            break;
         default:
            throw new ArgumentOutOfRangeException(nameof(notification), notification, null);
      }
   }
}

As an example, of how the script of one of our squares could be:

public class Yellow : MonoBehaviour
{
   [SerializeField] private Mediator mediator;
   [SerializeField] private SpriteRenderer[] satelliteRds;
   
   private void OnMouseOver() => mediator.Notify(Notification.MouseHover);
   private void OnMouseDown() => mediator.Notify(Notification.Click);
   private void OnMouseExit() => mediator.Notify(Notification.ResetColors);


   public void Rotate()
   {
      transform.Rotate(Vector3.forward, 45f);
      mediator.Notify(Notification.Rotate);
   }

   public void ChangeSatellitesColor()
   {
      foreach (var srd in satelliteRds)
      {
         srd.color = Color.black;
      }
   }
   
   public void ResetToDefaultColors()
   {
      foreach (var srd in satelliteRds)
      {
         srd.color = Color.white;
      }
   }
}

This solves our previous problems. All our logic exists in the Mediator class and any change that we want to make, like removing the pink square, will only affect the Mediator class. Each script of the squares, will contain logic that sends a signal to the Mediator of what has happened and the resulting call of that signal is the mediator’s responsibility.

Adding The Sender Info To The Notify Method

The previous implementation of the Mediator works if we want something to happen, to every game object from every object that sends the signal. There is no way for us to know from where the signal came from. If we want for example, to rotate the pink square when only the yellow square gets clicked, we don’t have the data to code that in the mediator as it is currently implemented.

We can add that information, as an argument to the Notify method, if we change the method’s signature like this:

public void Notify(MonoBehaviour sender, Notification notification)

then by calling the Notify, for example:

private void OnMouseOver() => mediator.Notify(this, Notification.MouseHover);

we can perform checks on the type:

switch (notification)
      {
         case Notification.Click:
            
            purple.ChangeColor();

            if(sender is Red)
               pink.Rotate();

            break;
            .
            .
            .

Obviously the sender doesn’t have to be a Monobehaviour, if our game objects have some common data, and we need to perform checks on that data, then creating a common interface with properties can be better.

The Mediator Pattern With Scriptable Objects

The mediator pattern in Unity, can become really powerful when it is implemented in a scriptable object.

Scriptable Objects in Unity, have the advantages (and disadvantages) of singletons as they have only one state that is shared, but are also used by Unity the same way as any other asset. They get loaded when needed and unloaded when there are no more references to them.

With Monobehaviours, we need to have our game objects in the scene so that we can drag and drop the references in the editor. Scriptable objects, can exist as references in our prefabs that have not yet been instantiated. A scriptable object mediator doesn’t exist in our scene as a game object, but exists whenever it is referenced by a game object that is on our scene.

This allows us to dynamically create and destroy game objects, that will respond to our notifications when needed. The only problem with this approach, is that we need a mechanism to register and unregister our game objects to the mediator, so that the mediator scriptable object can have references to the game objects that have been instantiated.

First we create a common interface:

public interface IRotateAndChangeColor
{
   public void Rotate();
   public void ChangeColor();
   public void ResetToDefaultColors();
}

Then we have our monobehaviour scripts implement that interface:

public class Yellow : MonoBehaviour, IRotateAndChangeColor
{
   [SerializeField] private MediatorSO mediator;

   private readonly Color defaultColor = Color.red;
   private SpriteRenderer sr;
        
   private void Awake() => sr = GetComponent<SpriteRenderer>();

   private void OnMouseOver() => mediator.Notify(this, Notification.MouseHover);
   private void OnMouseDown() => mediator.Notify(this, Notification.Click);

   private void OnMouseExit() => mediator.Notify(this, Notification.ResetColors);

   private void OnEnable() => mediator.Subscribe(this);

   private void OnDisable() => mediator.UnSubscribe(this);
   
   public void Rotate() => transform.Rotate(Vector3.forward, 5f);
   
   public void ChangeColor() => sr.color = Color.black;
        
   public void ResetToDefaultColors() => sr.color = defaultColor;
}

The OnMouseOver, OnMouseDown and OnMouseExit event methods here, exist for every object that we want to be able to send a notification. The IRotateAndChangeColor interface, is implemented by every object that we want to register to the scriptable object mediator and respond to notifications.

Both, the objects that send notifications and the objects that respond to the notifications, have a reference to our scriptable object mediator.

The objects that send messages use the mediator.Notify method and the objects that respond to messages implement the IRotateAndChangeColor and use the mediator.Subscribe and mediator.UnSubscribe methods.

Finally, here is the implementation of our mediator:

[CreateAssetMenu(fileName = "Mediator", menuName = "Scriptable Objects/Mediator")]
public class MediatorSO : ScriptableObject
{
    private readonly List<IRotateAndChangeColor> _subscribers = new();
    
    public void Subscribe(IRotateAndChangeColor iRotateAndChangeColor) => _subscribers.Add(iRotateAndChangeColor);

    public void UnSubscribe(IRotateAndChangeColor iRotateAndChangeColor) => _subscribers.Remove(iRotateAndChangeColor);

    public void Notify(MonoBehaviour sender, Notification notification)
    {
        foreach (var subscriber in _subscribers)
        {
            switch (notification)
            {
                case Notification.Click:
                    if(sender is Blue)
                        subscriber.ChangeColor();
                    break;
                case Notification.MouseHover:
                    subscriber.Rotate();
                    break;
                case Notification.ResetColors:
                    subscriber.ResetToDefaultColors();
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(notification), notification, null);
            }
        }
    }
}

All the subscribers in this implementation, respond to all notifications. If we want different subscribers for each notification, we can create an interface, a subscribe/unsubscribe method and a collection for each one.

But creating a collection plus subscribe/unsubscribe methods for each notification, starts to feel awfully familiar. This is because C# already has a mechanism for this.

Combining The Mediator Pattern With Events

Instead of creating interfaces and subscribe/unsubscribe methods for each notification, we can combine our scriptable object mediator with the observer pattern. C# has a build in implementation of the observer pattern with the event keyword.

Our scriptable object mediator, can have one event for each notification. Each object that wants to get notified can subscribe to the appropriate event, and each game object that wants to send a notification can invoke that event.

Our scriptable object mediator, can be simplified to this:

[CreateAssetMenu(fileName = "MediatorEventsSO", menuName = "Scriptable Objects/MediatorEventsSO")]
public class MediatorEventsSO : ScriptableObject
{
    public event Action<MonoBehaviour> Clicked;
    public event Action<MonoBehaviour> MouseHover;
    public event Action<MonoBehaviour> MouseExited;

    public void OnClicked(MonoBehaviour monoBehaviour) => Clicked?.Invoke(monoBehaviour);
    public void OnMouseHover(MonoBehaviour monoBehaviour) => MouseHover?.Invoke(monoBehaviour);
    public void OnMouseExited(MonoBehaviour monoBehaviour) => MouseExited?.Invoke(monoBehaviour);
}

Obviously as before, we might not need the Monobehaviour argument, our methods can have zero arguments, or an argument type that is appropriate for our use case depending on the data we need.

Now the previous implementation of the Yellow class, will be like this:

public class Yellow : MonoBehaviour
{
   [SerializeField] private MediatorEventsSO mediator;

   private readonly Color defaultColor = Color.red;
   private SpriteRenderer sr;
        
   private void Awake() => sr = GetComponent<SpriteRenderer>();

   private void OnMouseOver() => mediator.OnMouseHover(this);
   private void OnMouseDown() => mediator.OnClicked(this);
   private void OnMouseExit() => mediator.OnMouseExited(this);

   private void OnEnable()
   {
      mediator.Clicked += ChangeColor;
      mediator.MouseHover += Rotate;
      mediator.MouseExited += ResetToDefaultColors;
   }

   private void OnDisable()
   {
      mediator.Clicked -= ChangeColor;
      mediator.MouseHover -= Rotate;
      mediator.MouseExited -= ResetToDefaultColors;
   }

   private void Rotate(MonoBehaviour monoBehaviour) => transform.Rotate(Vector3.forward, 5f);
   private void ChangeColor(MonoBehaviour monoBehaviour) => sr.color = Color.black;
   private void ResetToDefaultColors(MonoBehaviour monoBehaviour) => sr.color = defaultColor;
}

With this approach, we have an additional benefit. All our dependencies point towards our mediator. The mediator has no dependencies to the classes that are going to receive the notifications. For this reason we have improved the encapsulation of our types, by making the methods that react to notifications private.

An additional benefit, is that this approach doesn’t only allow us to create/destroy game objects at runtime, but it also allows us to test our prefabs in isolation. If for example we have a player character that whenever he is about to attack the different types of enemies have different responses, we have no dependencies between them. We can test our player character alone in a scene, or any of our enemies or any combination as we want. The only dependency that exists, is between each of those classes and the mediator, that is automatically loaded by Unity whenever there is a reference to it, because it is a scriptable object.

Conclusion

The last implementation of the mediator, is the most elegant and most appropriate for 99% of the cases. I wanted to start from the classic implementation of the mediator pattern and explain the thought process of adapting a classic pattern to Unity:

  • In the classic implementation, we get the dependencies using constructor injection. Unity’s game objects don’t have constructors. (Actually they have parameterless constructors, that get executed after every serialization/deserialization, but that doesn’t help us with constructor injection).

  • To get the dependencies in our game objects, we have to use Unity’s SerializeField attribute and perform the necessary dependency injections inside the Unity editor.

  • After that, we can see that there is no need for our mediator to be a game object, as it doesn’t need to be rendered to the screen. For this reason we can make it a scriptable object and inject the dependencies the same way.

  • The difference this time, is that the scriptable object cannot have references to our scene’s game objects, so we create subscribe and unsubscribe methods to handle those dependencies.

  • Because we can have different notifications, having only one collection that handles our subscribers can create problems, as we don’t want all of our subscribers to be subscribed to all the notifications

  • Instead of creating a collection with subscribe/unsubscribe methods for each notification, we can see that this resembles a very familiar mechanism of C#, the events.

  • By using events, we have gotten rid of the subscribe/unsubscribe methods and now the only dependencies that exist are from our classes to the scriptable object mediator.

Step by step, the architecture is improved, by handling the dependencies using mechanisms that are provided by the engine. We went from having dependencies between all our game objects, to finally having only one dependency, the dependency to the scriptable object mediator, that we don’t have to worry about, because Unity can automatically load/unload it, whenever it is needed.

The code for every step of this post can be found in this github repo.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.


About Giannis Akritidis

Hi, I am Giannis Akritidis. Programmer and Unity developer.

Follow @meredoth
Follow me: