Hello everyone,
This is the third part of our article series explaining the UI Toolkit sample project, QuizU. A small UI-based game project that showcases common techniques in game architecture and can help developers transition to UI Toolkit. You can download the project from the Unity Asset Store.
If you missed the first two, you can find the previous posts in this series here:
Today’s article is about how to manage numerous UIs at scale as your project expands. We’ll look at the technique we used for UI management in the QuizU application. This approach can help you organize your UI Toolkit-based interfaces as your application scales. It does so while keeping everything fast and responsive to the end user.
Read UI Toolkit at runtime: Get the breakdown to get started making your own runtime UIs in UI Toolkit. Also, see the User interface design and implementation e-book for a quick start guide to UI Toolkit and interface design in general.
Let’s dive in!
Test your knowledge with QuizU.
The QuizU mini-game uses seven main screens. These typify what you might find in many applications:
-
Start Screen: This is the first screen, typically featuring the game’s logo/ branding and a “start” or “play” button.
-
Main Menu Screen: From this screen the player can navigate to various parts of the game such as the levels, settings, or other menus.
-
Settings Screen: This is where the player can adjust game preferences, like audio settings.
-
Level Selection Screen: This screen allows the player to choose the game level or stage they wish to play.
-
Game Screen: This is where the main gameplay happens. In the context of the QuizU game, this is where questions are presented, and answers are collected.
-
Pause Screen: This screen is displayed when the game is temporarily halted, providing options to quit or resume the game.
-
EndScreen: This win/lose screen displays the player’s score or performance when the game is complete while showing options to replay or return to the main menu.
These screens connect to form the UI navigation flow:
The UI navigation flow connects each UI screen.
Setting up UXMLs
In Unity’s UI Toolkit, UXML files are akin to blueprints for UI structures. They define hierarchies of UI elements, creating what’s known as a VisualTreeAsset. Each of these UXML files can be reused, nested, and combined with other UXMLs to construct complex and interactive UIs.
The workflow in UI Toolkit differs from the traditional UGUI approach. Instead of populating the Hierarchy with multiple UI objects, you’ll primarily work with UI elements through UXML files – either through UI Builder or directly from scripts.
Ultimately, you only need a single UI Document and Panel asset to render the UI on the screen. The custom UI management will then display part of the visual tree as needed.
Within a project, each screen can have its own UXML file which holds the layout and arrangement for that particular screen. This allows each UI designer or developer to work on individual UXMLs, reducing merge conflicts with teammates.
In the QuizU sample project, these separate UXMLs are assembled into one master file, the UIScreens.uxml, which is then referenced inside the UI Document. Every screen’s visual tree is designed to occupy the entire space of the parent container (positioning: absolute, width: 100%, height: 100%), ensuring each interface is independent of the others.
This workflow can benefit larger teams. Everyone can be responsible for a smaller, self-contained portion of the UI which can later be added to a unified UI Document. In this methodology, UXMLs are analogous to nested Prefabs, but without the actual GameObjects.
Look through the UIScreens.uxml in the QuizU sample to see how it’s assembled.
Assemble the UXMLs for use in one UI Document.
Screen stack navigation
The QuizU sample uses a pattern called the “screen stack” or “stack-based state machine” for turning UIs on and off. The idea is to manage screens by stacking them on top of each other.
By keeping your UI screens in a stack, this pattern provides a built-in mechanism to maintain user navigation history, similar to a web browser’s back button.
This Last-In-First-Out (LIFO) style of navigation stores each fullscreen UI as a layer, with only the top layer active at a time. This combines the idea of using a stack data structure with a finite-state-machine (FSM).
Though it shares similarities with the setup from state patterns for game flow, it is more simplified for this use case. Here, each screen functions as one state of the FSM. The state machine can only be in one state at a time, known as the current or active state.
Managing UIs with Stack-based State Machine or Screen Stack
Each screen, or state, represents one “unit” of the UI, essentially everything visible on the screen at one given time. As these units become active, they are pushed onto the stack. When they are no longer needed, they are popped off the stack. The topmost screen on the stack is always the active state. Transitions between states occur in response to certain conditions.
Opening a new screen pushes a new state onto the stack, and that becomes the active state. Closing a screen or navigating back pops the top state from the stack, making the previous state the active one.
For instance, selecting ‘Play’ from a main menu screen pushes the gameplay screen onto the stack. Opening a pause menu from the game then adds the pause screen to the stack. Resuming the game pops the pause screen off the stack, returning control to the gameplay screen.
While this pattern is fairly widespread, its implementations can vary. To illustrate one basic approach using UI Toolkit, we’ve created some sample classes, the UIScreen and UIManager. Both scripts are available in the QuizU\Assets\Quiz\Scripts\UI folder.
UIManager versus SequenceManager
In the sample project, both the UI Manager and the Sequence Manager operate at the same time and have some overlapping features. This particular application is UI dependent, and many events, like button clicks, can affect both managers.Here, the UI Manager handles tasks related to the UI, using the UI Toolkit. On the other hand, the Sequence Manager oversees the general flow of the entire application.
When you start combining UI elements with GameObjects in your game, having these two separate managers will become more logical, as they’ll work together to handle different aspects of the game.
UI Screen
The UIScreen class works in conjunction with the UI Manager to form the UI logic of the QuizU user interface.
UIScreen is an abstract base class that provides the framework for creating one functional “unit” of the user interface. Therefore, the QuizU project includes classes like GameScreen, StartScreen, PauseScreen, etc., all of which derive from UIScreen.
The base UIScreen class includes a few methods:
-
Initialize: This performs some basic setup, such as querying a UIDocument to find the topmost element within the Visual Tree Asset.
-
Show: This method shows the UI element with a transition if enabled.
-
Hide: This method hides the UI element with a transition if enabled.
-
HideImmediately: This method hides the UI element without a transition.
Essentially UIScreens are UI objects that can show or display themselves.
Each UI Screen can show or hide itself.
Note how UIScreen also provides a number of properties and settings, such as:
-
m_HideOnAwake: Whether the UI screen should be hidden when the game starts
-
m_IsTransparent: Whether the UI screen should be partially see-through
-
m_UseTransition: Whether the UI screen should use a transition when hiding or showing itself
-
m_TransitionDelay: The amount of time to wait before showing the UI screen after it is initialized
Also, take these into consideration when working with UIScreens:
-
Use the m_ParentElement field to access the topmost element of your UI screen’s hierarchy. This can be useful for UQuery operations. For larger UI hierarchies, searching from a smaller branch of the visual tree can be faster than querying from the rootVisualElement.
-
Enable m_UseTransition to fade the UIScreen on or off using USS style classes. Use the style’s transition duration and the m_TransitionDelay field to adjust the timing. When the transition finishes, a TransitionEndEvent then turns the parent element off entirely. This adds a small visual polish versus toggling the screen on or off abruptly.
-
The EventRegistry is an optional helper class that uses the IDiposable pattern to manage the registration and unregistration of event callbacks – much like how you would subscribe or unsubscribe to System.Actions or UnityEvents.
While the garbage collector usually takes care of removing the callback with its associated VisualElement, using the EventRegistry can help in certain situations. Learn more about the technique in the upcoming article 5 of this series (we’ll add a link to the article when it’s published). -
In this demo project, each UI screen has one corresponding UXML file with all the visual elements. Then, they combine into one UIScreens.uxml. You can nest even more visual trees if you require additional complexity.
Once we have a UI Screen that can turn on and off, we’ll need another class, the UIManager, to maintain the screen stack.
Optimization tip
To reduce overhead, the UIScreen derives from a System.Object instead of a MonoBehaviour. Though they lose the ability to set fields in the Inspector, they can reference project assets through Resources.Load or Addressables. Also, you can initialize specific fields in the UIScreen constructor.
UIManager
The UIManager class is responsible for turning UI screens on and off. It uses a stack to manage all of the UIScreens under its control, with only one screen active at a time.
A stack works somewhat similar to Lists but uses a Last-In-First-Out (LIFO) data structure. This means that the last element added is the first one to be removed, similar to stacking a deck of cards. This is useful for situations where you need to reverse the order of elements or maintain a specific order of actions, such as going back in your navigation. Here are a few takeaways from the QuizU implementation:
-
The m_History stack starts with the Main Menu Screen, which functions as the “home screen.” Showing a UIScreen means pushing onto this LIFO stack. The top screen is always visible and active. This history stack maintains a collection of previously shown screens, allowing the system to “go back” until it reaches the home screen.
-
UIScreens can be partially transparent or see-through. This provides an overlay effect, where the top screen’s see-through areas reveal elements of the screens below. This allows screens underneath to be inactive but visible.
-
Game events are tied to each screen. UIManager registers event listeners in the OnEnable method. For instance, the UIEvents.MainMenuShown event activates the UIEvents_MainMenuShown method, showing the main menu screen.
-
UIScreen instances are stored in a master list using Reflection. This can help hide or show all of the UIs at once.
-
The UIManager unregisters events in the OnDisable method. This prevents errors from dangling event listeners if the UIManager instance is deactivated or destroyed.
Used together, the UIScreen and UIManager form the “screen stack” or “stack-based screen” pattern often used in game development.
Adapting UI design patterns
Consider the screen stack design presented here as one approach for how you might work with UI Toolkit. Evaluate your application’s needs and adapt it accordingly. Some projects may benefit more from a “tab-based” or “drawer-based” navigation scheme.You can combine these with the screen stack design or use a different UI pattern altogether. Keep in mind that each choice in software design comes with tradeoffs. For example, our stack uses a set of pre-instantiated UXMLs combined into a single file; essentially this acts as a custom-created pool where we deactivate screens not currently in use. If you have a large number of UIs, you may need to balance this with memory usage. It may be more efficient to instantiate the VisualTreeAssets at runtime.
UI Toolkit gives you a lot of flexibility here. Weigh the pros and cons of each design pattern and then decide as a team how to proceed.
GameScreen example: Controlling other UIs
If you’re building a game that requires more complex UI Screens, it can be beneficial to divide them into smaller, more manageable components. The GameScreen.cs file in the QuizU sample shows one example of how to break the UI logic into smaller scripts.
Though the UI consists of one UXML file, the C# logic spans several UI classes:
-
GameScreen: This is the controller for several other UI displays.
-
QuestionDisplay: This UI displays and formats the current question.
-
ResponseDisplay: This displays the user’s response to a question.
-
MessageDisplay: This message bar at the bottom of the screen gives feedback to the user for correct and incorrect answers.
-
ProgressDisplay: This progress bar at the top of the screen represents the user’s progress through the quiz.
-
LifeBarDisplay: This icon shows the remaining lives or guesses the user has left.
This organizational scheme divides the GameScreen like so:
The GameScreen encompasses several smaller classes.
The GameScreen class functions loosely as the controller for its smaller constituent displays. It’s responsible for creating and disposing of the smaller displays under its management:
public class GameScreen : UIScreen
{
ResponseDisplay m_ResponseDisplay;
QuestionDisplay m_QuestionDisplay;
MessageDisplay m_MessageDisplay;
ProgressDisplay m_ProgressDisplay;
LifeBarDisplay m_LifeBarDisplay;
public GameScreen(VisualElement rootElement): base(rootElement)
{
m_ResponseDisplay = new ResponseDisplay(rootElement);
m_QuestionDisplay = new QuestionDisplay(rootElement);
m_MessageDisplay = new MessageDisplay(rootElement);
m_ProgressDisplay = new ProgressDisplay(rootElement);
m_LifeBarDisplay = new LifeBarDisplay(rootElement);
m_LifeBarDisplay.AssignTooltip("Guesses remaining");
}
public override void Disable()
{
base.Disable();
m_ResponseDisplay.Dispose();
m_QuestionDisplay.Dispose();
m_MessageDisplay.Dispose();
m_ProgressDisplay.Dispose();
m_LifeBarDisplay.Dispose();
}
}
Note the following about this implementation:
-
The GameScreen class does not inherit from MonoBehaviour, reducing any unnecessary overhead. This foregoes built-in life cycle events (like OnEnable, Awake, and Start) for initialization in lieu of using a constructor.
-
Each of the smaller displays implements the
IDisposable
interface. This allows the GameScreen to dispose of them at once when invoking theDisable
method (article 5 in this series will focus on Event Handling in UI Toolkit using an EventRegistry, we’ll add a link to it here as soon as it’s published). -
In keeping with the base UIScreen class, the GameScreen constructor uses the topmost VisualElement,
rootElement
, to initialize the sub-displays. -
The smaller displays exercise a great deal of autonomy from the GameScreen itself. The GameScreen will destroy and dispose of them in Disable but otherwise, the m_ResponseDisplay, m_QuestionDisplay, et al. function as in independent UIs. They communicate with the rest of the interface via events.
Of course, implementations can vary depending on your needs. Your version might use only some of these techniques. For example, if you need the GameScreen logic most closely connected to the individual displays, each Display could reference the GameScreen more directly (or use local events).
Further reading
We hope that the QuizU project can help you get started in UI Toolkit to create UI systems for your Unity projects. Remember that you can always find more support in the Forums and Documentation.
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.