MVCS and Unity

A practical guide to game development with MVCS

·
7 min read

A marketing screenshot promoting the Fit Me Challenges mobile app

In 2018, my partner and I decided to build an app together in Unity. The result was Fit Me Challenges, an app aimed at taking on small fitness challenges with a slight gamified twist.

As most side projects go, the code is nothing to celebrate. However, a software design pattern that I adapted turned out to be far more helpful than I expected. This pattern was MVCS.

What is MVCS?

This particular acronym stands for models, views, controllers and services.

It is a derivative of other software design patterns, Model-View-Controller (MVC) and Model-View-Controller-Store (MVCS).

MVCS is an architectural design pattern that helps define relationships between entities within an application. When applied, it helps to breakdown larger scenarios into smaller objects and defined behaviors.

I use this pattern not as a blueprint that must be strictly adhered to, but a set of guidelines to help:

  • Reduce decision making
  • Reduce code complexity
  • Increase code readability
  • Simplify project file and folder structures

There are “rules.” But some can be bent, others can be broken.

What are the rules?

The rules defined below are there to avoid any overlap in responsibility. The boundaries these rules create also ensure that classes do not grow in complexity, which improves readability.

That being said, the general rule is:

Controllers fetch models from services and present them in views.

The pattern also helps with naming conventions. Each entity type is appended to the class name and filename, allowing me to quickly navigate my project for the logic I am after.

The rules for each entity are as follows.

##-- MODEL --##
  • Restricted to primitive types only e.g. int, float, string etc
  • Can include light logic over private member variables
  • Models do not reference each other directly (decoupled)
##-- VIEW --##
  • Can use primitive types or models
  • The only entity responsible for presenting data to a user
  • Notifies the controllers about user interaction
##-- CONTROLLER --##
  • Contains all of the "game" logic
  • Controllers do not interact with other controllers
  • Can interact with all of the other entities (models, views & services)
##-- SERVICE --##
  • Acts as a repository for models
  • Responsible for persisting state between sessions
  • There can only be one instance of a service (singleton pattern)

How do these entities fit together?

Controllers and services make up the larger and more complex entities. Where models are shared between them to fill the views.

A relationship diagram of the MVCS design pattern

How would this work in a real example?

Let’s look at implementing a screen that appears in most apps on the market, the in-app store. We will break this screen down into smaller objects and behaviors following the MVCS pattern.

Note: If you wish to see this screen in action, check out the sample Unity project here.

A screenshot of the in-app store from the Fit Me Challenges mobile app

The basic building blocks of a store are its products. Each product has a small set of information that is presented to a customer for their consideration when viewing the store.

The first step is to represent our product details as a model, which we will call ProductModel. Next, to provide access to these models, we will use a service called ProductService.

To make use of these models, we will create a controller called StoreController. Lastly, the controller can then present each model to a user through a view called ProductView.

An in-app store represented by its MVCS entities

The details for each of these products are predetermined, so we can use Unity’s ScriptableObjects as a way to encapsulate these details as runtime assets. At runtime we can load these assets and generate our ProductModels from them, rather than hard coding them into our classes.

To keep this blog post short, I will skip over some of the implementation details of how services and models work together. However, I may do a deeper dive into this topic at a later date.

For now, let’s take a look at these concepts as code.

ProductModel as the model

public class ProductModel
{
  public string Name { get; set; }
  public float  Cost { get; set; }

  // Light logic that saves time. 
  public string CostDescription()
  {
    return $"The cost is ${Cost}";
  }
}

ProductView as the view

public class ProductView : MonoBehaviour
{
  public Text NameText;
  public Text CostText;

  // Present the information to the user.
  public void Populate(string name, string cost)
  {
    NameText.text = name;
    CostText.text = cost;
  }
}

StoreController as the controller

public class StoreController : MonoBehaviour
{
  public GameObject ProductViewPrefab; 
  public GameObject ProductViewContainer; 
    
  private List<ProductView> _productViews;

  void Start()
  {
    _productViews = new List<ProductView>();

    // Use a service to fetch the models.
    var productModels = ProductService.Instance.GetAllProducts();

    foreach (var product in productModels)
    {
      // Create a new instance of the view.
      var itemGO = Instantiate(ProductViewPrefab, Vector3.zero, Quaternion.identity);
      itemGO.transform.SetParent(ProductViewContainer.transform, false);
      itemGO.transform.SetAsLastSibling();

      // Get the view script.
      var itemView = itemGO.GetComponent<ProductView>();

      // Hold onto the view for further updates.
      _productViews.Add(itemView);

      // The controller passes the model to the view.
      itemView.Populate(product.Name, product.CostDescription());
    }
  }
}

ProductService as the service

public class ProductService : SingletonBehaviour<ProductService>
{
  private ProductFactory _factory;
  private List<ProductModel> _products;

  public override void Init()
  {
    _factory = new ProductFactory();
    _factory.LoadDatabase(); // Loads our ScriptableObject assets

    // Convert the assets into models.
    _products = _factory.GetProducts();
  }

  public List<ProductModel> GetAllProducts()
  {
    return _products;
  }
}

And that’s it. I hope you found this post useful. There is a working example you can download from my GitHub repository here. Feel free to raise an issue on the repo, if you have any questions.

Thanks!