MVP architecture adapted for Unity

Published: (January 31, 2026 at 05:07 PM EST)
6 min read
Source: Dev.to

Source: Dev.to
Note: 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.

The product

There is no “one architecture to rule them all.”

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

  • Language‑free – children around the world can use it.
  • Inclusive – includes support for children with disabilities.

Technical perspective

  • 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 overview

The architecture has two types of entities:

  1. MVP modules
  2. Systems

MVP module

An MVP module has three components:

ComponentResponsibility
View (inherits from MonoBehaviour)Handles user input, displays data, talks directly to the Presenter
Presenter (plain C# class)Contains all business logic, fetches data from the Model, updates the View only through an interface
Model (plain C# class)Stores data needed by the module, separated for clarity

Flow

  1. The View is the entry point of an MVP module.
  2. The View creates its Presenter(s).
  3. A View may have more than one Presenter for reusability.
  4. The View passes its interface to the Presenter.
  5. The Presenter creates its Model (if needed).
  6. The module is ready.

All business logic lives in the Presenter. Imagine replacing the View with a command‑line implementation – the application would still work because the Presenter uses no Unity APIs and communicates only via interfaces.

Simple code example

// 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;
    }
}

// View interface ------------------------------------------------
public interface IFooView : BaseMvpView
{
    void SetName(string barName);
}

// Presenter ----------------------------------------------------
public class FooPresenter : BaseMvpPresenter
{
    private readonly IFooView _view;
    private readonly FooModel _model;

    public FooPresenter(IFooView view)
    {
        _view  = view;
        _model = new FooModel();
    }
}

// Model ---------------------------------------------------------
public class FooModel : BaseMvpModel
{
    public string BarName { get; set; }
}

Explanation

  • BaseScreen (the View’s base class) is a MonoBehaviour responsible for canvas management (Open, Close, …).
  • BaseMvpView is a generic interface that the view interface extends:
public interface BaseMvpView where T : BaseMvpPresenter { }
  • BaseMvpModel is a simple interface:
public interface BaseMvpModel
{
    int Version { get; }
}

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

Systems

Systems are essentially MVP modules without a View. Their purpose is to serve all the MVP modules.

  • All systems are instantiated by a single MonoBehaviour (called AppManager) which is accessible by MVPs (a Unity‑adapted singleton).
  • Because of IL2CPP issues with Zenject, we built a simple dependency manager instead.

Characteristics

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

Example systems

SystemResponsibility
Profile SystemPersistent data storage
API SystemServer communication
Purchase SystemIn‑app purchases
Analytics SystemAnalytics providers and events (GameAnalytics, Firebase, …)
UI SystemScreens, pop‑ups, navigation, back actions

PlayInfo – the mediator

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 it 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.

Testing strategy

We use two types of tests:

  1. Unit tests – covering presenters and systems. They have minimal dependencies which can be easily mocked.
  2. Integration / UI tests – 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).

Isolation

  • Each module is fully isolated by interfaces and (rarely) events.
  • This makes refactoring/changing safer and reduces unintended side effects.

Typical module composition

  • 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 or Physics.

  • To start Coroutines, use AppManager (the MonoBehaviour singleton).
// Example: starting a coroutine from a presenter
public class FooPresenter : BaseMvpPresenter
{
    private readonly IFooView _view;
    private readonly FooModel _model;
    private readonly AppManager _appManager;

    public FooPresenter(IFooView view, AppManager appManager)
    {
        _view        = view;
        _model       = new FooModel();
        _appManager  = appManager;
    }

    public void DoSomethingAsync()
    {
        _appManager.StartCoroutine(DoWork());
    }

    private IEnumerator DoWork()
    {
        // Unity‑specific logic here
        yield return new WaitForSeconds(1f);
        _view.SetName("Done");
    }
}

Other Unity‑specific needs (physics, animation events, etc.) are handled similarly: the Presenter delegates the call to a system or to AppManager, keeping the Presenter itself free of direct Unity dependencies.

TL;DR

  • MVP gives us testable, decoupled UI logic.
  • Systems provide shared services without polluting the View layer.
  • PlayInfo is a lightweight mediator for legacy‑style communication.
  • AppManager is the single Unity entry point for anything that truly needs a MonoBehaviour.

Feel free to adapt the ideas to your own project – the goal is to keep concerns separated, make testing easy, and still be able to leverage Unity’s powerful runtime features when you need them.

Avoiding Duplication in MVP Architecture

These cases were rare for us. Although I’ll take maintainability over reusability any day, repeating yourself still hurts.

Ways we addressed this

  • Build generic UI components
    Example: a Student Profile UI element reused across multiple views.

    • No need to repeat the same UI code.
    • Changing the component (or its prefab) once updates every screen that uses it.
  • Put stateless logic into utility classes
    Example: an e‑mail‑validation regex that can be called from any module.

  • Use multiple presenters per view and inject the proper interface (Interface Segregation)
    Example: the “Contact Support” logic already exists in Settings. Re‑use that presenter in the Shop screen instead of duplicating the code.

  • When a situation doesn’t fit the two cases above, treat it as a system
    Provide a service that MVP modules can consume.

Why MVP (or MVVM) can work for Unity projects

Even though these patterns weren’t originally designed for games, they shine in menu‑heavy, logic‑driven Unity projects. Adopting MVP gave us:

  • Testability – presenters can be unit‑tested in isolation.
  • Separation of concerns – UI, business logic, and data are cleanly divided.
  • Maintainability – changes in one layer rarely ripple into others.

Got feedback?

If you have questions or a better idea to improve this approach, let me know. :D

Back to Blog

Related posts

Read more »