MVC for Unity: Evolving it to AMMVC
Preface
In this article, I am going to share my experience with MVC pattern and also Unity Game Engine.
Before starting the article I want to introduce myself. I am a game developer and currently working in the mobile game industry. My company is creating puzzle-casual games.
Because of the size of the code-base and the project lifetime, our systems have to be well-written, testable, scalable, and portable. Just like other companies in the industry we have to be agile and innovators. Because of that, we tried many of those patterns and ideologies.
But today, our subject is MVC. Firstly I am going to cover “What is MVC?”, then I am going to expand the pattern with my experience. Which, you can use the advantages of unity, and also you can test it easily on the Unity unit test engine. Even you can go for TDD if you desire.
Attribution
Before then start, I want to thank Eduardo Dias da Costa for his great article about MVC. The article is well written and inspireful. I recommend to everyone to check it out.
I am going to go on the same simple game called “10 Bounces” and I will develop it with new additional systems. So, we can see how my AMVCM* pattern handles the scaling, and how easily systems can communicate with each other. And most importantly how are the systems going to stay independent.
*I will go into detail later in the article.
What is MVC?
MVC (Model, View, Controller) pattern is about to split the code into three significant parts. With this approach, you will get a single responsible, easy-to-manage, and easy-to-test system. Before diving deep, let’s describe those three components.
Model
The model class will contain data on the system. Traditionally model class keeps simple with contains just fields, properties, and constructors. Data manipulator methods write on the controller class.
View
The view class will handle the input-output part of our system. It will get input from the player and transfer it to the controller class or it will show output to the player on UI.
Controller
The controller is the logical layer of the system. You can assume it as the heart of the system or manager of the system. Because of the end of the day, the controller will interpret the input, manipulate the model, send events (internal or external) to other classes, and when you want to write unit tests most of the time you are going to write tests for the controller.
Structure of the MVC
The model itself will remain independent from the system. The controller will depend on the model but the view can’t reach the model. The only way to access the data on the model is to get it from the controller. In this way, we can format the data or encapsulate it to protect it from corruption. Because of this approach, the view will be dependent on the controller. However, the controller will send data to the view through events. With that approach, the controller will stay independent from the view components. In our projects, what kind of view or how many views are we using on the current state will not matter for the system. Even on this point, can you sense the potential of the abstraction?
Adapting MVC to the Unity Environment
While we are developing on Unity, we already have some prebuilt and helpful structures such as MonoBehaviour and ScriptableObject.
MonoBehaviour helps write classes as components and attach them to the game objects. We will use from MonoBehaviour extended classes for Controller and View.
ScriptableObject will allow us to serialize classes and project-wide accessibility during the game is running. And also ScriptableObjects are very useful for creating variants of a specific class with different datasets. For example weapons (sword, bow, staff…). We will use ScriptableObject for model classes.
Implementing of Application and Monitor Layer
MVC pattern is great on its own. But when you want to scale it up and create more than one MVC, how to connect them without creating dependencies will be a problem.
At this point Application layer will help us connect our systems and transmit data through events. Details will be in the latter part of the article but for now, you can keep this in your mind: the application layer is the key to scalability and portability.
Another additional layer is the Monitor layer. The monitor layer is an option. You don’t have to implement it in your project. Without a monitor, your project will work without any lack. But, still, the monitor is very helpful if you don’t have any UI element to test your system in run mode. You can create a monitor class for observing the data served by the controller and also you can send pseudo inputs to the controller. At the publishing phase of your product, you can use monitor classes for feeding your test UI’s (like test panel) or simply disable it. In this article, I’m going to create a monitor class too.
10 Bounces
“10 Bounces” is a simple game project. We have a platform and a ball. The ball is bouncing on its own on the platform because of the ball’s bouncy physic material. When it completes ten bounces, you win! That’s it.
Later in the article, you can find some source code for the project. But still, the article may not contain every detail of the project. For further information, I suggest you download the project. For installing the project from GitHub, click here.
Even in this simple project, we can talk about a few different systems.
We need a game system for tracking the game state.
We have to detect the bounces of the ball. Let’s call it a bounce system.
Let’s start by creating the structure of the project.
Hierarchy
As you see I created a root game object and name it “APPLICATION” and organized every game object under three branches. Monitors, controllers, and views. If you’re careful enough, you’ve noticed that I did not create a branch from the model layer. That’s because we don’t extend our model classes from MonoBehaviour.
And also I organized my scripts as in the picture above. Organizing the project hierarchy is just as important as organizing the scene hierarchy.
For a bigger project, you can have more than one view and model. In this case, you create a subfolder under Bounce and Game folder. Such as:
Scripts/Game/Views/…
Scripts/Bounce/Models/…
Application
As I mentioned before, the application layer is our top-level component.
public class Application: MonoBehaviour
{
public event Action<string, Object[]> OnGameNotificationSent;
public event Action<string, Object[]> OnBounceNotificationSent;
public void Notify(string notificationString, params Object[] data)
{
if (GameNotification.Contains(notificationString))
{
OnGameNotificationSent?.Invoke(notificationString, data);
}
else if (BounceNotification.Contains(notificationString))
{
OnBounceNotificationSent?.Invoke(notificationString, data);
}
}
}
As you see, the application is using events for sending data through systems. Your system can subscribe to these events if necessary and take action according to the information received. When you want to append a new system to your application only thing you have to do is add new events and notifications to your application class but not change it. “Open-Close”, right?. But, what are the notifications?
Notifications
Notifications are very familiar with enums. I used it instead of enums because of easier to remove and expending with different filters when you need to. But still, you can use enums or string enum pattern if you desire.
public static class GameNotification
{
public const string _Win = "game_win";
public const string _Lose = "game_lose";
public const string _Start = "game_start";
public static bool Contains(string notificationString)
{
return notificationString == _Win
|| notificationString == _Lose
|| notificationString == _Start;
}
}
We are using them for specifying the events. With them, systems can understand and route them through to types like a win, lose or start.
ElementOf
Element is a very simple abstract class that exist for access to higher element on the structure hierarchy. For a controller class, the master is the application. For a view or a monitor, the master is its controller class.
public abstract class ElementOf<T> : MonoBehaviour where T : MonoBehaviour
{
protected T Master
{
get
{
if (_master == null)
{
_master = FindObjectOfType<T>();
}
if (_master == null)
{
var obj = new GameObject();
obj.name = typeof(T).Name;
_master = obj.AddComponent<T>();
}
return _master;
}
}
private T _master;
}
Controller classes will extend from the element for sending and getting notifications. View, model, or monitor classes won’t access the application directly on my structure. In my experience, in this way debugging and understanding the flowing of the program will be much much easier. Stick to the hierarchy!
GameController
GameController is responsible for tracking the state of the game. And notify others when the winning condition is achieved.
public class GameController : ElementOf<Application>
{
private GameModel _model;
[SerializeField] private int winCondition;
private void Awake()
{
_model = new GameModel(winCondition);
Master.OnBounceNotificationSent += HandleBounceNotification;
}
private void HandleBounceNotification(string notificationString, Object[] payload)
{
if (notificationString == BounceNotification._BallHitGround)
{
HandleBallHitToGround(payload);
}
}
private void HandleBallHitToGround(Object[] payload)
{
var bounces = ((BounceModel) payload[0]).bounces;
var isWin = CheckForWinCondition(bounces);
if (isWin)
GameWin();
}
private void GameWin()
{
Master.Notify(GameNotification._Win);
}
private bool CheckForWinCondition(int bounces)
{
return bounces >= _model.WinCondition;
}
}
BounceController
BounceController is responsible for getting bounce input from the ball and increasing the bounce data.
public class BounceController : ElementOf<Application>
{
public BounceModel Model { get; private set; }
public event Action OnGameWin;
public event Action<BounceModel> OnBallHitToGround;
private void Awake()
{
Model = new BounceModel();
Master.OnGameNotificationSent += HandleGameNotificationSent;
Master.OnBounceNotificationSent += HandleBounceNotificationSent;
}
private void OnDestroy()
{
Master.OnGameNotificationSent -= HandleGameNotificationSent;
Master.OnBounceNotificationSent -= HandleBounceNotificationSent;
}
private void HandleBounceNotificationSent(string notificationString, Object[] payload)
{
if (notificationString == BounceNotification._BallHitGround)
{
OnBallHitToGround?.Invoke((BounceModel) payload[0]);
}
}
private void HandleGameNotificationSent(string notificationString, Object[] payload)
{
if (notificationString == GameNotification._Win)
{
OnGameWin?.Invoke();
}
}
public void IncreaseBounces()
{
Model.Bounces++;
Master.Notify(BounceNotification._BallHitGround, Model);
}
}
GameModel and BounceModel
Because of the comparison differences between these two models, I will explain them under one title.
public class GameModel
{
public readonly int WinCondition;
public GameModel(int winCondition)
{
WinCondition = winCondition;
}
}public class BounceModel: Object
{
public int Bounces;
}
As you see both models have only one field. Even, though GameModel will take its field value from the constructor and contains it as read-only, It does not much matter. The important part is that BounceModel is extending from Object but GameModel is not. If you remember, our application can send payload data with events. For sending BounceModel as payload, it should extend from Object. Until now, I don’t have to send GameModel as a payload. Therefore, I don’t have to extend it.
BallView
BallView is an input type of view component. It takes place under the ball game object and detects collisions.
public class BallView : ElementOf<BounceController>
{
private void OnCollisionEnter(Collision collision)
{
Master.IncreaseBounces();
}
}
The ball game object has a collider and rigidbody component.
Because of it, BallView can use the OnCollisionEnter method and detect collisions with other colliders then calls the controller’s IncreaseBounces method.
ScoreView
ScoreView is an output type of view component. As you see view components can handle inputs and outputs. In our case, they are separated. Still, they can handle input and output jobs under one class like popups.
public class ScoreView : ElementOf<BounceController>
{
[SerializeField] private TMP_Text scoreText;
private void Start()
{
Master.OnBallHitToGround += SetScoreText;
SetScoreText(Master.Model);
}
private void OnDestroy()
{
Master.OnBallHitToGround -= SetScoreText;
}
private void SetScoreText(BounceModel model)
{
scoreText.text = model.Bounces.ToString();
}
}
Tracking bounce amount and updating the UI when it’s updated is very simple with events. The only thing you have to do is subscribe to the required event. That’s all.
BounceMonitor
As I mentioned before, when you don’t have the publish-ready prefabs, you have to work with placeholder ones. I mean until now. If you write a monitor class, you don’t have to work with tiresome placeholder prefabs.
public class BounceMonitor : ElementOf<BounceController>
{
[SerializeField, ReadOnly] private int bounces;
private void Start()
{
bounces = Master.Model.Bounces;
Master.OnBallHitToGround += HandleBallHitToGround;
}
private void OnDestroy()
{
Master.OnBallHitToGround -= HandleBallHitToGround;
}
private void HandleBallHitToGround(BounceModel model)
{
bounces = model.Bounces;
}
[Button]
private void AddBounce()
{
Master.IncreaseBounces();
}
}
As you see, with a basic class you can see outputs on the inspector and can send inputs to the system. Monitors behave like a view. As an extra, you can write test methods and test the system through inspector on running mode.
By the way, I am using Sirenix Odin for handling the editor part of the code. You can write an editor class for BounceMonitor and get the same result.
Porting the Project with Full of the Systems or Several of the Systems
When you want to export some of your systems (or all of your systems) from the current project need to do’s are very simple.
For example, I want to export the BounceSystem but I don’t want to export the GameSystem. That’s ok. Because all systems are independent of each other and the application is independent of all the systems, all you need to do is tick the elements of the systems you want to export from the project.
Conclusion
In this article, I tried to show the MVC pattern’s potential. And how to break the limits with Unity. In the game industry, most of the time we have to deal with many systems simultaneously. Most of them are dependent on each other. Hard to understand what is going on and why is going on. Even with well-organized documents, understanding the data flow through codes could take hours. Using the application layer on top of the MVC pattern can be very helpful for clarifying your project. Because the concept of the systems is similar to each other. A new developer who just joined the team can adapt easily.
That’s all for this article. Thanks for the reading.