This is the seccond part of our article series explaining the UI Toolkit sample project, QuizU, now updated for Unity 6.1.
QuizU is 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. Make sure to install Unity 6.1 to follow along with the project in the Editor.
Here are the other posts in this series:
Post 1: The QuizU UI Toolkit sample: Now updated for Unity 6
Post 3: QuizU: State patterns for game flow
Post 4: QuizU: The Model View Presenter pattern
Post 5: QuizU: Event handling in UI Toolkit
Post 6: QuizU: Vector API
Post 7: QuizU: UI Toolkit performance tips
In this article we look at 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.
If you are new to UI Toolkit and looking for a quick introduction, feel free to check out our âGetting started with UI Toolkitâ course on Learn. Make sure to also download our e-book
UI Toolkit for advanced Unity developers (Unity 6 edition), a comprehensive guide to UI Toolkit and interface design in general.
Letâs dive in!
The main screens in QuizU & setting up the navigation flow
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 quiz gameplay happens 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 significantly from that of the legacy Unity UI (UGUI) system. Rather than placing and managing UI GameObjects directly in the Hierarchy, developers define layout and structure using UXML and apply styling via USS (Unity Style Sheets). These elements are instantiated into the Visual Tree, either using the UI Builder (Unityâs visual authoring tool) or programmatically through C# scripts. This promotes a declarative, data-driven UI development model more aligned with modern web development paradigms.
Ultimately, you only need a single UI Document and Panel Settings asset to render the UI to the screen. The custom UI management can then display part of the visual tree as needed and in our case as depending on the state machine
Within a project, itâs common for each screen or interface to be defined in its own UXML file, which holds the layout and arrangement for that particular screen. This modularity allows each UI designer or developer to work on individual UXMLs, reducing merge conflicts with teammates.
In the QuizU sample project, individual screen UXMLs are assembled into one master file, the UIScreens.uxml, which is then referenced inside and loaded in 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 and consistent across screens.
This workflow scales well for larger teams. Each contributor can take ownership of 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 overhead of GameObjects. This gives you better performance and clearer separation of concerns in UI development.
This is what the hierarchy UIScreens.uxml in the QuizU sample looks like assembled, consisting of multiple screens.
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. You can think of it as similar to having a stack of plates on top of each other. The last one that goes in is always on top and thus the first one to remove as well.
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).
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.
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 UI state, represents one âunitâ of the interface, 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. If the player then opens the pause menu from the game this 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 navigation pattern is widely used in UI development 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 serves as the framework for creating functional âunitsâ of the user interface. It provides a consistent interface and shared behavior for all UI screens.
The QuizU project includes classes like GameScreen, StartScreen, PauseScreen, etc., all of which derive from and extend 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.
public virtual void Initialize()
{
if (m_HideOnAwake)
{
HideImmediately();
}
m_EventRegistry = new EventRegistry();
m_EventRegistry.RegisterCallback<TransitionEndEvent>(m_RootElement, ParentElement_TransitionEnd);
}
- Show: This method shows the UI element with a transition if enabled.
public virtual void Show()
{
// Use helper class to run coroutines
Coroutines.StopCoroutine(ref m_DisplayRoutine);
m_DisplayRoutine = Coroutines.StartCoroutine(ShowWithDelay(m_TransitionDelay));
}
- Hide: This method hides the UI element with a transition if enabled.
public virtual void Hide(float delay = 0f)
{
// Use helper class to run coroutines
Coroutines.StopCoroutine(ref m_DisplayRoutine);
m_DisplayRoutine = Coroutines.StartCoroutine(HideWithDelay(delay));
}
- HideImmediately: This method hides the UI element without a transition.
public void HideImmediately()
{
m_RootElement.style.display = DisplayStyle.None;
}
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 post #5 in this series.
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 the visibility of the UI screens by turning them on and off as needed. It uses a stack-based system to manage all of the UIScreens under its control, with only one screen active at any given 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â in history 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
The screen stack design demonstrated here is one approach for how you might work with UI Toolkit. Evaluate your applicationâs needs and adapt design patterns accordingly.Some projects may benefit more from a âtab-basedâ (e.g., settings panels) or âdrawer-basedâ (e.g., side menus) 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
IDisposableinterface. This allows the GameScreen to dispose of them at once when invoking theDisablemethod (see post #5, Event Handling in UI Toolkit for more information about using an EventRegistry). -
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 with UI Toolkit to create UI systems for your Unity projects. Remember that you can always find more support in the UI Discussions forum and UI Toolkit 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 and SOLID. More Unity best practices guides are available in documentation.






