DEV Community

Cover image for MVP architecture adapted for Unity
Ged Bashiri
Ged Bashiri

Posted on

MVP architecture adapted for Unity

Intro

This article explains how we adopted an MVP (Model–View–Presenter) architecture for our Unity game. I’ll walk through the overall structure, explain the main modules, and then discuss the pros, cons, and how we handled some common challenges.


Disclaimer

This is not a strict MVP handbook, nor a perfect one-to-one mapping of classic MVP into Unity.

What you’ll see here is an adaptation that works well for our game, which is closer to an app with lots of menus and many mini-games. It may or may not fit your project perfectly, but hopefully it gives you useful ideas.

MVP has been used in Unity before, and there are many tutorials out there (for example:
https://learn.unity.com/tutorial/build-a-modular-codebase-with-mvc-and-mvp-programming-patterns).

My main concern with many existing examples, is that they rely heavily on MonoBehaviour APIs everywhere. That undermines one of the biggest benefits of MVP: Testability (in my opinion of course)


Our App

Before talking architecture, it’s important to understand the product. There is no “one architecture to rule them all.”

One ring to rule them all.

Our app, Magrid is a learning solution for kids aged 3 to 10 (check it out if you have a kid or you are a 5 years old by any chance), designed to address two major goals:

  • It is language-free, so children around the world can use it
  • It is inclusive, including support for children with disabilities

From a technical perspective, the app contains:

  • Many menus (login/register, student management, performance review, curriculum management, etc.)
  • Many mini-games (pattern recognition, visual perception, number comparison, and more)

The main technical concern was separation of concerns, both for menu logic and for the mini-games themselves.


Architecture

The architecture has 2 types of entities: MVP modules and Systems.

MVP Modules

An MVP module has 3 components (duh!):

Well, Who would have thought?

View:

  • Essentially a MonoBehaviour
  • Handles user input
  • Displays data
  • Talks directly to the Presenter

Presenter:

  • Not a MonoBehaviour
  • Contains all business logic
  • Fetches data from Model and processes it
  • Updates the View only through an interface

Model:

  • Stores data needed by the module
  • Separated for clarity

MVP Lifecycle

The View is the entry point of an MVP module.
It can be initiated either by Unity event functions or by another MVP module.

  1. The View creates its Presenter(s)
  2. A View may have more than one Presenter for reusability (covered in last section)
  3. The View passes its interface to the Presenter
  4. The Presenter creates its Model (if needed)
  5. The module is ready

All business logic lives in the Presenter.

Imagine, if you were to replace the View with a command-line implementation, the application would still work. because the Presenter Uses no Unity APIs and Communicates only via interfaces

Presenter doesn’t need visuals to work. If your imagination is good enough.

Here is simple example of each components:

View:

public class FooView : BaseScreen, IFooView
{
    [SerializeField] private TMP_Text exposedInExpectorExample;

    private FooPresenter _presenter;

    private void Start()
    {
        _presenter = new FooPresenter(this);
    }

    public void SetName(string barName)
    {
        exposedInExpectorExample.text = barName;
    }
}
Enter fullscreen mode Exit fullscreen mode

View interface:

public interface IFooView : BaseMvpView<FooPresenter>
{
     public void SetName(string barName);
}
Enter fullscreen mode Exit fullscreen mode

Presenter:

public class FooPresenter : BaseMvpPresenter
{
    private IFooView _view;
    private FooModel _model;

    public FooPresenter(IFooView view)
    {
        _view = view;
        _model = new FooModel();
    }
}
Enter fullscreen mode Exit fullscreen mode

Model:

public class RegisterTeacherModel : BaseMvpModel
{
     public string BarName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

In the example:
BaseScreen, which our view extends, is a MonoBehaviour responsible for canvas management(Open, Close, …)

BaseMvpView, which our interface extends, is just a generic interface

public interface BaseMvpView<T> where T : BaseMvpPresenter
{
}
Enter fullscreen mode Exit fullscreen mode

BaseMvpModel also is just a simple interface

public interface BaseMvpModel
{
    public int Version { get; }
}
Enter fullscreen mode Exit fullscreen mode

The Version field helps with data migration when loading persistent data after updates. Each module can manage its own migration logic.

System

Systems are more or less an MVP module without View.

The systems are accessible via dependency injection.

Their purpose is to serve all the MVP modules. They all will be initiated by a single MonoBehaviour (I called it AppManager) which is Accessible by MVPs (A Unity adapted singleton).
This could also be implemented using dependency injection.
I chose not to use Zenject due to IL2CPP issues and instead built a simple dependency manager.\

Key points:

  • Each system exposes an interface
  • All systems can be mocked in tests
  • Systems are initialized during app loading

Common Systems:

  • Profile System — persistent data storage
  • API System — server communication
  • Purchase System — in-app purchases
  • Analytics System — analytics providers and events (Game analytics, Firebase, …)
  • UI System — screens, popups, navigation, back actions

PlayInfo

Ok, there is another entity which I called it PlayInfo.

Here me out, you can just ignore this if you are starting from scratch.

Here me out, you can just ignore this if you are starting from scratch.

PlayInfo handles rare cases where MVP modules need to communicate.

It contains:

  • A set of events that modules can subscribe to
  • Some shared variables

Originally, this existed mainly to help transition from a legacy codebase, where everything accessed everything else via static methods. PlayInfo acted as a mediator between refactored MVP modules and legacy code. In the end, we kept a few of its events because they turned out to be genuinely useful.


Review

Pros:

Testability (Main Goal)

We use two types of tests:

Unit tests

  • You can have excessive unit testes, covering presenters and systems. They have minimum dependencies which can be easily mocked.

Smoke / Integration tests

  • You can simulate user interactions and test multiple modules together.
  • Simulating user interactions and checking UI can be done using UnityTest and Reflection which deserves its own article.

Encapsulation

Each module is fully isolated.
Communication happens only via:

  • Interfaces
  • (Rarely) events This makes refactoring/changing safer and reduces unintended side effects.

Cons:

Boilerplate:

Each module usually needs:

  • View
  • View Interface
  • Presenter
  • Model

Unity Edge Cases

At the end of the day, this is still a game. Sometimes you need unity APIs such as Coroutines, Physics.

our solution:

  • To start Coroutines, use AppManager (Remember? MonoBehaviour Singleton)

These cases were rare for us.
If this becomes frequent in your project, it may be worth introducing a new entity.

Reusability

Although, I’ll take maintainability over reusability any day, but repeating yourself still hurts.

Ways we addressed this:

View Reuse

  • Build generic UI components. For example a Student Profile UI element reused across multiple views. Not only you don’t repeat yourself over and over but also changing code/prefab once will update all screens everywhere

Logic Reuse

  • Stateless logic goes into Utils.
  • For example email validation regex can be used in multiple places.

Presenter reuse

  • A view can use multiple Presenters and pass proper interface to the presenter constructor. (Interface segregation)
  • Example: “Contact Support” logic already exists in Settings. Reuse that Presenter in the Shop screen instead of duplicating code

If your situation doesn’t fit in these two cases, most probably it deserves to be a system and provide service to MVP modules in need.


Conclusion

Sometimes Games and Apps are very similar and they are not very distinguishable. Architectures like MVP or MVVM, while not originally designed for games, can work extremely well for menu-heavy, logic-driven Unity projects. Here I tried adopt MVP architecture for Unity environment and benefited from:

  • Testability
  • Separation of concerns
  • Maintainability

In the end, if you have questions or better idea to improve this approach, let me know. :D

Top comments (0)