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.”
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!):
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.
- The View creates its Presenter(s)
- A View may have more than one Presenter for reusability (covered in last section)
- The View passes its interface to the Presenter
- The Presenter creates its Model (if needed)
- 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
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;
}
}
View interface:
public interface IFooView : BaseMvpView<FooPresenter>
{
public void SetName(string barName);
}
Presenter:
public class FooPresenter : BaseMvpPresenter
{
private IFooView _view;
private FooModel _model;
public FooPresenter(IFooView view)
{
_view = view;
_model = new FooModel();
}
}
Model:
public class RegisterTeacherModel : BaseMvpModel
{
public string BarName { get; set; }
}
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
{
}
— BaseMvpModel also is just a simple interface
public interface BaseMvpModel
{
public int Version { get; }
}
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.

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.
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, useAppManager(Remember?MonoBehaviourSingleton)
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)