Simple MVC for Unity

There are a lot of different implementations of MVC design pattern available for Unity, but I’ve noticed one problem with most of them.
They require a lot of knowledge about programming in general, and some of them use some fancy techniques which unnecessarily complicates your code.

And who wants to have hard to read and maintain code? πŸ˜‰

MVC

MVC stands for ModelViewController and is one of most used design pattern in IT, but not necessarily in Game Dev. This design pattern is used to separate your code based on use.

Here we have three parts of this pattern:
Model – these are classes responsible for storing and handling data.
View – these are responsible for handling displaying data and receiving input from the user.
Controller – and these are responsible for the logic behind the app in general. They also handle respond to the input from view, and they are passing data into views.

Implementation

When implementing the MVC design pattern, I’m starting with some general idea of what I want to achieve with it. Mostly I’m separating each stage of the game like Menu, Game or even Game Over as different Controllers. Of course, each of these Controllers might be implemented differently.

For example, I like to use State Machine in Game Controller so I can split logic into Prepare, Loop and Post game phase.

Before we jump into Controllers for game stages, we will need root for our code. This, of course, can also be a Controller! Root Controller!

using UnityEngine;

/// <summary>
/// Root controller responsible for changing game phases with SubControllers.
/// </summary>
public class RootController : MonoBehaviour
{
    // SubControllers types.
    public enum ControllerTypeEnum
    {
        Menu,
        Game,
        GameOver
    }

    // References to the subcontrollers.
    [Header("Controllers")]
    [SerializeField]
    private MenuController menuController;
    [SerializeField]
    private GameController gameController;
    [SerializeField]
    private GameOverController gameOverController;

    /// <summary>
    /// Unity method called on first frame.
    /// </summary>
    private void Start()
    {
        menuController.root = this;
        gameController.root = this;
        gameOverController.root = this;

        ChangeController(ControllerTypeEnum.Menu);
    }

    /// <summary>
    /// Method used by subcontrollers to change game phase.
    /// </summary>
    /// <param name="controller">Controller type.</param>
    public void ChangeController(ControllerTypeEnum controller)
    {
        // Reseting subcontrollers.
        DisengageControllers();

        // Enabling subcontroller based on type.
        switch (controller)
        {
            case ControllerTypeEnum.Menu:
                menuController.EngageController();
                break;
            case ControllerTypeEnum.Game:
                gameController.EngageController();
                break;
            case ControllerTypeEnum.GameOver:
                gameOverController.EngageController();
                break;
            default:
                break;
        }
    }

    /// <summary>
    /// Method used to disable all attached subcontrollers.
    /// </summary>
    public void DisengageControllers()
    {
        menuController.DisengageController();
        gameController.DisengageController();
        gameOverController.DisengageController();
    }
}

You can already notice that Root Controller has references to other controllers. These controllers will take control over our game depending on where our user is in the app.

So now each of them will have a reference to UI so we should prepare a base class which will contain just that! πŸ€“

using UnityEngine;

/// <summary>
/// Base class for SubControllers with reference to Root Controller.
/// </summary>
public abstract class SubController : MonoBehaviour
{
    [HideInInspector]
    public RootController root;

    /// <summary>
    /// Method used to engage controller.
    /// </summary>
    public virtual void EngageController()
    {
        gameObject.SetActive(true);
    }

    /// <summary>
    /// Method used to disengage controller.
    /// </summary>
    public virtual void DisengageController()
    {
        gameObject.SetActive(false);
    }
}

/// <summary>
/// Extending SubController class with generic reference UI Root.
/// </summary>
public abstract class SubController<T> : SubController where T : UIRoot
{
    [SerializeField]
    protected T ui;
    public T UI => ui;

    public override void EngageController()
    {
        base.EngageController();

        ui.ShowRoot();
    }

    public override void DisengageController()
    {
        base.DisengageController();

        ui.HideRoot();
    }
}

You can notice that I made UI reference generic, and this is because I want for each Controller to have individual UI Root. This also requires a little base class.

using UnityEngine;

/// <summary>
/// Base class for UI roots for different controllers.
/// </summary>
public class UIRoot : MonoBehaviour
{
    /// <summary>
    /// Method used to show UI.
    /// </summary>
    public virtual void ShowRoot()
    {
        gameObject.SetActive(true);
    }

    /// <summary>
    /// Method used to hide UI.
    /// </summary>
    public virtual void HideRoot()
    {
        gameObject.SetActive(false);
    }
}

So you might wonder where our models are?

Good that you’ve asked! They don’t exist! At least, not at this point… πŸ˜…
But if you want to have a convenient way to store and access data you can use DataStorage example from Singleton post. πŸ˜‰

Example

Now as we have our base ready, we should start implementing an example of it! And to make clear how I’m going to structure my hierarchy in the scene, here is a picture.

As we will have just a simple data model for game data we can start with that.

using UnityEngine;

/// <summary>
/// Game data model.
/// </summary>
public class GameData
{
    // Game score
    public int gameScore = 0;

    // Game time
    public float gameTime = 0;
}

Cool, now onto Menu!

using UnityEngine;

/// <summary>
/// Controller responsible for menu phase.
/// </summary>
public class MenuController : SubController<UIMenuRoot>
{
    public override void EngageController()
    {
        // Attaching UI events.
        ui.MenuView.OnPlayClicked += StartGame;
        ui.MenuView.OnQuitClicked += QuitGame;

        base.EngageController();
    }

    public override void DisengageController()
    {
        base.DisengageController();

        // Detaching UI events.
        ui.MenuView.OnQuitClicked -= QuitGame;
        ui.MenuView.OnPlayClicked -= StartGame;
    }

    /// <summary>
    /// Handling UI Start Button Click.
    /// </summary>
    private void StartGame()
    {
        // Changing controller to Game Controller.
        root.ChangeController(RootController.ControllerTypeEnum.Game);
    }

    /// <summary>
    /// Handling UI Quit Button Click.
    /// </summary>
    private void QuitGame()
    {
        // Closing the game.
        Application.Quit();
    }
}
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Menu view with events for buttons.
/// </summary>
public class UIMenuView : UIView
{
    // Event called when Play Button is clicked.
    public UnityAction OnPlayClicked;

    /// <summary>
    /// Method called by Play Button.
    /// </summary>
    public void PlayClicked()
    {
        OnPlayClicked?.Invoke();
    }

    // Event called when Quit Button is clicked.
    public UnityAction OnQuitClicked;

    /// <summary>
    /// Method called by Quit Button.
    /// </summary>
    public void QuitClicked()
    {
        OnQuitClicked?.Invoke();
    }
}

Next step will be Game! But I’m not going to implement actual game here… πŸ˜…

using UnityEngine;

/// <summary>
/// Controller responsible for game phase.
/// </summary>
public class GameController : SubController<UIGameRoot>
{
    // Reference to current game data.
    private GameData gameData;

    public override void EngageController()
    {
        // New game need fresh data.
        gameData = new GameData();

        // Attaching UI events.
        ui.GameView.OnFinishClicked += FinishGame;
        ui.GameView.OnMenuClicked += GoToMenu;

        // Restarting game time.
        ui.GameView.UpdateTime(0);

        base.EngageController();
    }

    public override void DisengageController()
    {
        base.DisengageController();

        // Detaching UI events.
        ui.GameView.OnMenuClicked -= GoToMenu;
        ui.GameView.OnFinishClicked -= FinishGame;
    }

    /// <summary>
    /// Unity method called each frame as game object is enabled.
    /// </summary>
    private void Update()
    {
        // Increasing time value.
        gameData.gameTime += Time.deltaTime;
        // Displaying current game time.
        ui.GameView.UpdateTime(gameData.gameTime);
    }

    /// <summary>
    /// Handling UI Finish Button Click.
    /// </summary>
    private void FinishGame()
    {
        // Assigning random score.
        gameData.gameScore = Mathf.CeilToInt(gameData.gameTime * Random.Range(0.0f, 10.0f));
        // Saving GameData in DataStorage.
        DataStorage.Instance.SaveData(Keys.GAME_DATA_KEY, gameData);

        // Chaning controller to Game Over Controller
        root.ChangeController(RootController.ControllerTypeEnum.GameOver);
    }

    /// <summary>
    /// Handling UI Menu Button Click.
    /// </summary>
    private void GoToMenu()
    {
        // Changing controller to Menu Controller.
        root.ChangeController(RootController.ControllerTypeEnum.Menu);
    }
}
using UnityEngine;
using UnityEngine.Events;
using TMPro;

/// <summary>
/// Game view with events for buttons and showing data.
/// </summary>
public class UIGameView : UIView
{
    // Reference to time label.
    [SerializeField]
    private TextMeshProUGUI timeLabel;

    // Event called when Finish Button is clicked.
    public UnityAction OnFinishClicked;

    /// <summary>
    /// Method called by Finish Button.
    /// </summary>
    public void FinishClick()
    {
        OnFinishClicked?.Invoke();
    }

    // Event called when Menu Button is clicked.
    public UnityAction OnMenuClicked;

    /// <summary>
    /// Method called by Menu Button.
    /// </summary>
    public void MenuClicked()
    {
        OnMenuClicked?.Invoke();
    }

    /// <summary>
    /// Method used to update time label.
    /// </summary>
    /// <param name="time">Game time.</param>
    public void UpdateTime(float time)
    {
        timeLabel.text = string.Format("{0:#00}:{1:00.000}", (int)(time / 60), (time % 60));
    }
}

You can notice that we are creating a new instance of the game data model and we are filling it with data.

So let’s make use of that data with Game Over phase! 🏁

using UnityEngine;

/// <summary>
/// Controller responsible for game over phase.
/// </summary>
public class GameOverController : SubController<UIGameOverRoot>
{
    // Reference to current game data.
    private GameData gameData;

    public override void EngageController()
    {
        // Getting game data from data storage.
        gameData = DataStorage.Instance.GetData<GameData>(Keys.GAME_DATA_KEY);
        // Removing game data from data storage as it is no longer needed there.
        DataStorage.Instance.RemoveData(Keys.GAME_DATA_KEY);

        // Attaching UI events.
        ui.GameOverView.OnReplayClicked += ReplayGame;
        ui.GameOverView.OnMenuClicked += GoToMenu;

        // Showing game data in UI.
        ui.GameOverView.ShowScore(gameData);

        base.EngageController();
    }

    public override void DisengageController()
    {
        base.DisengageController();

        // Detaching UI events.
        ui.GameOverView.OnMenuClicked -= GoToMenu;
        ui.GameOverView.OnReplayClicked -= ReplayGame;
    }

    /// <summary>
    /// Handling UI Replay Button Click.
    /// </summary>
    private void ReplayGame()
    {
        // Changing controller to Game Controller.
        root.ChangeController(RootController.ControllerTypeEnum.Game);
    }

    /// <summary>
    /// Handling UI Menu Button Click.
    /// </summary>
    private void GoToMenu()
    {
        // Changing controller to Menu Controller.
        root.ChangeController(RootController.ControllerTypeEnum.Menu);
    }
}
using UnityEngine;
using UnityEngine.Events;
using TMPro;

/// <summary>
/// Game over view with events for buttons and showing data.
/// </summary>
public class UIGameOverView : UIView
{
    // Reference to score label.
    [SerializeField]
    private TextMeshProUGUI scoreLabel;

    // Reference to time label.
    [SerializeField]
    private TextMeshProUGUI timeLabel;

    // Event called when Replay Button is clicked.
    public UnityAction OnReplayClicked;

    /// <summary>
    /// Method called by Replay Button.
    /// </summary>
    public void ReplayClick()
    {
        OnReplayClicked?.Invoke();
    }

    // Event called when Menu Button is clicked.
    public UnityAction OnMenuClicked;

    /// <summary>
    /// Method called by Menu Button.
    /// </summary>
    public void MenuClicked()
    {
        OnMenuClicked?.Invoke();
    }

    /// <summary>
    /// Method used to show game data in UI.
    /// </summary>
    /// <param name="gameData">Game data.</param>
    public void ShowScore(GameData gameData)
    {
        scoreLabel.text = gameData.gameScore.ToString("N0");
        timeLabel.text = string.Format("{0:###0}:{1:00.000}", (int)(gameData.gameTime / 60), (gameData.gameTime % 60));
    }
}

At this point, these controllers are switching with each other. They also pass game data between Game and Game Over phase using DataStorage from one of the previous posts about Singletons.

The end result should look like that:

Great! You have done it! MVC doesn’t have any more secrets for you! πŸ™Œ

This example is available in my public repository here. πŸ”—

Share it if you liked it and hope to see you here next time! πŸ”₯

5 4 votes
Article Rating
Subscribe
Notify of
guest
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
osman
osman
1 year ago

Awesome !!

Anjum
Anjum
3 months ago

How about sub menus? For example, if menuView has sub views as well i.e. SoundMenu, GraphicsMenu etc. Then how will the structure look like?