Create a ScreenManager
Code and workflows for navigate between different views using UI Builder and UI Toolkit
Last updated
Code and workflows for navigate between different views using UI Builder and UI Toolkit
Last updated
Navigate between different views using UI Builder by creating Navigation Scene with a ScreenManager.
Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Creating-a-ScreenManager
Often in games there is a "title" screen with options or settings the user sets up before going into the game. In this section we will implement the ability to switch between different UI views like that in this page.
We will create a new Scene called "NavigationScene." We choose the name "NavigationScene" because the users of Moetsi XR experiences usually need UI to "navigate" to the environment they want to join.
NavigationScene will have a GameObject that contains a "UI Document" Component which has a "Source Asset" field. We will put in a ScreenManager ("TitleSceneManager") that will control the display of different views in the Scene. You should think of the TitleSceneManager as the "switchboard" that decides which views are shown.
We will have 4 "views" within our TitleScreenManager:
Title Screen (the first view you see)
Host Game Screen (to host a local game)
Join Game Screen (to join a local game)
Manual Connect Screen (to manually connect to a game)
"ScreenManager" is not an official Unity term. It is a term we use to indicate a design pattern whereby a "managing" View handles the switching between different views.
You will notice a pattern for how we build UI. We start with an UXML. Usually we put a custom Visual Element (cVE) as the first child of the UXML.
The most basic building block in UI Toolkit is a visual element. The visual elements are ordered into a hierarchy tree with parent-child relationships. The diagram below displays a simplified example of the hierarchy tree, and the rendered result in UI Toolkit.
Visual elementsThe VisualElement class is the base for all nodes in the visual tree. The
VisualElement
base class contains common properties for all controls, such as styles, layout data, and event handlers. Visual elements can have children and descendant visual elements. For example, in the diagram above, the firstBox
visual element has three child visual elements:Label
,Checkbox
, andSlider
.You can customize the appearance of visual elements through stylesheets. You can also use event callbacks to modify the behavior of a visual element.
VisualElement
derives into subclasses that define additional behavior and functionality, such as controls. UI Toolkit includes a variety of built-in controls with specialized behavior. For example, the following items are available as built-in controls:
Buttons
Toggles
Text input fields
You can also combine visual elements together and modify their behavior to create custom controls. For a list of built-in controls, see the Control reference page.
Within that custom Visual Element (cVE) we put all the elements that come to mind when we think of UI: text, buttons, labels, input fields, images (these are all also "Visual Elements" in UI Toolkit terminology) and give each of the elements a "Name" so that we can reference them within our custom Visual Element (cVE).
Standard Visual Elements are Visual Elements provided by Unity with standard functionality (label, textfield, buttons, input fields, images). cVEs will be created by us and they do things like add callbacks to children Visual Elements or set data to Visual Elements.
We will follow the flow of UXML > cVE > Standard Visual elements throughout this gitbook.
Admittedly, this might be a bit confusing without any context so let's just get to it!
First let's begin by adding the necessary packages to our manifest
Open the manifest.json file in your Project folder and add these three lines to the bottom:
You might get warnings and errors after adding these packages.
This is because the packages are still a bit wonky. Fear not, these warnings and errors will not cause any problems; please ignore them. Each time you open the project the errors will appear, but they won't cause any trouble.
This does NOT mean from now on you can ignore all errors in the gitbook. Make sure you do not have any errors before you add these packages and note the type of errors that appear after you install the packages. These are UI Toolkit/UI Builder errors.
Create a new folder in "Assets" called "UI"
Within our "Scenes" folder, right-click, select "Create", then select Scene and name the newly created scene "NavigationScene"
Double click on "NavigationScene" to open it
Right click in the Hierarchy and create an empty GameObject called TitleScreenUI
Add an "Input System Event System (UI Toolkit)" component
Class EventSystemUse this class to handle input, and send events to a UI Toolkit runtime panel.
Also add a "UI Document" component
Class UIDocumentDefines a Component that connects VisualElements to GameObjects. This makes it possible to render UI defined in UXML documents in the Game view.
Notice the field in the UI Document component called "Source Asset"
As mentioned in the Overview page of this section of the gitbook, this is going to be our "parent" for all our visual elements/uxmls
Open the UI folder, right-click inside, hover over "Create", navigate down to UI Toolkit, and select "Panel Setting Asset" name it "PanelSettings"
Class PanelSettingsDefines a Panel Settings asset that instantiates a panel at runtime. The panel makes it possible for Unity to display UXML-file based UI in the Game view.
We created a "Panel Settings" with default Unity values
No need to update most of these as the default values work fine
Digging into the weeds of each field in Panel Settings is a bit overkill for this gitbook, but if you're curious we recommend that you check out the documentation linked above
Select PanelSettings and in the inspector change Scale Model to "Constant Pixel Size"
Drag "PanelSettings" into the "Panel Settings" field in the UI Document component found in Inspector when TitleScreenUI is selected in Hierarchy
Right-click in our UI folder, hover over Create, navigate down to UI Toolkit, and select "UI Document" and name the file TitleScreenManager
This will be our parent uxml of our NavigationScene UI
Drag it to the Source Asset field of the UI Document component found in Inspector when TitleScreenUI is selected in Hierarchy
Now create your next 4 UI Documents (right-click in UI folder, Create, hover over UI Toolkit, and select UI Document, naming each individual UI Document as follows):
TitleScreen
HostGameScreen
JoinGameScreen
ManualConnectScreen
If you expand each of the uxmls (UI Documents) you will notice that they each have something called inlineStyle
We will have a shared USS that will allow us to share stylings across views
But a view might have some stylings that are specific to that one particular view and to no other views; these stylings are called "inline" styles
(AKA styles that are part of the document but do not come from a separate USS sheet)
For now, we're not going to paste in any code snippets into these uxmls we just made right now
Now let's create our 4 custom Visual Elements (cVEs)
Again, they will have the exact same names as the uxml files. Please try not to get confused!
To make each of the 4 cVEs, just right-click in the UI folder > Create > C# Script and name each file according to the four bullet points below, pasting the code snippet underneath into each cs files:
TitleScreenManager
HostGameScreen
JoinGameScreen
ManualConnectScreen
These are the custom Visual Elements (cVEs) we will nest in our UXMLs
In the code for each, you will notice that we work within 3 boilerplate "sections" (for lack of a better word) to build our UI. They are as follows:
UxmlFactory
(this will allow us to read from the UXML we are nested in so we can add functionality)
We will not alter this
this.RegisterCallback(OnGeometryChange)
We will not alter this
OnGeometryChange(GeometryChangedEvent evt)
This runs each time the layout is updated
Here we will assign any visual elements we want to interact with and register any callbacks
Think of it like Create() or OnCreate()
We have named our custom Visual Elements (cVEs) after the uxmls they will be nested in.
FYI: you are able to change the name of your cVEs and nest them anywhere; the name is not a dependency for working with uxml.
However, if you do change the name you must change it in the 3 places you see the name used in the files above:
Where the public class is defined (at the top)
Where you define the uxml factory
The constructor that contains registering the callback OnGeometryChange
Again, if you change the name of a cVE, the uxmls will not automatically pick up this change. Instead, you will need to manually update the names in the uxml files themselves.
We at Moetsi like to match names as a rule of thumb (although it may seem confusing) to provide clarity on exactly what each cVE should do. We don't find it too confusing because the uxmls and cVEs are different file types so the little iconography next to the file names symbolize the difference between the two.
These are a lot of opinionated decisions we made for the sake of the gitbook. Anyone with frontend webapp experience knows how many different ways you can skin a cat when it comes to a front end. We are trying to show you all the techniques you can use with latest packages. Actual structure and separation of concerns in projects can vary a lot based on architecture approach.
Class VisualElement.UxmlFactoryInstantiates a VisualElement using the data read from a UXML file.
GeometryChangedEventEvent sent after layout calculations, when the position or the dimension of an element changes. This event cannot be cancelled, it does not trickle down, and it does not bubble up.
Now that we have created our uxmls and cVEs let's open up UI Builder
Navigate to "Window', select "UI Toolkit", then choose "UI Builder"
We are now ready to to structure our components
You might not have "TitleScreenManager" already available in the Hierarchy when you open UI Builder, don't worry we open it up as our next step below
We created our UI components in this section and we are prepared to structure them in the next section
We created NavigationScene
We added a TitleScreenUI GameObject
We added Event System (UI Toolkit) and UI Document
We created uxmls
TitleScreenManager
TitleScreen
HostGameScreen
JoinGameScreen
ManualConnectScreen
We created custom Visual Elements (cVEs)
TitleScreenManager
HostGameScreen
JoinGameScreen
ManualConnectScreen
We navigated to UI Builder
We are going to use the UI Builder to put our different components together and to also add additional standard Visual Elements. In the next section we will also do styling in the UI Builder.
You will notice that there is no way to create new Visual Elements through the UI Builder. This is why we started this section by creating those items first. It is generally a good idea to plan out your views and structure before working with UI Builder to make construction a bit easier.
The main UI Builder window
StyleSheets: Manage StyleSheets and USS Selectors to share styles across UI Documents (UXML) and elements.
Hierarchy: Select, reorder, reparent, cut, copy, paste, delete elements in the UI hierarchy of your document.
Library: Create new elements or instance other UI Documents (UXML).
Viewport: See what your UI Document (UXML) looks like and edit elements visually directly on the Canvas.
Code Previews: See what the UI Builder is creating as text for both the UI Document (UXML) and the StyleSheets (USS).
Inspector: Use it to change the attributes and style properties of the selected element or USS selector.
It's a good idea to watch Unity's UI Builder video to get an overview of the different parts of UI Builder. This gitbook will touch on most aspects of UI Builder, but to get a more exhaustive understanding we recommend watching Unity's run-through of the entire system.
While UI Builder is open, navigate to the "Library" section and take a look at what exists on the "Standard" tab
Standard Visual Elements are broken out into two sections:
Containers
Controls
We will use these Standard Visual Element to build our UI in UI Builder
Now click on the Project tab
This tab is broken out into
UI Documents (UXML)
Custom Controls (C#) (Our custom Visual Elements, cVEs)
Expand the UI folder in the Assets folder and notice the uxmls we created
Scroll to TitleScreenManager, hover over, and click on the icon that appears on the right
We have now loaded our TitleScreenManager uxml into our Hierarchy
Be careful not to double-click any items in project as that actually loads into the Hierarchy (unless you intend to load the item you double clicked into your current Hierarchy)
Also notice that you cannot "open" our cVEs like you can our uxmls
There is no icon that appears when you hover over a cVE
In order to update the contents of our cVEs, we must go into the actual file
Drag TitleScreenManager.cs from Custom Controls into the Hierarchy
Click on TitleScreenManager.uxml in the Hierarchy and hit save
Generally, always click on the uxml and save after making any change
UI Builder is still a bit wonky, so it's is better to be safe than sorry and a necessary step to saving is selecting the uxml in the Hierarchy before hitting save
UI BUILDER CURRENTLY HAS A "DOUBLE SAVE BUG"
Currently if you "save" after a change in UI Builder, it will save fine the first time.
But if you hit save AGAIN, immediately without making changes (and sometimes when making a change regarding children/hierarchy), it will save, BUT UI BUILDER WILL VISUALLY LOSE DATA.
If you then hit save a third time, it will save the lost data state (aka erasing everything).
So...
Only "save" once per change And if you hit "save" and UI Builder loses data DO NOT HIT SAVE AGAIN (or it will save the lost data state) Instead change to another UXML in the Project hierarchy and then switch back (UI Builder will correctly show the data again)
We have reached out to Unity to fix this issue. You can see it described here: https://forum.unity.com/threads/saving-twice-in-a-row-in-ui-builder-causes-errors-and-lost-data.1296954/
Below in the GIF we demonstrate this bug (data being lost in UI Builder) and then show that by switching to another UXML and back you can return your data state.
We now have our custom Visual Element (cVE) nested in our uxml
Woo! Our ScreenManager is all set up!
FYI: Having a "ScreenManager all set up" is when there is a parent UXML + a child cVE, which will control all other children uxmls/Visual Elements
Click on our TitleScreenManager cVE in Hierarchy and notice how the large blue highlighted area shrinks down to a line when we click from the TitleScreenManager uxml to the cVE
You may need to expand the window to see this
This is because our cVE is currently styled to consume only as much "room" on the screen as its contents take up
And because our cVE is empty, the styled area shrinks
It is important to note that the size of the TitleScreenManager uxml in UI Builder does not have a connection to the size the game will be rendered at.
The uxml will take as much space as exists (similar to view height and view width in Web Development).
By resizing this uxml canvas, we can test how the UI will react to different screen sizes/resolutions.
Within the Viewport pane, you can find the Canvas, a floating re-sizable edit-time container that contains a live version of the UI Document (UXML) being edited. If you can't see it, try clicking on the Fit Canvas button in the Viewport toolbar to bring it into view. Any settings related to the Canvas, like its size, are not saved as part of the UI Document (UXML) but will be remembered (using an internal separate settings file) for the next time you open the same UI Document (UXML).
From UI Builder's Setting up the edit-time Canvas documentation
Maximize the UI Builder to make it easier to navigate
Click on TitleScreenManager uxml and resize it to see that it the blue highlighted area always takes up the size of the canvas
Type in values in Canvas Size in the Inspector to see the canvas resize
Drag the sides of the canvas and notice how the "Canvas Size" values in the Inspector change
Hit "Fit Canvas" in the top and it will automatically set to be contained within the Viewport
We aren't going to dive much deeper into the functionality of the UI Builder because it's so thoroughly explained in Unity's UI Builder talk
We will move on and focus on how to make certain functionalities work
Click on our TitleScreenManager cVE and in the Inspector, type TitleScreenManager in the "Name" field
In "StyleSheets" (top left) hit the plus sign and select "Create new USS"
Create a new USS named "TitleScreenUI" in the "UI" folder (you might need to expand the Finder window in order to navigate to the UI folder)
If you don't save often, sometimes UI Builder can get confused
Now select the TitleScreenManager cVE and in the Inspector navigate to "Flex"
Update "Grow" to 1
Notice how the highlighted area now encompasses the entire canvas
We have "styled" our TitleScreenManager cVE to "grow" to the entire canvas
Now under "StyleSheet" in the Inspector type ".screen" into the Style Class List input field and then click "Extract Inlined Styles to New Class"
Nice! We just made a new "class" of style called ".screen"
We can now apply this style to other Visual Elements and it will update the Visual Element's styling
We will use this class for all our "screens"
Now expand TitleScreenUI.uss in StyleSheets
Here is where we see that the UI Builder has automatically put our extracted class in our USS
Thanks, UI Builder!
Now let's drag our 4 other uxmls into our TitleScreenManager cVE
They should be nested within the cVE
We now have our initial setup of our TitleScreenManager
We nested our TitleScreenManager cVE within the uxml
We created TitleScreenUI USS
We created a .screen class
We nested TitleScreen, HostGameScreen, JoinGameScreen, ManualConnectScreen uxmls in our TitleScreenManager cVE
We are going to add Visual Elements to our nested uxmls (TitleScreen, HostGameScreen, JoinGameScreen, ManualConnectScreen). While TitleScreenManager.uxml is the focus in our Hierarchy, it is not possible to add Visual Elements to the nested uxmls. We will quickly show you what we mean.
In UI Builder, navigate to the Library, select the Standard tab, and drag a VisualElement into our nested TitleScreen uxml
Save TitleScreenManager.uxml
Now go to Library, then to the Project tab, and open the TitleScreen uxml
No VisualElement!
That is because we did not insert the VisualElement into the actual TitleScreen uxml file
It is part of the TitleScreenManager uxml (because it was the focus in our Hierarchy)
Delete the VisualElement and save
TitleScreenManager.uxml is the file that's actually keeping track of this new VisualElement
This approach of not actually nesting the VisualElement in the child uxml can work, but in this gitbook we want to have self-contained files for separation of concerns
So let's take a brief look at the different ways we can edit these nested uxmls
Right-click on TitleScreen in TitleScreenManager's Hierarchy and notice the options on the bottom:
Open in UI Builder
Open Instance in Isolation
Open Instance in Context
Show In Project
We will be editing our uxmls through the "Open in UI Builder" option
This allows you to focus on the exact screen you are working on
This will open the uxml in the same way that navigating to Library > Project > UI Documents > uxml selection would
You can use the other options to edit while in the context of the greater Hierarchy but we have found it easier to focus on one view at a time when doing anything other than just minor touchups
Click on "Open in UI Builder"
The uxml will take up as much space as its parent
The parent of TitleScreenManager uxml is the actual display so it will take up as much space as the display
The parent of TitleScreen is our cVE (TitleScreenManager) which we have made take up the entire display as well (through the .screen class)
We are going to add a VisualElement that takes up the entire screen to this uxml so that we can make full use of the display when adding our content
This might seem a little confusing at first but we want to take up the whole area with a parent element
We just made a full screen container, why do we need another one?!
Think of it as turning on the lights to a room
We want the light everywhere so we can place our objects relative to each other knowing the boundaries of the room
If we were to place our items in the room in the dark, we will not be able to control where they are relative to boundaries
(Eh, that wasn't our best analogy. If you have a better one please reach out in the Moetsi Discord!)
Drag a standard VisualElement to the Hierarchy
Change the VisualElement's name to "screen"
Instead of changing the name when the item is selected in Inspector, you can also do this from the Hierarchy by renaming the element "#screen"
The "#" is a convention from CSS (web development)
In StyleSheets (top left of UI Builder), click the "+" and "Add Existing USS" and select "TitleScreenUI.uss"
When the #screen VisualElement is selected in Hierarchy, go to Inspector and under StyleSheet type in ".screen" in the Style Class List field. Click "Add Style Class to List"
This will add ".screen" from TitleScreenUI.uss to "#screen"
We can see that screen now has the same styling as our .screen class and grows to take up space
We want to be able to differentiate screens so let's change the background
Select "#screen" and in the Inspector scroll down to "Background" and change the color to a blueish color (or whatever color you like)
Next, let's add 3 buttons as children of screen
Find the Button in the Standard tab under Controls, and drag three of them under #screen in Hierarchy as children
Name = host-local-button
Text: Host game
Name = join-local-button
Text: Join game
Name = manual-connect-button
Text: Manually connect
Now we will change the backgrounds and add a button to each of the other views as well
We will navigate to one of the 3 other views from TitleScreen using TitleScreen's 3 buttons
The other views will have a button back to TitleScreen
For the other 3 screens we will not add a standard VisualElement as we did for TitleScreen, we will add our cVEs
We did not need to add a cVE for TitleScreen to add callbacks to the buttons because we will use the cVE TitleScreenManager
If this is still a bit confusing, please bare with us and once we start adding callbacks it should make more sense
Hit save!
Open up HostGameScreen by navigating to the Project tab in Library and clicking the open icon when hovering over it:
Hit + under StyleSheets and click "Add Existing USS," select TitleScreenUI USS
Drag HostGameScreen from "Custom Controls" in Library into the Hierarchy
Add the .screen class to the HostGameScreen cVE (found in the StyleSheet section, type ".screen" and click Add Style Class to List)
Name the cVE "HostGameScreen" (by typing in HostGameScreen into the "Name" field in Inspector)
Next
Make the background greenish
Add a Label (drag one from Controls under Standard tab in Library)
Don't name the Label
Text = "Host Screen"
Add a button
Name = "back-button"
text = "Back"
We have our basic (albeit not very pretty) HostGameScreen
You might be wondering why didn't we name the label?
Or not, but we are going to tell you anyway!
In order to reference VisualElements in our custom Visual Elements (cVEs) they must have names to identify them (like an id)
If we don't plan on interacting with an element, then there is no real functional need to name them
Other than to make the Hierarchy a bit more clear to know what is going on
Which we will eventually do in the next section, "Styling a View," but it's too much to do right now
Next up open JoinGameScreen
Add TitleScreenUI USS
Drag JoinGameScreen from "Custom Controls" in Library into the Hierarchy
Add the .screen class to the JoinGameScreen cVE
Name the cVE "JoinGameScreen"
Next
Make the background orange-ish
Add a Label
Text = "Join Screen"
Add a button
Name = "back-button"
text = "Back"
One more screen to go, ManualConnectScreen
Open ManualConnectScreen
Add TitleScreenUI USS
Drag ManualConnectScreen from "Custom Controls" in Library into the Hierarchy
Add the .screen class to the ManualConnectScreen cVE
Name the cVE "ManualConnectScreen"
Next
Make the background purplish
Add a Label
Text = "Manual Connect Screen"
Add a button
Name = "back-button"
text = "Back"
Now let's navigate to TitleScreenManager
What is going on why is it when we click on each of our nested uxmls the highlighted area is so small?
We have not added the .screen class to all the uxmls themselves
Just the Visual Elements within them
Let's fix that
Add the .screen class to each of the nested uxmls
Okay so they are not tiny any more, they grow to take the space but now there are 4 equally spaced screens
We want to only display one screen at a time
Click on HostGameScreen in the Hierarchy and in the Inspector navigate to the Display section and change the Display field from "flex" to "none" (you need to hover over the little icons to know which one is "flex" and which one is "none")
Repeat for JoinGameScreen and ManualConnectScreen
We have our single TitleScreen visible with 3 buttons that will take us to the other views
One more thing to checkout in UI Builder:
At the bottom you can pull up "UXML Preview" and "USS Preview" and see that what we created in TitleScreenManager.uxml is being actually saved as code
This illustrates that UI Builder is almost like an "interpreter" of the uxml and USS we are creating and all the changes we make are saved and reflected in these files
In the next step we will add callbacks to the buttons we created to change the display values of different screens
Now now have our NavigationScene UI set up
We added TitleScreenUI USS to each uxml
We added a .screen class, background, name, buttons and labels to each view
We added the .screen class to each uxml
We updated the Display property in the inspector to "none" of
HostGameScreen
JoinGameScreen
ManualConnectScreen
Now that we have our screens and buttons set up, we want to add logic to be able to toggle between different views.
The TitleScreenManager cVE will handle adding callbacks to the TitleScreen uxml buttons and the back buttons that return to the uxml. This way we have a single cVE that we know is in charge of displaying views (separation of concerns).
Close out of UI Builder and open up TitleScreenManager.cs (the cVE)
We are going to need to declare 4 variables for our 4 views at the top
TitleScreen
HostGameScreen
JoinGameScreen
ManualConnectScreen
Then we are going to have to "query" our nested children by name, and assign them to those variables
Once we have our views, we will then query within those views to find our buttons and assign callbacks
Finally, we will create functions to hide and display views when different buttons are pressed
Paste the code snippet below into TitleScreenManager.cs:
Now hit play and check out the navigation
There may be console warnings Internal: deleting an allocation that is older than its permitted lifetime of 4 frames (age = 5)
if the frame rate is very fast
We now have our TitleScreenManager functioning
We updated our TitleScreenManager custom Visual Element to provide callbacks and functions when but it is harmestsclicked
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/
git checkout 'Creating-a-ScreenManager'
Simplified hierarchy of the visual tree
You can directly resize the Canvas inside the Viewport by dragging its edges or corners. For exact sizing, you can click on the header of the Canvas to access its settings via the Inspector pane, where you will see fields for Canvas height and width. In the same section, you can also lock the Canvas size to the size of the Unity Game Window using the Match Game View checkbox to better match a runtime UI environment:
With the Canvas selected, in the Inspector, you can change the Canvas background to make editing the UI in context easier. You can set it to be a solid color, a specific texture (ie. a mockup from a UI Designer), or a live view from a Camera in the currently open Unity Scene: