Hi everyone,
This is post #4 in our ongoing series explaining the new UI Toolkit sample project for programmers, QuizU.
Learn more about the Settings Screen in the article
QuizU is a sample of an interactive quiz application that shows how UI Toolkit components can work together, leveraging various design patterns, in a small but functional game, complete with multiple screens and game flow management.
This series of articles takes you through the sample, explaining how we implemented the project using UI Toolkit.
We recommend that you download QuizU running on 2022 LTS to follow along.
The previous posts in this series are here:
- Welcome to the new sample project QuizU
- QuizU: State pattern for game flow
- QuizU: Managing menu screens in UI Toolkit
Today’s post focuses on how QuizU employs the Model View Presenter (MVP) design pattern. The MVP pattern is an architectural pattern that helps us maintain separation of concerns, something that’s especially helpful as we add features to the project.
MVP divides our code into three distinct parts:
Visualization of how the MVP pattern works
-
Model: This contains the data and the rules that govern this data. For example, this could include the game’s current state, game attributes, or logic regarding that data (e.g. rules for leveling up a character, health, or scoring). The model has no knowledge of the View.
-
View: This is the user interface of your application. It displays the data to the user and sends user interactions (like button clicks) to the Presenter. The View in the MVP pattern does not directly interact with the Model.
-
Presenter: This script sits between the Model and the View. It handles user input events from the View, updates the Model as conditions change in the game, and updates the View to reflect those changes.
Essentially, the View, or user interface, does not directly connect with the data that it represents. It instead relies on the Presenter to tell it what to do. Though we might have a separate script for UI logic, the View doesn’t handle any “business logic” for our game. That’s safely tucked away in a separate silo.
These separate layers of software are useful for:
-
Scalability: Having distinct parts of the software that handle specific tasks makes it easier to manage and grow your codebase.
-
Modularity: With MVP, changes in one component don’t ripple through the entire system. Each component can be developed, tested, and modified independently. This makes your code easier to debug.
-
Reusability and Maintainability: Parts of your code (like Models or Presenters) can often be reused across different parts of your application. If there’s a problem, it’s quicker to isolate and fix bugs in a specific layer than having to sift through code spaghetti.
If you ever need to rewrite the UI – let’s say you want to update your UGUI interface to UI Toolkit – MVP makes that process easier. Changing any one of the three parts of MVP has minimal impact on the others. This is also a big plus when working on a large team where the work is divided among several developers.
MVP is a variation of the MVC (Model-View-Controller) family of software patterns. Both are patterns for structuring code into three parts, but in the MVC, a Controller (instead of a Presenter) handles user input and manipulates the Model. The Model can then directly update the View.
If you want to read more about MVC and MVP, see the free e-book, Level up your code with game programming patterns.
QuizU: MVP with UI Toolkit example
Let’s walk through a simple example in the QuizU project to see one way to set up MVP using the UI Toolkit-based interface. The SettingsScreen is a basic interface that allows the user to change master volume levels as well as the levels for the individual sound effects or background music.
To see it in action, open the Boot scene from the Scenes folder of the project. Then, select Settings from the menu.
The UI only contains a few UI Sliders like so:
The Settings Screen.
To build this screen in QuizU, we use MVP to structure our assets and classes:
-
The Model consists of the AudioSettings (\Quiz\Scripts\ScriptableObjects\AudioSettingsSO.cs) ScriptableObject, which connects to the MainAudioMixer asset. This contains audio configuration data, including the master volume, sound effects volume, and music volume. The Model also holds sound effect AudioClips that correspond to various game events (e.g. winning the game, answering incorrectly, etc).
-
The View includes the SettingsScreen class (\Quiz\Scripts\UI\Screens\SettingsScreen.cs). It’s responsible for rendering the UI and responding to user interactions. In this case, that involves manipulating the UI sliders for audio settings. The SettingsScreen.uxml and SettingsScreen.uss define the structure and style of the UI.
-
The Presenter – the SettingsPresenter class (\Quiz\Scripts\Managers\SettingsPresenter.cs) – acts as the glue between the Model and the View. After receiving user input from the View, it processes and updates the Model accordingly. If external changes happen to the Model (e.g. resetting when starting the game or loading data from a save), it can also refresh the View to match.
Let’s take a closer look at each part and see how they work.
The Model: AudioSettings
In MVP, everything in our SettingsScreen represents some value stored elsewhere. While you can use MonoBehaviours for data storage, ScriptableObjects offer a more optimized approach. They not only serialize their fields in the Inspector but also allow for a project-level Model that’s accessible from any scene.
The AudioSettingsSO (QuizU\Assets\Quiz\Scripts\ScriptableObjects\AudioSettingsSO.cs) ScriptableObject includes:
-
A reference to the MainAudioMixer that will control the sound levels.
-
Float properties represent the master volume, sound effects volume, and music volume from a range of 0 to 1.
-
A collection of AudioClips stands in for various in-game sounds (correctly answering, incorrectly answering, passing the quiz, failing the quiz, or just clicking a button).
Because the ScriptableObject is an asset at the project level, it can only refer to other project-level assets like prefabs. It can’t directly reference objects from the Scene Hierarchy. This is inconvenient sometimes, but in this case, it’s a good thing.
Remember, this Model knows nothing about any Views or Presenters - it stores and updates its data as directed. Using a ScriptableObject just enforces that.
The AudioSettings ScriptableObject
The View: Settings Screen
The View represents the user interface and consists of two parts:
-
The UI Toolkit-based interface: This includes the SettingsScreen.uxml and SettingsScreen.uss that define the visual tree hierarchy and its style sheets.
-
The SettingsScreen class: This custom script manages all the UI elements (e.g. the sliders) and updates their state to match the data in the Model.
The SettingsScreen class (Quiz\Scripts\UI\Screens\SettingsScreen.cs) references all of the interactive elements within the parent container’s visual tree and registers callbacks for sliders and button clicks.
It then determines what to do when the user applies input. In this implementation, that means updating each slider’s corresponding label element and then notifying the Presenter when the UI has changed.
Because the View doesn’t reference the Presenter, it communicates through events. Every time the user drags a slider handle at runtime, it raises a ChangeEvent with the new float value.
For example, when the user adjusts the master volume slider, this invokes the event handler (MasterVolumeChangeHandler). That updates the m_MasterVolumeLabel.text and then raises the SettingsEvents.MasterSliderChanged event. The Presenter listens to this event and then notifies the Model.
The SettingScreen represents the View logic.
The Presenter: SettingsPresenter
Central to the pattern, the SettingsPresenter (\Quiz\Scripts\Managers\ SettingsPresenter.cs) sits between the View and the Model. It handles changes from the View, updates the Model, and vice versa. The Presenter receives user input events from the View, invokes appropriate methods on the Model, and performs necessary calculations/manipulation of the data. This ensures that View and Model stay in sync but are decoupled.
In the QuizU sample, the SettingsPresenter class is a MonoBehaviour. It subscribes to events raised by both the SettingsScreen (View) and the AudioSettingsSO ScriptableObject (Model).
public class SettingsPresenter : MonoBehaviour
{
...
// Event subscriptions
private void OnEnable()
{
// Listen for events from the View/UI
SettingsEvents.MasterSliderChanged += SettingsEvents_MasterSliderChanged;
SettingsEvents.SFXSliderChanged += SettingsEvents_SFXSliderChanged;
SettingsEvents.MusicSliderChanged += SettingsEvents_MusicSliderChanged;
// Listen for events from the Model
SettingsEvents.ModelMasterVolumeChanged += SettingsEvents_ModelMasterVolumeChanged;
SettingsEvents.ModelSFXVolumeChanged += SettingsEvents_ModelSFXVolumeChanged;
SettingsEvents.ModelMusicVolumeChanged += SettingsEvents_ModelMusicVolumeChanged;
}
// Event unsubscriptions
private void OnDisable()
{
SettingsEvents.MasterSliderChanged -= SettingsEvents_MasterSliderChanged;
SettingsEvents.SFXSliderChanged -= SettingsEvents_SFXSliderChanged;
SettingsEvents.MusicSliderChanged -= SettingsEvents_MusicSliderChanged;
SettingsEvents.ModelMasterVolumeChanged -= SettingsEvents_ModelMasterVolumeChanged;
SettingsEvents.ModelSFXVolumeChanged -= SettingsEvents_ModelSFXVolumeChanged;
SettingsEvents.ModelMusicVolumeChanged -= SettingsEvents_ModelMusicVolumeChanged;
}
…
}
As you examine the SettingsPresenter, you’ll note:
-
Whenever the user moves a volume slider, the Presenter receives an event and notifies the Model to update.
-
Likewise, if a game event changes the Model directly (e.g. the volume levels loaded from saved settings or loading for the first time), the Presenter notifies the View via events.
-
The SettingsPresenter does not use UnityEngine.UIElements. This is another way to enforce the separation of concerns. Only the View will reference and manage UI elements. Instead, it sends messages between the user-interface (the View) and the AudioSettings ScriptableObject data (the Model).
-
For convenience, the SettingsPresenter can maintain direct references to the View and the Model. In some circumstances, we might need to let the Presenter bypass events. For example, we can take advantage of the MonoBehaviour lifecycle events (Start or Awake) to initialize slider values when we enter Play mode.
The SettingsPresenter is the intermediary between the Model and View.
SettingsEvents
As you’ve seen in the previous examples, the View, Model, and Presenter from QuizU use public static delegates from the SettingsEvents class to communicate.
These aren’t events in the strict C# sense – we want external objects to raise them – but they function as events for messaging purposes:
public static class SettingsEvents
{
// Presenter -> View: sync UI sliders to Model
public static Action<float> MasterSliderSet;
public static Action<float> SFXSliderSet;
public static Action<float> MusicSliderSet;
// View -> Presenter: handle user input
public static Action<float> MasterSliderChanged;
public static Action<float> SFXSliderChanged;
public static Action<float> MusicSliderChanged;
// Presenter -> Model: update volume settings
public static Action<float> MasterVolumeChanged;
public static Action<float> SFXVolumeChanged;
public static Action<float> MusicVolumeChanged;
// Model -> Presenter: model values changed (e.g. loading saved values)
public static Action<float> ModelMasterVolumeChanged;
public static Action<float> ModelSFXVolumeChanged;
public static Action<float> ModelMusicVolumeChanged;
}
These delegates sit in between the objects that need to communicate. That lets them trade messages while staying decoupled.
This approach helps the Presenter, View, and Model each do what they do best. The View focuses on UI interactions, the Model manages data, and the Presenter coordinates between the two.
Further reading
Like other design patterns, you’ll want to adapt MVP to your needs and style. For example, if the benefits of a ScriptableObject aren’t apparent, maybe use a MonoBehaviour instead. The goal is to strike a balance between a consistent architecture and the flexibility to mix and match where necessary.
Though we hope that you don’t often need to do a massive rewrite of your UI code, using patterns like MVP can help prepare you for when it’s necessary. Then, you’ll be ready when the application scales and grows with your team.
If you’re interested in more software design patterns, please make sure to see the free e-book Level up your code with game programming patterns. You can also find our other best practices guides in the documentation.
Finally, just a reminder that QuizU is not our only UI Toolkit demo. It complements two big pieces of content we released last year to help you get started with UI Toolkit:
-
UI Toolkit sample – Dragon Crashers: This demo is on the Asset Store. It’s a slice of a full-featured interface added to the 2D project Dragon Crashers, a mini RPG, using the Unity 2021 LTS UI Toolkit workflow at runtime.
-
User interface design and implementation in Unity: This free e-book covers UI design and art creation fundamentals, and then moves on to instructional sections on UI development in Unity, mainly with UI Toolkit, but also with a chapter covering Unity UI.
Thanks for reading!