Model-View-Presenter (MVP) and MVC
Summary
Model-View-Controller (MVC) and its variation Model-View-Presenter (MVP) are architectural patterns for user interfaces. Both separate the data, the logic that changes it, and the on-screen presentation so that UI code does not become tangled with game data. Unity’s e-book describes the general idea as separating “the logical portion of your software from the data and from the presentation”, which “helps reduce unnecessary dependencies and potentially cut down on spaghetti code” (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).
The e-book presents MVP as the variation that “many Unity developers opt to use”: the View does not watch the Model itself, and a Presenter sits between them. Use it for HUDs, health bars, inventory screens, settings menus and any interface whose data also matters to gameplay. It builds on the observer-pattern, because the Model announces changes through an event.
Implementation
MVC: three layers
The e-book defines the three MVC layers as follows (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns):
- Model stores data. It is “strictly a data container that holds values. It does not perform gameplay logic or run calculations.”
- View is the interface. It “formats and renders a graphical presentation of your data onscreen.”
- Controller handles logic: “Think of this as the brain. It processes the game data and calculates how the values change at runtime.”
In classic MVC, the Controller handles input and writes results to the Model, and the View observes the Model directly so it can redraw when data changes. In Unity that means writing a View script for each UI element that subscribes to the Model:
// MVC-style: the View observes the Model itself.
public class HealthBarView : MonoBehaviour
{
[SerializeField] private Health health;
[SerializeField] private Slider slider;
private void OnEnable() => health.HealthChanged += Refresh;
private void OnDisable() => health.HealthChanged -= Refresh;
private void Refresh() => slider.value = (float)health.CurrentHealth / health.MaxHealth;
}Why Unity projects tend to use MVP
The e-book’s reasoning runs in three steps. First, in Unity “the existing UI framework (either the UI Toolkit or Unity UI) naturally functions as the View”, so you do not build UI components from scratch. Second, “following the traditional MVC pattern would require View-specific code to listen for any changes in the Model’s data at runtime”. Third, while that is “a valid approach”, many Unity developers instead make the controller an intermediary, so the View does not observe the Model at all (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).
In MVP, according to the e-book:
- The Presenter (the Controller’s replacement) “acts as a go-between for the other layers. It retrieves data from the Model and then formats it for display in the View.”
- MVP “switches which layer handles input”: the View receives user input (Unity UI’s
Button,ToggleandSlider) and forwards it to the Presenter through UI events. - The Presenter manipulates the Model; “a state-change event from the Model tells the Presenter that the data has been updated”; the Presenter passes the new data to the View, which refreshes.
Arguably the fit is practical rather than theoretical: Unity’s Slider or ProgressBar cannot subscribe to your C# events on its own, so some script has to sit between your data and the built-in widget. MVP makes that script an explicit, single place (the Presenter) rather than a scattering of small View scripts.
| MVC | MVP | |
|---|---|---|
| Who watches the Model? | The View | The Presenter |
| Who receives user input? | The Controller | The View, which forwards it to the Presenter |
| Does the View know the Model? | Yes | No |
| In Unity, the View is usually | A custom script wrapping UI components | The UI components themselves (uGUI or UI Toolkit) |
The Model: Health
The Model holds the value and raises an event whenever it changes. It contains no UI code.
public class Health : MonoBehaviour
{
public event Action HealthChanged;
[SerializeField] private int maxHealth = 100;
private int currentHealth;
public int CurrentHealth => currentHealth;
public int MaxHealth => maxHealth;
private void Awake() => currentHealth = maxHealth;
public void Decrement(int amount) => SetHealth(currentHealth - amount);
public void Increment(int amount) => SetHealth(currentHealth + amount);
public void Restore() => SetHealth(maxHealth);
private void SetHealth(int value)
{
currentHealth = Mathf.Clamp(value, 0, maxHealth);
HealthChanged?.Invoke();
}
}This follows the e-book’s Health component, which “stores the actual health value and invokes an event, HealthChanged, every time that value changes” and “does not contain gameplay logic, only methods to increment and decrement the data” (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns). Unity’s sample project notes that the Model “could also be a System.Object or ScriptableObject”; a plain C# class is the easiest to unit test, and a ScriptableObject lets several scenes share the same data (Unity Technologies, game-programming-patterns-demo sample repository on GitHub; see unity-scriptableobjects).
The Presenter: HealthPresenter (Unity UI)
The Presenter is the only class that references both the Model and the View.
public class HealthPresenter : MonoBehaviour
{
[Header("Model")]
[SerializeField] private Health health;
[Header("View")]
[SerializeField] private Slider healthSlider; // Min Value 0, Max Value 1
private void OnEnable() => health.HealthChanged += UpdateView;
private void OnDisable() => health.HealthChanged -= UpdateView;
private void Start() => UpdateView();
// Called by the View (a Button's OnClick in the Inspector) or by gameplay code.
public void Damage(int amount) => health.Decrement(amount);
public void Heal(int amount) => health.Increment(amount);
public void ResetHealth() => health.Restore();
private void UpdateView()
{
// The Presenter formats Model data for display.
healthSlider.value = (float)health.CurrentHealth / health.MaxHealth;
}
}The flow for one click on a Damage button:
- View → Presenter. The button’s
OnClick(aUnityEvent, wired in the Inspector) callsHealthPresenter.Damage(10). - Presenter → Model. The Presenter calls
health.Decrement(10). - Model → Presenter.
Healthclamps the value and raisesHealthChanged. - Presenter → View.
UpdateViewconverts the integer into a 0–1 fraction and sets the slider.
The e-book adds that the Presenter “usually waits to update the user interface… until the Health raises its HealthChanged event”, which matters when setting a Model value takes time, such as saving to disk or a database. In its sample scene, clicking the target or pressing the reset button “inform[s] the HealthPresenter… rather than change the Health directly” (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).
Start refreshes the View once because all Awake calls in a scene run before any Start, so Health has initialised by then (see monobehaviour-lifecycle). Subscribe in OnEnable and unsubscribe in OnDisable, as described in observer-pattern, or a destroyed Presenter can keep receiving events.
The same Presenter with UI Toolkit
With unity-ui-toolkit the View is a UIDocument built from UXML and USS. The Presenter queries elements by name instead of taking Inspector references to components. Unity’s sample project, now on Unity 6 (6000.0), includes this variation as a separate scene (Unity Technologies, game-programming-patterns-demo sample repository on GitHub).
public class HealthToolkitPresenter : MonoBehaviour
{
[SerializeField] private UIDocument document; // View
[SerializeField] private Health health; // Model
private ProgressBar healthBar;
private Button resetButton;
private void OnEnable()
{
VisualElement root = document.rootVisualElement;
healthBar = root.Q<ProgressBar>("health-bar");
resetButton = root.Q<Button>("reset-button");
resetButton.clicked += OnResetClicked; // View -> Presenter
health.HealthChanged += UpdateView; // Model -> Presenter
}
private void OnDisable()
{
resetButton.clicked -= OnResetClicked;
health.HealthChanged -= UpdateView;
}
private void Start() => UpdateView();
private void OnResetClicked() => health.Restore();
private void UpdateView()
{
healthBar.value = 100f * health.CurrentHealth / health.MaxHealth;
healthBar.title = $"{health.CurrentHealth} / {health.MaxHealth}";
}
}The Model is unchanged. Only the Presenter knows which UI system is in use, so swapping Unity UI for UI Toolkit does not touch gameplay code.
Alternative: UI Toolkit data binding (towards MVVM)
Unity 6’s UI Toolkit has runtime data binding, which “binds the properties of any plain C# object to the properties of a UI control” (Unity, Unity Manual 6.0, “Runtime data binding”). Instead of a Presenter copying values by hand, you set an element’s dataSource and register a binding to a property path. A property must carry the [CreateProperty] attribute (from Unity.Properties) to be bindable (Unity, Unity Manual 6.0, “Define a data source for runtime binding”).
public class HealthData
{
[CreateProperty] public float CurrentHealth { get; set; } = 100f;
}
// In a MonoBehaviour that owns a UIDocument:
var bar = document.rootVisualElement.Q<ProgressBar>("health-bar");
bar.dataSource = healthData;
bar.SetBinding(nameof(ProgressBar.value), new DataBinding
{
dataSourcePath = new PropertyPath(nameof(HealthData.CurrentHealth)),
bindingMode = BindingMode.ToTarget // one-way: data -> UI
});Unity’s sample project uses this approach in a separate MVVM (Model-View-ViewModel) scene: a HealthViewModel binds a ScriptableObject health model to the health bar, with converters that turn the integer into a colour and a status label (Unity Technologies, game-programming-patterns-demo sample repository on GitHub). Bindings can also be authored in UI Builder without code.
Two points are worth knowing before choosing binding over a Presenter. By default, “active binding objects are updated every frame”; to update only on change, the data source must implement IDataSourceViewHashProvider or INotifyBindablePropertyChanged (Unity, Unity Manual 6.0, “Define a data source for runtime binding”). And binding only applies to UI Toolkit; a Unity UI (uGUI) Canvas still needs a Presenter or View script.
Other engines
Godot projects typically express the same split with a data node or Resource that emits a signal and a UI scene that connects to it, which is closer to MVC or MVP depending on who owns the connection. Unreal’s UMG widgets can be driven from a C++ or Blueprint presenter-style class, and Unreal also offers an optional viewmodel plugin for an MVVM approach.
Trade-offs
The e-book’s view is that “MVP (and MVC) really shine for larger applications” with sizeable teams and long maintenance periods (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).
Benefits it lists:
- Smooth division of work. With the View separated from the Presenter, UI development “can happen nearly independently from the rest of the codebase”, so front-end specialists can own the View.
- Simpler unit testing. Gameplay logic is separated from the interface, so it can be exercised “without actually needing to enter Play mode in the Editor”.
- Readable, maintainable code. Classes are smaller, and fewer dependencies mean fewer places for bugs to hide.
Costs it lists:
- You need to plan ahead. MVC and MVP “are larger architectural patterns” than the others in the e-book; splitting classes by responsibility takes organisation and more work up front.
- Not everything fits. In a pure implementation anything that renders is part of the View, and not every Unity component (for example a
MeshRenderer) splits neatly into data, logic and interface. Simple scripts may gain little.
The e-book’s advice is to “let the unit tests guide you” and not “force the pattern onto your project”. In student projects this suggests applying MVP to the HUD and menus, where the separation pays off quickly, rather than to every component.
| Approach | Strengths | Weaknesses |
|---|---|---|
UI script reads game data in Update | Quickest to write | Polls every frame; UI and game code tightly coupled |
| MVC (View observes Model) | Simple; one View script per widget | Every widget needs a wrapper script that knows the Model |
| MVP (Presenter between) | Model and View fully decoupled; easy to swap UI systems | One more class per screen or widget |
| UI Toolkit data binding / MVVM | Little glue code; designers can bind in UI Builder | UI Toolkit only; per-frame updates unless change tracking is added |
Examples
- Health bar:
HealthModel,HealthPresenter, and a Slider or ProgressBar View, as above. - Inventory screen: an
InventoryModel raisesItemAddedandItemRemoved; anInventoryPresenterrebuilds or updates slots; slot buttons forward clicks to the Presenter, which asks the Model to equip or drop. - Settings menu: a
GameSettingsScriptableObject Model; the Presenter maps slider and toggle changes to volume and quality values and saves them. - Score and lives HUD: a game manager acts as the Model and raises
ScoreChanged; a HUD Presenter formats the number with leading zeros (see unity-gamemanager-pattern).
Related
- observer-pattern — the Model’s change event is an Observer subject; the Presenter is the observer
- unity-object-communication — wider guide to how scripts talk to each other, including events and ScriptableObject channels
- unity-ui-toolkit — UXML, USS and UI Builder, the modern View layer
- unity-scriptableobjects — ScriptableObject Models shared across scenes
- csharp-delegates —
Actionand theeventkeyword used by the Model - ui-design — design principles for the interfaces the View presents
- state-pattern — another pattern from the same e-book; a state machine’s change event can feed a Presenter
- command-pattern — View input can be turned into commands for undoable UI actions
- source-unity-game-programming-patterns — primary source for the Unity implementation