Navigate Between Scenes
Code and workflows to navigate between NavigationScene and MainScene using UI buttons

What will be developed on this page

Navigating between scenes triggered by UI Toolkit elements and creating client/server worlds appropriately
We will adding logic to navigate between NavigationScene and MainScene and properly handle creating and destroying ECS worlds.

Navigating from NavigationScene to MainScene

We have 3 views that could take us to MainScene:
    HostGameScreen
    JoinGameScreen
    ManualConnectScreen
Asteroid NavigationScene view flow diagram
Rather than have separate logic in each of their associated custom Visual Elements (cVEs), we are going to create a new script that handles all transitions from NavigationScene to MainScene, named "ClientServerLauncher." We will pull in references to each of these views and will add callbacks to the "Host Game" and "Join Game" buttons that will trigger the transition.
Later on in the next Multiplayer section we will add additional logic to ClientServerLauncher so that we can select which IP address we connect to as a client.
In this page we will implement ClientServerBootstrap to prevent triggering Client/Server world creation until we are ready to transition to MainScene and click one of our transition buttons.

Bootstrap

The default bootstrap creates client server Worlds automatically at startup. It populates them with the systems defined in the attributes you have set. This is useful when you are working in the Editor, but in a standalone game, you might want to delay the World creation so you can use the same executable as both a client and server.
To do this, you can create a class that extends ClientServerBootstrap to override the default bootstrap. Implement Initialize and create the default World. To create the client and server worlds manually, call ClientServerBootstrap.CreateClientWorld(defaultWorld, "WorldName"); or ClientServerBootstrap.CreateServerWorld(defaultWorld, "WorldName");.
The following code example shows how to override the default bootstrap to prevent automatic creation of the client server worlds:
1
public class ExampleBootstrap : ClientServerBootstrap
2
{
3
public override bool Initialize(string defaultWorldName)
4
{
5
var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);
6
GenerateSystemLists(systems);
7
8
var world = new World(defaultWorldName);
9
World.DefaultGameObjectInjectionWorld = world;
10
11
DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, ExplicitDefaultWorldSystems);
12
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world);
13
return true;
14
}
15
16
}
Copied!
    Currently, because we have the NetCode package installed, NetCode automatically creates Client/Server worlds when the application starts
      If you don't have the NetCode package installed, please go visit the previous "DOTS NetCode" section for installation instructions
    Click play while NavigationScene is open then navigate to Entity Debugger (Window > Analysis > Entity Debugger) and checkout the available worlds from the dropdown
Client and Server worlds automatically being created in NavigationScene
    We want to limit the worlds to DefaultWorld while in NavigationScene
      This is a preemptive measure we are taking because later on, in the Multiplayer section, we won't know whether we want to join a game as just a client or as a client/server, so we want to hold off on creating these NetCode worlds
      So we will implement NetCodeBootstrap
    Un-click Play and right-click inside the Multiplayer Setup folder, select "Create" to create a new C# script called NetCodeBootstrap
    Paste the code snippet below into NetCodeBootstrap.cs:
1
using Unity.Entities;
2
using Unity.NetCode;
3
using UnityEngine;
4
#if UNITY_EDITOR
5
using Unity.NetCode.Editor;
6
#endif
7
8
public class NetCodeBootstrap : ClientServerBootstrap
9
{
10
public override bool Initialize(string defaultWorldName)
11
{
12
13
var world = new World(defaultWorldName);
14
World.DefaultGameObjectInjectionWorld = world;
15
16
var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);
17
GenerateSystemLists(systems);
18
19
DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, ExplicitDefaultWorldSystems);
20
#if !UNITY_DOTSRUNTIME
21
ScriptBehaviourUpdateOrder.AddWorldToCurrentPlayerLoop(world);
22
#endif
23
return true;
24
}
25
}
Copied!
    Please be patient! We noticed in our testing that it takes a couple tries for Unity to pick up this new bootstrap script ⏳
    Now hit play in NavigationScene, then navigate to Entity Debugger to take a look at the available worlds
    Good, NetCodeBootstrap has prevented automatic creation of worlds 👍
    Now let's move onto creating ClientServerLauncher that will manually build (the previously automatically-created) Client and Server worlds
    Right-click in the NavigationScene Hierarchy and create a new empty GameObject called "ClientServerLauncher"
      Move it just below "LocalGamesDiscovery" in the scene Hierarchy
      Add a component to the GameObject that is a new script named "ClientServerLauncher"
        First create the file by right-clicking in the Multiplayer Setup folder and selecting Create > C# Script

🔑MAJOR KEY ALERT 🔑

It is recommended by Unity to create the Client and Server worlds BEFORE navigating to a scene with converted SubScenes (as opposed to first navigating to MainScene then creating client and server worlds).
When the scene is loaded it automatically triggers loading of all SubScenes it contains into all worlds. If you manually create the worlds you need to do so before you load the scene with your content or they will not stream in any sub-scenes. It would also be possible to manually trigger SubScene streaming - but in this case it would mean you need to manually keep track of all SubScenes.
    Now let's update ClientServerLauncher
    Paste the code snippet below into ClientServerLauncher.cs:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using System.Net;
5
using System.Net.Sockets;
6
using System.Net.NetworkInformation;
7
using Unity.Entities;
8
using Unity.NetCode;
9
using UnityEngine.UIElements;
10
using UnityEngine.SceneManagement;
11
12
public class ClientServerLauncher : MonoBehaviour
13
{
14
//These are the variables that will get us access to the UI views
15
//This is how we can grab active UI into a script
16
//If this is confusing checkout the "Making a List" page in the gitbook
17
18
//This is the UI Document from the Hierarchy in NavigationScene
19
public UIDocument m_TitleUIDocument;
20
private VisualElement m_titleScreenManagerVE;
21
//These variables we will set by querying the parent UI Document
22
private HostGameScreen m_HostGameScreen;
23
private JoinGameScreen m_JoinGameScreen;
24
private ManualConnectScreen m_ManualConnectScreen;
25
26
27
28
void OnEnable()
29
{
30
31
//Here we set our variables for our different views so we can then add call backs to their buttons
32
m_titleScreenManagerVE = m_TitleUIDocument.rootVisualElement;
33
m_HostGameScreen = m_titleScreenManagerVE.Q<HostGameScreen>("HostGameScreen");
34
m_JoinGameScreen = m_titleScreenManagerVE.Q<JoinGameScreen>("JoinGameScreen");
35
m_ManualConnectScreen = m_titleScreenManagerVE.Q<ManualConnectScreen>("ManualConnectScreen");
36
37
//Host Game Screen callback
38
m_HostGameScreen.Q("launch-host-game")?.RegisterCallback<ClickEvent>(ev => ClickedHostGame());
39
//Join Game Screen callback
40
m_JoinGameScreen.Q("launch-join-game")?.RegisterCallback<ClickEvent>(ev => ClickedJoinGame());
41
//Manual Connect Screen callback
42
m_ManualConnectScreen.Q("launch-connect-game")?.RegisterCallback<ClickEvent>(ev => ClickedJoinGame());
43
}
44
45
// Update is called once per frame
46
void Update()
47
{
48
49
}
50
51
void ClickedHostGame()
52
{
53
//When we click "Host Game" that means we want to be both a server and a client
54
//So we will trigger both functions for the server and client
55
ServerLauncher();
56
ClientLauncher();
57
58
//This function will trigger the MainScene
59
StartGameScene();
60
}
61
62
void ClickedJoinGame()
63
{
64
//When we click 'Join Game" that means we want to only be a client
65
//So we do not trigger ServerLauncher
66
ClientLauncher();
67
68
//This function triggers the MainScene
69
StartGameScene();
70
}
71
72
73
74
public void ServerLauncher()
75
{
76
//CreateServerWorld is a method provided by ClientServerBootstrap for precisely this reason
77
//Manual creation of worlds
78
79
//We must grab the DefaultGameObjectInjectionWorld first as it is needed to create our ServerWorld
80
var world = World.DefaultGameObjectInjectionWorld;
81
#if !UNITY_CLIENT || UNITY_SERVER || UNITY_EDITOR
82
ClientServerBootstrap.CreateServerWorld(world, "ServerWorld");
83
84
#endif
85
}
86
87
public void ClientLauncher()
88
{
89
//First we grab the DefaultGameObjectInjectionWorld because it is needed to create ClientWorld
90
var world = World.DefaultGameObjectInjectionWorld;
91
92
//We have to account for the fact that we may be in the Editor and using ThinClients
93
//We initially start with 1 client world which will not change if not in the editor
94
int numClientWorlds = 1;
95
int totalNumClients = numClientWorlds;
96
97
//If in the editor we grab the amount of ThinClients from ClientServerBootstrap class (it is a static variable)
98
//We add that to the total amount of worlds we must create
99
#if UNITY_EDITOR
100
int numThinClients = ClientServerBootstrap.RequestedNumThinClients;
101
totalNumClients += numThinClients;
102
#endif
103
//We create the necessary number of worlds and append the number to the end
104
for (int i = 0; i < numClientWorlds; ++i)
105
{
106
ClientServerBootstrap.CreateClientWorld(world, "ClientWorld" + i);
107
}
108
#if UNITY_EDITOR
109
for (int i = numClientWorlds; i < totalNumClients; ++i)
110
{
111
var clientWorld = ClientServerBootstrap.CreateClientWorld(world, "ClientWorld" + i);
112
clientWorld.EntityManager.CreateEntity(typeof(ThinClientComponent));
113
}
114
#endif
115
}
116
117
void StartGameScene()
118
{
119
//Here we trigger MainScene
120
#if UNITY_EDITOR
121
if(Application.isPlaying)
122
#endif
123
SceneManager.LoadSceneAsync("MainScene");
124
#if UNITY_EDITOR
125
else
126
Debug.Log("Loading: " + "MainScene");
127
#endif
128
}
129
}
Copied!
    You'll notice that there are 3 key functions in ClientServerLauncher:
      ServerLauncher (creates server world)
      ClientLauncher (creates client world(s) if Thin Clients exist)
      StartGameScene (triggers loading MainScene)
    Depending on whether we click "Host Game" or "Join Game" we trigger 2 or 3 of these functions
      We always trigger StartGameScene and ClientLauncher
      We launch ServerLauncher only if we are a host
    Now that we are in the Navigating Between Scenes section of the gitbook, we are going to update GhostSpawnClassificationSystem
      We grab "CameraAuthoringComponent" in PlayerGhostSpawnClassification
      Now that we change scenes we can't guarantee that the component will be ready when we need it
      We will add RequireSingletonForUpdate<CameraAuthoringComponent>()to ensure that we do not try and grab CameraAuthoringComponent before it has been converted from PrefabCollection in ConvertedSubScene
    Add the line below in the OnCreate() method in PlayerGhostSpawnClassificationSystem.cs:
      We placed the line right after RequireSingletonForUpdate<NetworkIdComponent>();
1
RequireSingletonForUpdate<CameraAuthoringComponent>();
Copied!
    Select ClientServerLauncher in the Hierarchy, click Add Component in Inspector and add Client Server Launcher
    Drag TitleScreenUI GameObject from the Hierarchy onto the "Title UI Document" field under the Client Server Launcher component in Inspector while ClientServerLauncher is still selected in Hierarchy
    We now need to add our scenes to our build settings so we can navigate to them
      Navigate to MainScene, go to "Build Settings..." and click "Add Open Scenes"
      Return to NavigationScene, go to "Build Settings..." and click "Add Open Scenes"
    Go to Multiplayer > PlayMode Tools > set the Num Thin Clients equal to 1
Updating Title UI Document, build settings, and Num Thin Clients
    Hit play, and then click Host Game
Navigating to MainScene with the proper amount of ClientWorlds
We are now able to transition to MainScene with the proper amount of NetCode worlds
    We created NetCodeBootstrap
    We created ClientServerLauncher GameObject
      Added a new script ClientServerLauncher
    We updated ClientServerLauncher to trigger creation of client/server worlds and trigger a scene
    We updated PlayerGhostSpawnClassification to wait for our CameraAuthoringComponent
    We added our scenes to our build settings
    We dragged TitleUI to our ClientServerLauncher's "Title UI Document" field

Navigating from MainScene to NavigationScene

Similar to how we created client/server worlds to transition to MainScene, we now need to destroy those client/server worlds when we return to NavigationScene. We will also delete all entities created so we can start again in NavigationScene with a "blank slate".
    Let's update our ClientServerConnectionHandler to be able to take over these new "clean-up" duties
      This is a good place to put "clean-up" functionality because it's part of handling the connection between servers and clients
    Paste the code snippet below into ClientServerConnectionHandler.cs:
1
using System;
2
using System.Collections;
3
using System.Collections.Generic;
4
using UnityEngine;
5
using Unity.Entities;
6
using Unity.NetCode;
7
using UnityEngine.UIElements;
8
using UnityEngine.SceneManagement;
9
10
public class ClientServerConnectionHandler : MonoBehaviour
11
{
12
//this is the store of server/client info
13
public ClientServerInfo ClientServerInfo;
14
15
// these are the launch objects from Navigation scene that tells what to set up
16
private GameObject[] launchObjects;
17
18
//these will gets access to the UI views
19
public UIDocument m_GameUIDocument;
20
private VisualElement m_GameManagerUIVE;
21
22
void OnEnable()
23
{
24
// This will put callback on "Quit Game" button
25
// This triggers the clean up function (ClickedQuitGame)
26
m_GameManagerUIVE = m_GameUIDocument.rootVisualElement;
27
m_GameManagerUIVE.Q("quit-game")?.RegisterCallback<ClickEvent>(ev => ClickedQuitGame());
28
}
29
30
void Awake()
31
{
32
launchObjects = GameObject.FindGameObjectsWithTag("LaunchObject");
33
foreach(GameObject launchObject in launchObjects)
34
{
35
///
36
// checks for server launch object
37
// does set up for the server for listening to connections and player scores
38
//
39
if(launchObject.GetComponent<ServerLaunchObjectData>() != null)
40
{
41
//sets the gameobject server data (mono)
42
ClientServerInfo.IsServer = true;
43
44
//sets the component server data in server world(dots)
45
//ClientServerConnectionControl (server) will run in server world
46
//it will pick up this component and use it to listen on the port
47
foreach (var world in World.All)
48
{
49
//we cycle through all the worlds, and if the world has ServerSimulationSystemGroup
50
//we move forward (because that is the server world)
51
if (world.GetExistingSystem<ServerSimulationSystemGroup>() != null)
52
{
53
var ServerDataEntity = world.EntityManager.CreateEntity();
54
world.EntityManager.AddComponentData(ServerDataEntity, new ServerDataComponent
55
{
56
GamePort = ClientServerInfo.GamePort
57
});
58
//create component that allows server initialization to run
59
world.EntityManager.CreateEntity(typeof(InitializeServerComponent));
60
}
61
}
62
}
63
64
//
65
// checks for client launch object
66
// does set up for client for dots and mono
67
//
68
if(launchObject.GetComponent<ClientLaunchObjectData>() != null)
69
{
70
//sets the gameobject data in ClientServerInfo (mono)
71
//sets the gameobject data in ClientServerInfo (mono)
72
ClientServerInfo.IsClient = true;
73
ClientServerInfo.ConnectToServerIp = launchObject.GetComponent<ClientLaunchObjectData>().IPAddress;
74
75
//sets the component client data in server world(dots)
76
//ClientServerConnectionControl (client) will run in client world
77
//it will pick up this component and use it connect to IP and port
78
foreach (var world in World.All)
79
{
80
//we cycle through all the worlds, and if the world has ClientSimulationSystemGroup
81
//we move forward (because that is the client world)
82
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
83
{
84
var ClientDataEntity = world.EntityManager.CreateEntity();
85
world.EntityManager.AddComponentData(ClientDataEntity, new ClientDataComponent
86
{
87
ConnectToServerIp = ClientServerInfo.ConnectToServerIp,
88
GamePort = ClientServerInfo.GamePort
89
});
90
//create component that allows client initialization to run
91
world.EntityManager.CreateEntity(typeof(InitializeClientComponent));
92
}
93
}
94
}
95
}
96
}
97
98
// Start is called before the first frame update
99
void Start()
100
{
101
102
}
103
104
// Update is called once per frame
105
void Update()
106
{
107
108
}
109
//This function will navigate us to NavigationScene
110
void ClickedQuitGame()
111
{
112
113
#if UNITY_EDITOR
114
if(Application.isPlaying)
115
#endif
116
SceneManager.LoadSceneAsync("NavigationScene");
117
#if UNITY_EDITOR
118
else
119
Debug.Log("Loading: " + "NavigationScene");
120
#endif
121
}
122
123
//When the OnDestroy method is called (because of our transition to NavigationScene) we
124
//must delete all our entities and our created worlds to go back to a blank state
125
//This way we can move back and forth between scenes and "start from scratch" each time
126
void OnDestroy()
127
{
128
//This query deletes all entities
129
World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(World.DefaultGameObjectInjectionWorld.EntityManager.UniversalQuery);
130
//This query deletes all worlds
131
World.DisposeAllWorlds();
132
133
//We return to our initial world that we started with, defaultWorld
134
var bootstrap = new NetCodeBootstrap();
135
bootstrap.Initialize("defaultWorld");
136
137
}
138
}
Copied!
    Within MainScene, click on ClientServerConnectionHandler in the Hierarchy and drag the GameUI GameObject (also in Hierarchy) onto the "Game UI Document" field under Client Server Connection Handler component in Inspector, save, and navigate back to NavigationScene
Updating ClientServerConnectionHandler and returning to NavigationScene
    Hit play, host a game, play around, quit, and host again
Navigation between scenes functioning
    Note that hitting play in MainScene will no longer trigger starting the gameplay
      This is because client/server worlds are created in NavigationScene
    Note that hitting "Join Game" will not trigger gameplay because we are not connected to a server
      This will be updated in the Multiplayer section
We are now able to navigate back to NavigationScene by hitting the "Quit Game" button in MainScene
    We updated ClientServerConnectionHandler
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Navigating-Between-Scenes'
Last modified 7mo ago