Model View Controller Pattern for in-game UI

MVC For Unity Diagram

This article describes an approach to structuring UI in Unity. This approach is based on the Model View Controller (MVC) pattern, but it’s slightly adapted for Unity. This isn’t the only way to structure your UI, but it has worked well for all my previous Unity games.

Model View Controller Pattern

In case you’re unfamiliar with the MVC pattern, I’ll summarize it. MVC is a pattern used to separate UI, Data, and functionality. The Model holds the data to display, the View manages the layout, and the Controller handles the functionality. This separation (theoretically) makes it easier to change each part independently. For example, you could quickly redesign the layout without touching the data or the functionality. That’s MVC in a nutshell, so let’s look at how we can adapt this pattern for Unity UI.

MVC for Unity

First off, let’s talk about the Model. The Model is nothing but a big data container. There are several different ways you could achieve this, and to be honest, they’re probably all valid. It depends on your use case. However, I prefer to use a Scriptable Object.

Scriptable Objects as a Model

The advantage of using a Scriptable Object (SO) as your Model comes down to flexibility, testability, and ease of use.

Scriptable Objects can either be created at runtime or in the editor. This feature allows you to test different scenarios with very little code. For example, if you wanted to try several different UI scenarios, you could create a new Scriptable Object to represent each one. Then, it’s just a matter of switching between them and viewing the result. Additionally, if you need to pull data from a spreadsheet or database, you could accomplish that by creating an SO at runtime and filling it with the retrieved data. In other words, using an SO as your Model won’t force you into an awkward place down the line. It’s adaptable to any scenario.

Finally, Scriptable Objects are first-class citizens in the Unity Editor. What I mean by that is that they interact well with the Editor ecosystem, especially the Inspector. As a result, it’s easy to manipulate your data, which isn’t the case for databases or raw JSON. By the way, your Model could be as simple as an object with a list of variables:

public class DataModel : ScriptableObject
{
    public int Coins;
    public int Gems;
    public int Tickets;
    public int Friends;
    //...etc
}

The View Controller

As for the view controller, it’s a MonoBehaviour at the root of our prefab. If you haven’t read my previous two articles on the Main Loop and Structured Prefabs, I recommend you do so. The way I structure View Controllers is a Structured Prefab. The view controller is in charge of binding all your UI pieces to their corresponding variables. By the way, there are essentially two strategies for hooking up UI, polling or event-based. Both approaches are valid so let’s look at the pros and cons.

Polling-based UI

If you have a simple UI, using a polling-based method is much simpler. This works by simply setting the UI to the most recent data every frame. For example, if you have a coin counter in your in-game UI, just set the current number of coins in Update.

public class GameViewController : MonoBehaviour
{
    [SerializeField] CoinCounterPanel _coinCounterPanel;
    [SerializeField] GameViewModel _model;
    
    public void Update()
    {
        _coinCounterPanel.SetText(_model.coins);
    }
}

This type of polling-based UI system is easy to read and understand. Additionally, you don’t have to worry about managing state, and your UI will always be in sync. There are some disadvantages though. For one, you may be wasting CPU cycles to set data that hasn’t changed. Also, depending on the UI, the code might become very lengthy. However, given that this code tends to be straightforward, this may not be an issue.

Event-based UI

In an event-based system, the UI updates whenever an event occurs that changes your data.

public class CoinCounterPanel : MonoBehaviour
{
    GameViewModel _model;
    [SerializeField] Text _text;

    public void OnCreated(GameViewModel model)
    {
        _model = model;
        _model.OnCoinsValueChanged += OnCoinCountChanged;
    }

    void OnDestroy()
    {
        _model.OnCoinsValueChanged -= OnCoinCountChanged;
    }

    void OnCoinCountChanged(int previous, int current)
    {
        _text.text = $"Coins: {current}";
    }
}

This system is a little more complicated because you have to bind the data to the UI. In other words, you need to make sure that a specific method executes every time the bound variable changes. In the above example, we need OnCoinCountChanged to run every time the coins value changes. To do that, you need to keep track of every time the coins value changes. In my example, I did this by wrapping coins in a Setter that calls an event. So the Model code changes from this:

public class GameViewModel : ScriptableObject
{
    public int Coins;
}

To this:

public class GameViewModel : ScriptableObject
{
    [SerializeField] int _coins;

    public int Coins
    {
        get => _coins;
        set
        {
            int previous = _coins;
            _coins = value;
            OnCoinsValueChanged?.Invoke(previous, _coins);
        }
    }

    public event Action<int, int> OnCoinsValueChanged;
}

Hopefully, this example demonstrates the added complexity of the event-based approach. Not to mention, using this code, you could still modify the _coins value in the Inspector, and it wouldn’t notify the UI, so it’s not even a comprehensive solution. This is a lot of code to manage just one variable, so this won’t scale. Let’s build a better system.

I created an ObservableVariable type, named after the Observer pattern:

[Serializable]
public class ObservableVariable<T>
{
    [SerializeField] T _value;

    public event Action<T, T> OnValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            T previous = _value;
            _value = value;
            OnValueChanged?.Invoke(previous, _value);
        }
    }
}

It’s a generic version of the same code we used to make our coins variable observable earlier. Now we can modify our Model to look like this:

public class GameViewModel : ScriptableObject
{
    public ObservableVariable<int> _coins = new ObservableVariable<int>();
}

By the way, you may have noticed that I haven’t shown the GameViewController code for our event-based system. That’s because it no longer does any work beyond initialization. Once the UI is bound to the data, it’s completely passive. In other words, it just waits for changes to occur rather than actively looking for modifications. So it ends up looking like this:

public class GameViewController : MonoBehaviour
{
    [SerializeField] HealthPanel _playerHealthPanel;
    [SerializeField] CoinCounterPanel _coinCounterPanel;

    public void OnCreated(Health playerHealth, GameViewModel model)
    {
        _playerHealthPanel.OnCreated(playerHealth);
        _coinCounterPanel.OnCreated(model);
    }
}

Relationship Inversion

Right now, GameMain initializes GameViewController through a direct reference. As a result, if GameViewController is missing, then initializing the game will fail. However, depending on the situation, you may find that you want something more flexible. Let’s modify the GameViewController to register itself with GameMain rather than the other way around.

public class GameViewController : MonoBehaviour
{
    [SerializeField] HealthPanel _playerHealthPanel;
    [SerializeField] CoinCounterPanel _coinCounterPanel;

    public void Start()
    {
	GameMain.NotifyGameViewControllerWasCreated(this);
    }
    
    public void OnCreated(Health playerHealth, GameViewModel model)
    {
        _playerHealthPanel.OnCreated(playerHealth);
        _coinCounterPanel.OnCreated(model);
    }
}

And here’s a stripped-down version of GameMain to illustrate how this works:

public class GameMain : MonoBehaviour
{
    public static event Action<GameViewController> OnGameViewControllerCreated;

    void Awake()
    {
        OnGameViewControllerCreated += InitializeGameViewController;
    }

    void OnDestroy()
    {
        OnGameViewControllerCreated -= InitializeGameViewController;
    }

    public static void NotifyGameViewControllerWasCreated(GameViewController gameViewController)
    {
        OnGameViewControllerCreated?.Invoke(gameViewController);
    }

    void InitializeGameViewController(GameViewController gameViewController)
    {
        gameViewController.OnCreated(_player.Health, _model);
    }
}

The idea is that when the GameViewController awakes, it’ll notify GameMain who will then initialize it through the OnCreated method. I chose to orchestrate this through static events, but you may have a solution that’s more suited to your game. Now, no matter when GameViewController gets added to the scene, it’ll be initialized correctly. However, it’s worth mentioning that this flexibility leaves room for mistakes because the game won’t throw an error if the GameViewController is missing.

I brought up the idea of inverting the initialization relationship to demonstrate that when designing software, or anything for that matter, every decision has benefits and drawbacks. There’s never a universally perfect design decision; there are only decisions that best suit your given constraints. In this case, we gain flexibility but introduce the possibility of human error.

One last thing before I go, you may be wondering what happened to the View part of MVC. Well, given that the View is in charge of layout, the View is your UI Canvas and all the accompanying UI elements. In my case, I usually make this a child of my View Controller prefab.

If you’re interested, check out the accompanying project on Github. If you join my mailing list, I’ll notify you whenever I post a new article. If you’d like to see more ways to architect events, this time using Scriptable Objects, check out this Devlog from Unity’s Open Projects initiative.

Leave A Comment