I recently saw a well written article about the MVC pattern on reddit and it irked me enough that I answered with some of my concerns, and this is what inspired this post today.

MVC

The MVC pattern is relatively simple in principle : You have a controller that handles input, and store the state in a Model. The views present the information within the Model, and gets warned about changes. Right there, there’s a couple of subtility that might be lost on people :

There is only 1 controller It is the input of the system (thus, the only visible part of it) It stores data to exactly 1 model

There is only 1 model It only stores the data for the current state It warns whoever is listening of changes it did It is read-only from the views, and read/write to the controller

There are many views They read data from the model, and present them Presenting is not necessarily a visual thing.



Model

I find it’s always better when I learn by example, so we’ll study a very simple model

public class PlayerStateModel : IPlayerState { #region listener part private HashSet<IPlayerStateModelListener>_listeners = new HashSet<IPlayerStateModelListener>(); public bool RegisterListener(IPlayerStateModelListener listener) { return _listeners.Add(listener); } public bool UnregisterListener(IPlayerStateModelListener listener) { return _listeners.Remove(listener); } #endregion public PlayerStateModel(int currentHp, int maxHp) { CurrentHp = currentHp; MaxHp = maxHp; } #region Player related operations public int CurrentHp { get { return _currentHp; } private set { _currentHp = value; if (_currentHp < 0) { _currentHp = 0; } if (_currentHp > MaxHp) { _currentHp = MaxHp; } } } public int MaxHp { get; private set; } //Modify the current HP. Won't go over MaxHP. //Will always trigger OnHPModified. //by how much(can be positive or negative) public void ModifyCurrentHp(int amount) { int oldHp = CurrentHp; CurrentHp += amount; //Warn the listeners(this could be within the setter of CurrentHp) foreach (var listener in _listeners) { listener.OnHPModified(oldHp, amount, _currentHp); } } //other stuff like modify MaxHp, etc... #endregion }

Many things here, but it essentially boils down to 3 parts : The initializer (the constructor), the business stuff (the _currentHp, CurrentHp, MaxHp and the function manipulating it), and the listeners. The initializer is pretty much self-explanatory, so let’s skip it and talk about listeners.

Listeners

The listener pattern is used when an object wants to know when another object changes. Since the views almost always want to know when things changes, it’s the perfect pattern to use in this case. They are almost always done in the same fashion : Keep a list (or hashset in this case) of objects to warn, allow them to register/unregister themselves, and warn them when something changes. This code could easily be put in a templated base class (If you don’t know what that is, it’s essentially a way to treat types as “unknown” until we compile, and is very useful when you don’t want to repeat code for different types).

Business Logic

Business logic is a term often used to describe the actual work of something. Often you have the initialization code, you might have defensive code (like preconditions/early return if a parameter is null, or the object is in an invalid state, etc…). The business logic is the grunt work that must happen for the class to fullfil its function. It’s a bit like the nominal case : if everything is ok, this is the part of the code that will get executed. In our case, it’s everything within the “player related operations” region. In this example, the business logic is to hold the current HP/max HP, being able to modify the current HP, but not go above the max or below 0, and then warn whomever listens to us. That is the business logic of this model. Another subtlety that is not mentioned explicitly but is very important is that it needs to keep integrity at all time. It can never be corrupted by anything. In our case, it means being clamped at [0, MaxHp]. Please note that business logic doesn’t mean game logic. Business logic means what the class needs to do to achieve its goal, and its goal is to store data, keep integrity of said data, and warn listeners when data is changed. It doesn’t know if this change is happening during a loading screen, during an invincibility powerup, or anything of the like : It just store data, make sure of its integrity, and warns listener. Again : Stores and ensures the integrity of the data, warn listeners. Nothing else is done by it.

Another thing worth mentionning is the IPlayerStateModel. This simply exposes the model in a readonly fashion. This is the interface that the view will receive.

Controller

The controller is the public entry point of whomever wants to modify the model. It serves as a funnel and gatekeeper for any input into the model. The code will often look a lot like boiler plate, meaning that many times, it will simply have a method that does nothing, except call the same method on its model. That will often seems like tedious work (and it is), but having it act as a funnel is very useful. It will help tremendously when debugging, it’s can act as a gatekeeper when needed (it can prevent changes to the model if needed). Here is a quick example :

public class PlayerStateController //Hard things in programming : Naming thing. Do not confuse this with inputs { private IPlayerStateModel _model; public PlayerStateController(int currentHp, int maxHp) { _model = new PlayerStateModel(currentHp, maxHp); } public bool ModifyHp(int amount) { //Acting as gatekeeper if (amount == 0) { return false; } if (Game.IsNotPlaying) { //Log/throw error return false; } //doesn't know if it heals or not, just forwards it to the _model.ModifyHp(amount); } public KillPlayer() { if (Game.IsNotPlaying) { //Log/throw error return; } _model.ModifyHp(-_model.CurrentHp); } }

Since the controller is the entrypoint, it essentially has the same function has the PlayerStateModel. It could have more functions like the utility KillPlayer() function, but it shouldn’t have less (could, but shouldn’t). A Controller handles only a single model, and there should be only 1 controller per model. Otherwise, there’s not much to say about it.

Views

This is the fun part. Most of the time, when people see View, they think visual, but that’s not necessarily true. In the strict sense, a View is essentially a representation of the data within the Model. A View could be a wrapper around the Model that translate the data into something another system can handle. For instance, you could have a PositionModel that handles positions with the Unity Vector3 type, but have a library that only uses 3 doubles. The view could handle the conversion from the model’s Vector3 into the proper doubles. It’s also very useful in case of multi-threaded environment (I won’t cover here, but if you’re interested, let me know, I might do a post about it). Another subtlety with Views is that there can be more than one. For instance, Diablo has a screen that shows your inventory and equipement, and an avatar representation on screen. In this case, this is 2 view : The inventory screen’s equipement section and the avatar are simply 2 different representation of the same InventoryModel (it’s a simplification, it’s very likely a bit more complex than that, but you get the point). The belt area in could also be an inventory View

A sample view for displaying the current health of the player could look like this :

public class PlayerStateView : Monobehaviour, IPlayerStateModelListener { private IPlayerStateModel _model; //This is a gameobject with a script that handles displaying how much hp the player has, animations, etc.. [SerializeField] private Healthbar _healthbar; //This represents the avatar of the player [SerializeField] private Player _player; //Needs to be called right after creation public void Initialize(IPlayerStateModel model) { if (_model != null) { //log/throw error, we are already initialized } if (model == null) { //log/throw error, this should never happen } _model = model; } void Start() { if (_model == null) { //log/throw error, we need to be initialized } _model.RegisterListener(this); } void OnDestroy() { _model.UnregisterListener(this); } public void OnHPModified(int previousHp, int amount , int currentHp) { if (amount == 0) { return; } if (currentHp == 0) { Destroy(_player.gameObject); Destroy(this.gameObject); Destroy(_healthbar.gameObject); return; } _healthbar.SetCurrentHealth(currentHp); if (amount < 0) { _player.PlayDamageAnim(); } if (amount > 0) { _player.PlayHealAnim(); } } }

There isn’t much to see here. The only thing worth mentioning is that the Healthbar and Player could be separate views that receives the same model. I chose this form for brevity of code to an already long post. You choose what to do with the views at this point.

So there you have it! MVCs aren’t really witchcraft once you understand them : Models store data and broadcast their changes to whatever listens to it, Controllers handle the logic of updating the model when necessary, Views presents the data within a Model.