Last year I introduced a Unity-based model-view-controller (MVC) design pattern and in the many comments on that article, a theme arose. The “model” part of MVC is arguably not necessary since Unity stores so much of the data itself. Today’s article takes that theme and elaborates on it to create and introduce a new Unity-specific design pattern. Read on to see how this adaptation of MVC works!

The MVC pattern described in the last article looks like this:

MVC Diagram

The view is a MonoBehaviour whose responsibilities are input (e.g. buttons, touch) and output (e.g. graphics, sound). Input is exposed by events. Output is by functions. The view performs no logic such as AI or scoring points.

The model is not a MonoBehaviour. It’s responsible for storing the data representation of the object, such as position and health. It allows access to this data by properties and dispatches events when the data changes. It also performs most of the logic, hence the phrase “fat models, skinny controllers.”

That leads us to the controller, which is also not a MonoBehaviour. It’s the middle-man between the model and the view. It listens for input from the view and updates the model accordingly. It listens for data changes from the model and outputs using the view.

Some readers of this pattern found it strange that the model would store data such as an enemy’s position because Unity is already storing this in the game object’s Transform. Why store it twice and go through the trouble of synchronizing it between the view and the model? That’s a fair critique of the MVC pattern as described in the last article. It’s also the reason for today’s new design.

Here’s how the MV-C pattern looks:

MV-C Diagram

The model and the view have merged into a model-view, hence the name “MV-C”. It’s a MonoBehaviour that handles input and output as well as the data representation of the object. Unlike the models of MVC, it doesn’t perform any logic.

The controller now listens for input from the model-view and updates the model-view accordingly. This may seem strange, but the controller is now performing the logic that should occur due to that input. So it’s not just passing through the input for the model to handle it anymore, but instead handling the input itself. Likewise, it listens for data changes from the model-view, performs logic on those changes, and outputs using the model-view.

Now let’s see this pattern in action. Again we’ll implement a simple enemy, but with a couple of changes. First up is IEnemyModelView, the interface that the controller uses to access the model-view, and some associated event classes:

using System;
 
using UnityEngine;
 
// Interface for the model-view
public interface IEnemyModelView
{
	// Dispatched when the health changes
	event EventHandler<EnemyHealthChangedEventArgs> OnHealthChanged;
 
	// Dispatched when the position changes
	event EventHandler<EnemyPositionChangedEventArgs> OnPositionChanged;
 
	// Dispatched when the enemy is clicked
	event EventHandler<EnemyClickedEventArgs> OnClicked;
 
	// Position of the enemy
	Vector3 Position { get; set; }
 
	// Health of the enemy
	float Health { get; set; }
 
	// Destroy the enemy
	void Destroy();
}
 
public class EnemyClickedEventArgs : EventArgs
{
}
 
public class EnemyHealthChangedEventArgs : EventArgs
{
	public float Health { get; private set; }
 
	public EnemyHealthChangedEventArgs(float health)
	{
		Health = health;
	}
}
 
public class EnemyPositionChangedEventArgs : EventArgs
{
	public Vector3 Position { get; protected set; }
 
	public EnemyPositionChangedEventArgs(Vector3 position)
	{
		Position = position;
	}
}

Remember that the three parts of the model-view are input, output, and data representation. Events like OnClicked are for input. Setters like the one the Position property has are for output. Properties like Health are for the data representation.

Now let’s see how the model-view is implemented as a MonoBehaviour:

using System;
 
using UnityEngine;
 
// The combined model (data representation) and view (input and output) for an enemy. Does no logic.
public class EnemyModelView : MonoBehaviour, IEnemyModelView
{
	// Data events
	public event EventHandler<EnemyHealthChangedEventArgs> OnHealthChanged = (s, e) => {};
	public event EventHandler<EnemyPositionChangedEventArgs> OnPositionChanged = (s, e) => {};
 
	// Input events
	public event EventHandler<EnemyClickedEventArgs> OnClicked = (s, e) => {};
 
	// Data stored by the model-view
	// Outputs the data by mapping it to the material color
	private float health;
	public float Health
	{
		get { return health; }
		set
		{
			if (value != health)
			{
				health = value;
				GetComponent<Renderer>().material.color = Color.Lerp(Color.red, Color.green, value);
				OnHealthChanged(this, new EnemyHealthChangedEventArgs(health));
			}
		}
	}
 
	// Wrapper for data already stored by Unity
	// Outputs the data by direct pass-through: no logic
	public Vector3 Position
	{
		get { return transform.position; }
		set
		{
			if (value != transform.position)
			{
				transform.position = value;
				OnPositionChanged(this, new EnemyPositionChangedEventArgs(value));
			}
		}
	}
 
	// Default values can be set where appropriate
	void Awake()
	{
		Health = 1;
	}
 
	// Handle input by dispatching an event, not performing logic
	void Update()
	{
		if (Input.GetMouseButtonDown(0))
		{
			var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
			RaycastHit hit;
			if (Physics.Raycast(ray, out hit) && hit.transform == transform)
			{
				OnClicked(this, new EnemyClickedEventArgs());
			}
		}
	}
 
	// Destroy the enemy, but we don't know why
	public void Destroy()
	{
		Destroy(gameObject);
	}
}

Everything here is a very straightforward implementation. The model-view takes care of details, such as how to know when the player clicks on the enemy and how to render health. You can optionally move the code that maps health to a color from the model-view to the controller and add a set Color property to the model-view for the controller to call. Some will prefer to keep all the logic in the controller while others will prefer to keep rendering-specific code in the view. There are pros and cons to both approaches, so the above code shows how you’d do it in the model-view.

One thing to notice here is that the model-view doesn’t have an explicit reference to the controller. It doesn’t know which class is taking care of the logic. It just knows that input happened so it should dispatch an event, the output functions and set properties got called so it should render something, and which data makes up an enemy. That includes data that Unity is already storing, like transform.position. Most important is that absolutely no logic appears in this class except input handling and rendering.

Next up is the EnemyController:

// The logic for an enemy. Relies on a model-view to store the data representation, gather input,
// and output to the user.
public class EnemyController
{
	private readonly IEnemyModelView modelView;
	private float clickDamage;
 
	public EnemyController(IEnemyModelView modelView, float clickDamage)
	{
		this.modelView = modelView;
		this.clickDamage = clickDamage;
 
		// Listen to input from the model-view
		modelView.OnClicked += HandleClicked;
 
		// Listen to changes in the model-view's data
		modelView.OnHealthChanged += HandleHealthChanged;
	}
 
	private void HandleClicked(object sender, EnemyClickedEventArgs e)
	{
		// Perform logic as a result of this input
		// Here it's just a simple calculation to damage the enemy by reducing its health
		// Update the model-view's data representation accordingly if this is the case
		modelView.Health -= clickDamage;
	}
 
	private void HandleHealthChanged(object sender, EnemyHealthChangedEventArgs e)
	{
		// Perform logic as a result of this data change
		// Here it's just a simple rule that an enemy with no health is dead and gets destroyed
		// Output to the model-view accordingly if this is the case
		if (e.Health <= 0)
		{
			modelView.Destroy();
		}
	}
}

Notice that the controller isn’t a MonoBehaviour, nor does it reference one. It uses the model-view via the IEnemyModelView interface. We’ll see why this is important soon.

The next thing to notice is that the controller doesn’t store any of the data for the enemy: health or position. It uses the model-view for that. It also uses the model-view’s events to listen for input and changes in the data representation. It performs all the logic of (very simple) damage calculation and enforces the game rule that when an enemy runs out of health it dies. It uses the model-view to output the results of the damage calculations and to make the enemy die.

Now let’s see some code that uses MV-C:

using System;
 
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	void Awake()
	{
		// Create the model-view
		var prefab = Resources.Load<GameObject>("Enemy");
		var instance = Instantiate(prefab);
		var modelView = instance.GetComponent<IEnemyModelView>();
		modelView.Position = new Vector3(1, 2, 3);
 
		// Create the controller
		// No need to keep a reference because the model-view already has at least one
		new EnemyController(modelView, 0.25f);
	}
}

In this case the model-view is just a MonoBehaviour on a prefab. I used Resources.Load, but you could get it out of the scene just as easily. To hook up the controller you just need a one-liner to instantiate it and give it a reference to the model-view. You don’t even need to keep a reference to it since the model-view implicitly has references to it via event handlers that the controller adds in its constructor. When the game object is destroyed, both the model-view and the controller will be garbage-collected.

That’s it for the runtime code! However, one big advantage of this approach is that it makes it very easy to unit test all of your logic. All the logic is in the controller, so you really just need to make a fake model-view class implementing the model-view’s interface and give it to your controller. NSubstitute, which comes with Unity Test Tools, includes that along with the NUnit testing framework.

Let’s look at just a couple of tests for the very simple enemy controller above:

using NSubstitute;
using NUnit.Framework;
 
#pragma warning disable 0168, 0219 // unused variables
 
// Class with tests
[TestFixture]
public class TestEnemyController
{
	// A single test
	[Test]
	public void ReducesHealthByClickDamageWhenClicked()
	{
		// Make a fake model-view and give it to a real controller
		var modelView = Substitute.For<IEnemyModelView>();
		modelView.Health = 1;
		var controller = new EnemyController(modelView, 0.25f);
 
		// Fake the OnClicked event
		modelView.OnClicked += Raise.EventWith(new EnemyClickedEventArgs());
 
		// Make sure the controller damaged the enemy
		Assert.That(modelView.Health, Is.EqualTo(0.75f).Within(0.001f));
	}
 
	// Three tests in one. Specify parameters for each run of this function.
	[TestCase(0.1f,  0)]
	[TestCase(0,     1)]
	[TestCase(-0.1f, 1)]
	public void OnlyDestroysWhenHealthChangesToLessThanOrEqualToZero(
		float health,
		int numDestroyCalls
	)
	{
		// Make a fake model-view and give it to a real controller
		var modelView = Substitute.For<IEnemyModelView>();
		modelView.Health = 1;
		var controller = new EnemyController(modelView, 0.25f);
 
		// Fake the OnHealthChanged event
		modelView.OnHealthChanged += Raise.EventWith(new EnemyHealthChangedEventArgs(health));
 
		// Make sure the controller called Destroy() the right number of times
		modelView.Received(numDestroyCalls).Destroy();
	}
}

If you want to try out this example, including the unit tests, check out the GitHub project.

That wraps up today’s introduction of the MV-C design pattern. What do you think of it? Let me know in the comments!