What’s MVC?
You’ve probably heard about the Model-View-Controller pattern which has been widely adopted in web and app design. The general idea of MVC is to separate the internal representations of information so the projects can be better organized.
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, Windoes, 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’ve seen a lot of spaghetti code from junior developers on their Unity game projects. An example would be a script with 500+ lines controlling data, UI and logic at the same time. It is very hard to read the script and is a disaster when I need to locate and debug a specific functionality of the script. Therefore, it is very important to get the project organized with less code dependency, especially when the projects are cooperated by a team. By borrowing from the MVC pattern concept, we can create something similar to MVC and achieve a better code architecture.
Few reasons how MVC can help us here:
- Less dependency – the greatest benefit of spliting the view from controller, is to reduce the dependency and chance having missing reference errors. For instance, breaking anything from UI wouldn’t effect the main controller logic.
- Eaiser to build integration tests – since the UI is separated from the controller, we can build the intergration test with controller mockup disgrading UI and player input.
- Multiple UI pages – sometime there may be multiple UI for one functionality in a game, e.g. two different card collection screen from menu or from in-game. We can add/update specific UI without interfering the controller.
- Less merge conflict – e.g. while a technical artist updating a UI script and assigning its references in Unity, a developer can work independently on a controller scripts.
- Better readibility – it will be easier to locate the code from each module by its functionality.
So now we know how MVC can make the game project better organized. Every developer may has his own way implement MVC, this article will show you how we adpat to it in the detail and examples.
Model – Data
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
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 – Manager
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 set 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
So now you know how to implement MVC in your game project. Again, there’s not ONE formula on how to set it up and its project-to-project, but it’s more the concept behind it on how to arrange the scripts. We will go through another complete example – DailyLogin in the next article which will give you a better idea.
More Readings:
Unity with MVC: How to Level Up Your Game Development
The post MVC Pattern in Unity – Less Code Dependency and Better Code Architecture appeared first on Richard Fu.
Top comments (0)