MVC Pattern in Unity – Less Code Dependency and Better Code Architecture
MVC, short for Model-View-Controller, is a pattern that has gained widespread adoption in web and app design. It focuses on separating the internal representations of information to enhance project organization. This concept can also be applied in game development to cultivate a more organized programming habit.
For game or Unity development, we may have different names for each module but they are pretty much the same concepts:
- Model – Data, Database, Item, or any data wrapper classes.
- View – UI, Windows, Panel, or any classes that control UI representation and listen to the player inputs.
- Controller – Manager, Controller, or any logic controlling classes.
Why MVC?
I have come across many instances of “spaghetti code” in Unity game projects developed by junior developers. One common example is a script that controls data, UI, and logic, all within a single file spanning over 500 lines. It becomes extremely difficult to read, and locating and debugging specific functionalities in such scripts turns into a disaster. Hence, it is crucial to maintain organized projects with minimal code dependencies, especially when working in a team. By adopting concepts from the MVC pattern, we can create a similar structure that promotes better code architecture.
MVC offers several advantages:
- Dependency Reduction: Splitting the view from the controller has the greatest benefit of reducing dependency and the chance of missing reference errors. For instance, breaking anything in the UI wouldn’t affect the main controller logic.
- Efficient Integration Tests: We can build the integration test with a controller mockup that disregards the UI and player input, as the UI is separated from the controller.
- Handling Multiple UI Pages: In some cases, a game may have multiple UIs for a single functionality. For example, there could be two different card collection screens: one accessed from the menu and another from in-game. The controller should be able to add or update specific UI without causing interference.
- Minimized Merge Conflicts: For example, while a technical artist is updating a UI script and assigning its references in Unity, a developer can work independently on controller scripts.
- Enhanced Readability: For example, code related to character control can be found in the “Player” module, while code for enemy AI can be found in the “Enemy” module.
We discussed how MVC can help organize game projects more effectively. However, every developer may have their own approach to implementing it. In this article, we will provide a detailed explanation of how we adapt to MVC, using examples for better understanding.
Model – Data Management
In game development, there are normally two types of data:
- Pre-set values – either from the online database, a local CSV file, or Unity’s
ScriptableObject
asset file. E.g. player HP at each level, weapon base damages, enemy skills, etc. - In-game values – player progress data, stored in
PlayerPrefs
or any storing solution. E.g. player level, weapon level, enemy status, etc. Sometime used with its pre-set values but not neccessary.
No matter how the raw data is stored, we would always need the wrapper classes for easy access, this comes to the Model module.
To identify different data types, we use different naming:
- Pre-set data – XxxxData
- In-game data – XxxxItem
For example, a weapon Model can be:
public class WeaponData
{
public int baseDamage;
public int damagePerLevel;
...
}
public class WeaponItem
{
public int level;
public WeaponData data;
...
public int Damage => baseDamage + level * data.damagePerLevel;
...
public event UnityAction levelUpdated;
}
In this example, weapon has both preset values baseDamage
and damagePerLevel
, then the level
of the weapon is stored in the player progress, so the in-game resulting damage is calculated from both pre-set and in-game values.
Note that sometime we need a XxxxData class or XxxxItem class only, depends on the functionality we need.
View – UI Control
In the View module, in addiction controlling the UI appearance, it also includes the player input such as button click.
The main concept of MVC in Unity is to separate UI logic and elements from the controller script. In another word, the controller or master script should NOT make any direct call to interact with UI. In this case, callbacks are our friends.
Again from an example of weapon UI:
public class WeaponPanel : MonoBeahviour
{
[SerializedField] private WeaponSlot _slotPrefab;
...
public void Awake()
{
WeaponManager.panelShown += Show();
Hide();
foreach (WeaponItem weaponItem in WeaponManager.weaponItems) {
GameObject weaponSlotObject = Instantiate(_slotPrefab, transform);
weaponSlotObject.Set(weaponItem);
}
}
...
}
public class WeaponSlot : MonoBeahviour
{
[SerializedField] private Text _levelText;
[SerializedField] private Text _damageText;
private WeaponItem _weaponItem;
...
public void Set(WeaponItem weaponItem)
{
this._weaponItem = weaponItem;
_levelText.text = weaponItem.level;
_damageText.text = weaponItem.Damage;
_weaponItem.levelUpdated += Set(_weaponItem);
}
public void OnLevelUpClick()
{
WeaponManager.LevelUp(_weaponItem);
}
...
}
WeaponPanel
instantiate a list of WeaponSlot
, and listen to panelShown
event in WeaponManager
to be shown.
WeaponSlot
simply set the UI elements with the values in WeaponItem
, and listen to levelUpdated
event in WeaponItem
. Finally, it calls the LevelUp()
method from a player click.
Note that we don’t do any weapon level up logic in UI, but keep it to the controller script and update the UI elements when the level up succeeds thought callback.
We can also see the advantages of MVC here, in case the _levelText
object is removed accidentally in the Unity project, WeaponSlot.Set() breaks due to the occurring NullReferenceException. This will result in the weapon slot not showing a level number text, but WeaponManager staying unharmed so it still works in-game and the player can get the damage of the weapon and apply the value to an enemy, for instance.
Controller – Logic Mastery
Finally, we are in the controller module, where most logic takes place. In our naming convention, we name XxxxManager if the script only has one instance or static (e.g. WeaponManager), and XxxxController if the script has multiple instances (e.g. EnemyController, one for each enemy in-game).
As always, we have the weapon Controller as:
public static WeaponManager()
{
public static WeaponData[] weaponDatas;
public static WeaponItem[] weaponItems;
public static event UnityAction panelShown;
...
public static WeaponManager()
{
weaponDatas = GetWeaponDatas(); // Get list of pre-set weapon data from onlin database, CSV, ScriptableObject, etc
weaponItems = GetWeaponItems(); // Get list of in-game weapon item from player-specific PlayerPrefs, JSON, etc
}
public static ShowPanel()
{
panelShown?.Invoke();
}
public static LevelUp(WeaponItem weaponItem)
{
if (CanLevelUp(weaponItem)) { // Check if player has enough coins or meet the level up requirement
weaponItem.level++;
weaponItem.levelUpdated?.Invoke();
Save(weaponItems); // Save the new level values to progress
}
}
...
}
This script closes the loop for the whole enemy MVC pattern. First, we make it a static class because we only need one instance of it. Some developers may favourite using Singleton
and that works too.
Then, we can see that there’s a constructor method that sets up the list of pre-set and in-game data. Next, we have ShowPanel()
that simply executes panelShown
callback, so any script (e.g. main menu) can call WeaponManager.ShowPanel()
to show the weapon panel, without making a direct call to the UI instance.
Finally, we have LevelUp()
method doing the level-up check and saving the in-game data after the update. levelUpdated
event is executed so the corresponding WeaponSlot
UI will be updated.
Conclusion
Now that you know how to implement MVC in your game project, remember that there is no universal formula for setting it up. The approach may vary from project to project, but the concept of arranging scripts is crucial. In the next article, we will walk through another complete example – the DailyLogin
system in Unity, which will provide you with a better understanding. Stay tuned!
More Readings: