Level Up Your Game Development with Unity and MVC

Beginning programmers often embark on their coding journey with the traditional “Hello World” program. As they progress, they encounter increasingly complex tasks, each revealing a crucial principle:

The more extensive the project, the greater the complexity.

It quickly becomes evident that both individual and team projects demand a structured approach. Code sustainability is paramount, as it may require maintenance over extended periods. Relying on the original programmer’s availability for every modification is impractical and undesirable.

Software design patterns address this challenge by establishing clear rules for structuring projects. They guide programmers in dividing large projects into manageable components and organizing them systematically. This standardization simplifies code comprehension and navigation, particularly when encountering unfamiliar sections.

Adhering to these patterns enhances maintainability and streamlines the addition of new code. Time spent on planning development methodology is reduced. As problems are diverse, there is no one-size-fits-all design pattern. Selecting the most effective one requires careful consideration of their strengths and weaknesses in relation to the task at hand.

This tutorial shares my insights on employing the widely-used Unity game development platform and Model-View-Controller (MVC) patterns in game development. Throughout my seven years of experience tackling intricate game development projects, this design pattern has consistently delivered well-structured code and accelerated development cycles.

We’ll begin by outlining Unity’s fundamental architecture, the Entity-Component pattern. Subsequently, we’ll explore how MVC complements it and illustrate its application using a mock project.

Why This Matters

Software development boasts a wide array of design patterns. While they provide guidelines, developers often adapt them to address specific challenges.

This adaptability underscores the absence of a single, definitive approach to software design. Therefore, this article presents not a universal solution, but rather, the advantages and applications of two prominent patterns: Entity-Component and Model-View-Controller.

The Entity-Component Pattern

The Entity-Component (EC) pattern entails defining the application’s element hierarchy (Entities) before outlining their features and data (Components). In programming terms, an Entity acts as an object potentially containing an array of Components. Let’s visualize an Entity like this:

1
some-entity [component0, component1, ...]

Here’s a straightforward example of an EC tree.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- app [Application]
   - game [Game]
      - player [KeyboardInput, Renderer]
      - enemies
         - spider [SpiderAI, Renderer]
         - ogre [OgreAI, Renderer]
      - ui [UI]
         - hud [HUD, MouseInput, Renderer]
         - pause-menu [PauseMenu, MouseInput, Renderer]
         - victory-modal [VictoryModal, MouseInput, Renderer]
         - defeat-modal [DefeatModal, MouseInput, Renderer]

EC effectively mitigates the complexities of multiple inheritance. In intricate class structures, issues like the diamond problem can arise. This occurs when a class (D) inheriting from two classes (B and C) that share a base class (A) encounters conflicts due to differing modifications made by B and C to A’s features.

IMAGE: DIAMOND PROBLEM

Such issues are prevalent in game development, where inheritance is extensively employed.

Dividing features and data handlers into smaller Components enhances reusability across different Entities. This eliminates dependency on multiple inheritance, a feature absent in Unity’s primary languages, C# and Javascript.

Limitations of Entity-Component

While EC surpasses OOP in code organization, large projects can still lead to a “feature overload.” Identifying appropriate Entities and Components and defining their interactions can become challenging due to the numerous possibilities for their assembly.

IMAGE: EC FEATURE OCEAN

Imposing additional guidelines on top of Entity-Component can mitigate this issue. A helpful approach is categorizing software functions:

  • Managing raw data: creation, reading, updating, deletion, searching (the CRUD principle).
  • Interface elements: facilitating interaction, detecting events within their scope, triggering notifications.
  • Decision-making elements: receiving notifications, applying business logic, determining data manipulation.

Fortunately, a pattern already embodies these principles.

The Model-View-Controller (MVC) Pattern

The Model-View-Controller pattern (MVC) pattern divides software into three primary components: Models (data management), Views (interface/detection), and Controllers (decision/action). MVC’s adaptability allows implementation alongside ECS or OOP.

Game and UI development typically involve responding to user inputs or trigger events. These interactions align seamlessly with MVC, where notifications are sent, appropriate responses are determined, and data is updated accordingly.

This methodology enhances software planning and navigation, especially for new programmers. Separating data, interface, and decisions reduces the number of source files to be examined when modifying functionality.

Unity and EC

Let’s delve into Unity’s inherent structure.

Unity’s development platform is rooted in EC, where GameObjects represent Entities, and Components provide functionalities like visibility, movement, and interactivity.

The Unity editor’s Hierarchy and Inspector panels empower developers to assemble applications, attach Components, configure their initial states, and bootstrap games with minimal code.

SCREENSHOT: HIERARCHY PANEL
Hierarchy Panel with four GameObjects on the right
SCREENSHOT: INSPECTOR PANEL
Inspector Panel with a GameObject’s components

However, even with these tools, large projects can still encounter feature clutter within the hierarchy, complicating development.

Adopting the MVC approach and organizing elements based on their functions can establish a structured application:

SCREENSHOT: UNITY MVC EXAMPLE STRUCTURE

Adapting MVC for Game Development

Two slight modifications can enhance MVC’s suitability for Unity projects:

  1. Centralized Access: MVC class references scattered throughout the code can lead to reference errors, especially after crashes. Introducing a single root reference object provides a central access point to all instances.
  2. Reusable Components: Certain functionalities transcend MVC’s core categories and are best categorized as Components. These reusable elements, akin to Entity-Component Components, act as helpers within the MVC framework. Example: a Rotator Component handling rotation without notifications, storage, or decision-making.

These modifications result in the AMVCC (Application-Model-View-Controller-Component) pattern.

IMAGE: AMVCC DIAGRAM
  • Application: Single entry point and container for critical instances and application data.
  • MVC: The familiar trio.
  • Component: Reusable, self-contained scripts.

This modified pattern has proven highly effective in my projects.

Example: The “10 Bounces” Game

Let’s illustrate AMVCC with a simple game: 10 Bounces.

The game consists of a falling Ball with a SphereCollider and a Rigidbody, a Cube representing the ground, and five scripts embodying AMVCC.

Hierarchy

Before coding, outlining the class and asset hierarchy is crucial, adhering to the AMVCC structure.

SCREENSHOT: BUILDING THE HIERARCHY

The view GameObject encompasses visual elements and related scripts. In smaller projects, the model and controller GameObjects typically house only their respective scripts. Larger projects may include GameObjects with more specialized scripts within them.

This structure provides a clear path for accessing specific functionalities:

  • Data: application > model > ...
  • Logic/Workflow: application > controller > ...
  • Rendering/Interface/Detection: application > view > ...

Consistent adherence to these rules ensures maintainability even in legacy projects.

Note the absence of a Component container, as Components offer flexibility in attachment.

Scripting

Note: The following scripts are simplified for clarity. For a comprehensive implementation, refer to my Unity MVC framework, here’s the link. It provides core classes implementing the AMVCC structure.

Let’s examine the scripts for 10 Bounces.

A brief overview of Unity’s workflow: Components (Entity-Component) are represented by the MonoBehaviour class. They are instantiated during runtime either by dragging their source files onto GameObjects (Entities) or using the AddComponent<YourMonobehaviour>() command.

We start by defining the Application class (“A” in AMVCC) as the central container for all game elements. An Element helper base class provides access to the Application instance and its MVC children.

The Application class (single instance) houses three variables: model, view, and controller, serving as access points to MVC instances during runtime. These variables are MonoBehaviours with public references to their respective scripts.

The Element base class provides access to the Application instance, enabling communication between all MVC classes.

Both classes extend MonoBehaviour and are attached to GameObjects as Components.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// BounceApplication.cs

// Base class for all elements in this application.
public class BounceElement : MonoBehaviour
{
   // Gives access to the application and all instances.
   public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }}
}

// 10 Bounces Entry Point.
public class BounceApplication : MonoBehaviour
{
   // Reference to the root instances of the MVC.
   public BounceModel model;
   public BounceView view;
   public BounceController controller;

   // Init things here
   void Start() { }
}

The BounceElement serves as the foundation for the MVC core classes. BounceModel, BounceView, and BounceController act as containers for specialized instances. In this example, only the View has a nested structure due to its simplicity.

1
2
3
4
5
6
7
8
9
// BounceModel.cs

// Contains all data related to the app.
public class BounceModel : BounceElement
{
   // Data
   public int bounces;  
   public int winCondition;
}
1
2
3
4
5
6
7
8
// BounceView .cs

// Contains all views related to the app.
public class BounceView : BounceElement
{
   // Reference to the ball
   public BallView ball;
}
1
2
3
4
5
6
7
8
9
// BallView.cs

// Describes the Ball view and its features.
public class BallView : BounceElement
{
   // Only this is necessary. Physics is doing the rest of work.
   // Callback called upon collision.
   void OnCollisionEnter() { app.controller.OnBallGroundHit(); }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// BounceController.cs

// Controls the app workflow.
public class BounceController : BounceElement
{
   // Handles the ball hit event
   public void OnBallGroundHit()
   {
      app.model.bounces++;
      Debug.Log(Bounce +app.model.bounce);
      if(app.model.bounces >= app.model.winCondition)
      {
         app.view.ball.enabled = false;
         app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball
         OnGameComplete();
      } 
   }

   // Handles the win condition
   public void OnGameComplete() { Debug.Log(Victory!!); }
}

With the scripts defined, we can attach and configure them.

The hierarchy should resemble:

1
2
3
4
5
6
7
- application [BounceApplication]
    - model [BounceModel]
    - controller [BounceController]
    - view [BounceView]
        - ...
        - ball [BallView]
        - ...

The Unity editor view of BounceModel:

SCREENSHOT: BounceModel IN INSPECTOR
BounceModel with the bounces and winCondition fields.

Running the game should produce this output in the Console Panel:

SCREENSHOT: CONSOLE OUTPUT

Notifications

In the example, BallView calls the app.controller.OnBallGroundHit() method upon collision. While functional, a centralized notification system within the Application class improves organization.

We’ll modify the BounceApplication:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// BounceApplication.cs

class BounceApplication 
{
   // Iterates all Controllers and delegates the notification data
   // This method can easily be found because every class is “BounceElement” and has an “app” 
   // instance.
   public void Notify(string p_event_path, Object p_target, params object[] p_data)
   {
      BounceController[] controller_list = GetAllControllers();
      foreach(BounceController c in controller_list)
      {
         c.OnNotification(p_event_path,p_target,p_data);
      }
   }

   // Fetches all scene Controllers.
   public BounceController[] GetAllControllers() { /* ... */ }
}

A new script will contain the names of all notification events:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// BounceNotifications.cs

// This class will give static access to the events strings.
class BounceNotification
{
   static public string BallHitGround = ball.hit.ground;
   static public string GameComplete  = game.complete;
   /* ...  */
   static public string GameStart     = game.start;
   static public string SceneLoad     = scene.load;
   /* ... */
}

This approach enhances code readability by providing a central location to understand the application’s event-driven behavior.

Adapting BallView and BounceController to this new system:

1
2
3
4
5
6
7
8
9
// BallView.cs

// Describes the Ball view and its features.
public class BallView : BounceElement
{
   // Only this is necessary. Physics is doing the rest of work.
   // Callback called upon collision.
   void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// BounceController.cs

// Controls the app workflow.
public class BounceController : BounceElement
{
   // Handles the ball hit event
   public void OnNotification(string p_event_path,Object p_target,params object[] p_data)
   {
      switch(p_event_path)
      {
         case BounceNotification.BallHitGround:
            app.model.bounces++;
            Debug.Log(Bounce +app.model.bounce);
            if(app.model.bounces >= app.model.winCondition)
            {
               app.view.ball.enabled = false;
               app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball
               // Notify itself and other controllers possibly interested in the event
               app.Notify(BounceNotification.GameComplete,this);            
            }
         break;
         
         case BounceNotification.GameComplete:
            Debug.Log(Victory!!);
         break;
      } 
   }
}

For larger projects with numerous notifications, dedicated controllers for different notification scopes can prevent overly complex switch-case structures.

Real-World AMVCC

This example demonstrates a basic AMVCC implementation. Mastering the categorization of elements into Models, Views, and Controllers, along with visualizing entities within a hierarchy, are crucial skills.

More complex scenarios in larger projects might raise questions about classification or the need for further class separation.

Practical Guidelines

While a universal guide doesn’t exist, these rules can help determine MVC classification and class splitting.

Class Classification

Models

  • Store application’s core data (e.g., player health, ammo).
  • Handle data serialization, deserialization, and type conversion.
  • Manage data loading/saving (local/web).
  • Notify Controllers of operation progress.
  • Store the Game State for the Game’s Finite State Machine.
  • Never directly access Views.

Views

  • Retrieve data from Models for presenting the current game state to the user.
  • Refrain from modifying Models directly.
  • Strictly adhere to their defined functionalities (e.g., a PlayerView should not handle input or modify game state).
  • Act as black boxes with interfaces, notifying of significant events.
  • Avoid storing core data (e.g., speed, health).

Controllers

  • Do not store core data.
  • May filter notifications from specific Views.
  • Update and utilize Model data.
  • Manage Unity’s scene workflow.

Class Hierarchy

Overly prefixed variables or multiple variations of the same element indicate a need for class splitting (e.g., numerous Player classes or Gun types).

Example: A single Model for Player data with many playerDataA, playerDataB variables, or a Controller handling Player notifications with OnPlayerDidA, OnPlayerDidB methods.

Nesting elements simplifies code and allows switching between data variations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Model.cs

class Model
{
   public float playerHealth;
   public int playerLives;

   public GameObject playerGunPrefabA;
   public int playerGunAmmoA;

   public GameObject playerGunPrefabB;
   public int playerGunAmmoB;

   // Ops Gun[C D E ...] will appear...
   /* ... */

   public float gameSpeed;
   public int gameLevel;
}
1
2
3
4
5
6
7
// Model.cs

class Model
{
   public PlayerModel player;  // Container of the Player data.
   public GameModel game;      // Container of the Game data.
}
1
2
3
4
5
6
7
// GameModel.cs

class GameModel
{
   public float speed;         // Game running speed (influencing the difficulty)
   public int level;           // Current game level/stage loaded
}
1
2
3
4
5
6
7
8
// PlayerModel.cs

class PlayerModel
{
   public float health;        // Player health from 0.0 to 1.0.
   public int lives;           // Player “retry” count after he dies.
   public GunModel[] guns;     // Now a Player can have an array of guns to switch ingame.
}
1
2
3
4
5
6
7
8
9
// GunModel.cs

class GunModel
{
   public GunType type;        // Enumeration of Gun types.
   public GameObject prefab;   // Template of the 3D Asset of the weapon.
   public int ammo;            // Current number of bullets
   public int clips;           // Number of reloads possible
}

This structure enables intuitive navigation through source code. For instance, in a first-person shooter with diverse weapons, containing GunModel within a class allows creating and storing a list of Prefabs for each category.

Storing all gun information together (e.g., gun0Ammo, gun1Ammo) within a single GunModel would complicate data storage and highlight the need for a separate GunModel.

IMAGE: CLASS HIERARCHY
Improving the class hierarchy.

As always, balance is key. Avoid excessive compartmentalization that increases complexity. Experience is crucial for determining the optimal MVC structure for your project.

New game dev Special Ability unlocked: Unity games with the MVC pattern.

Wrapping Up

Countless software patterns exist. This article showcases one that has proven invaluable in my experience. Embrace new knowledge while critically evaluating its suitability.

Exploration of various patterns is encouraged to discover the best fit for your needs. this Wikipedia article provides a comprehensive list of patterns and their characteristics.

For those interested in AMVCC, my library, Unity MVC, offers the essential core classes to get started.

Licensed under CC BY-NC-SA 4.0