Host or Join a Multiplayer Session on LAN
Code and workflows for hosting and/or joining a multiplayer session on LAN, and gracefully handling hosts/clients leaving

What you'll develop on this page

Host/join a game and gracefully handle host/client leaving
We will add logic to our transition between NavigateScene and MainScene that configures whether or not MainScene loads up ServerWorld and what IP address ClientWorld connects to.
We will also gracefully handle hosts or clients leaving a game.

Hosting and joining

First, some background

We have 3 views that could take us to MainScene:
    HostGameScreen
    JoinGameScreen
    ManualConnectScreen
Asteroid NavigationScene view flow diagram
We will update the custom Visual Elements (cVE) of HostGameScreen and ManualConnectScreen views to initially populate their values with system data. "Player Name" and "Game Name" will default to the host name of the machine running the application. We will update JoinGameScreen in the next section, "Broadcasting and Joining on LAN," when we work on broadcasting.
We will also update our LocalGamesFinder script (which we use to populate the table) with two new public variables, "Broadcast Ip Address" and "Broadcast Port." These values will be used in the next section, but we will update our ServerLaunchObject with these values now in this section to avoid doubling back to this flow diagram (which would be annoying for us and not really teach us anything).
We have a single script called "ClientServerLauncher" that handles the callbacks for these 3 views mentioned above. We will update ClientServerLauncher so that it will create the ClientLaunchObject and ServerLaunchObject that currently exist in our MainScene.
We will also update our ClientLaunchObjectData and ServerLaunchObjectData with our new broadcast, game, and player fields. ClientServerConnectionHandler will then pass this data onto our ClientServerInfo object.
We will update ClientDataComponent and ServerDataComponent to hold this additional information. We will also make GameNameComponent to be used by the client to store the game name.
To pass the game name from the server to the client we will update our load game workflow.
Finally, we will create GameOverlayUpdater to update the Game UI on the client with the new game and player information.

Now let's implement

    Let's update LocalGamesFinder used by LocalGamesDiscovery in NavigateScene to be the "source of truth" for which IP address and port our server will broadcast UDP packets on
      This is similar to how the game port is stored in MainScene in the ClientServerInfo GameObject
    Add these lines to LocalGamesFinder.cs. Put them before OnEnable()
1
///The broadcast ip address and port to be used by the server across the LAN
2
public string BroadcastIpAddress = "255.255.255.255";
3
public ushort BroadcastPort = 8014;
Copied!
Update the LocalGamesFinder file with 2 new public variables
    Now let's update HostGameScreen to automatically populate data based on host name and IP address
    To do this, we will first update the uxml so that the game's IP address is read-only
      This way, the host is not able to configure which IP address their machine can bind on
      Why didn't we just build it this way in the first place?!
        We thought there would be more "oomph" to this tutorial if we point out that a machine cannot configure which IP address they can start a server on in Unity πŸ˜‰
      ​
    With the code snippet below, we are updating the HostGameScreen uxml by changing a TextField VisualElement to a Label VisualElement
    Paste the code snippet below into HostGameScreen.uxml:
1
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
2
<Style src="TitleScreenUI.uss" />
3
<ui:ScrollView class="screen-scroll-container">
4
<HostGameScreen name="HostGameScreen" class="screen HostGameScreen">
5
<ui:VisualElement name="header" class="header">
6
<ui:Button text="Main Menu" display-tooltip-when-elided="True" name="back-button" class="quit-button main-menu-button" />
7
</ui:VisualElement>
8
<ui:VisualElement name="main-content" class="main-content" style="top: 108px; left: auto; position: absolute;">
9
<ui:Label text="3D XR Asteroids" display-tooltip-when-elided="True" name="title" class="title" />
10
<ui:VisualElement name="section-title-container" class="section-title-container">
11
<ui:Label text="Host a Local Game" display-tooltip-when-elided="True" name="section-title" class="section-title" style="color: rgb(160, 194, 114);" />
12
</ui:VisualElement>
13
<ui:VisualElement name="game-name-container" class="data-section">
14
<ui:TextField picking-mode="Ignore" value="HostNameValue" text="GameName" name="game-name" class="data-section-input" />
15
<ui:Label text="Your Game Name" display-tooltip-when-elided="True" name="game-name-label" class="data-section-label" />
16
</ui:VisualElement>
17
<ui:VisualElement name="game-ip-container" class="data-section">
18
<ui:Label text="127.0.0.1" display-tooltip-when-elided="True" name="game-ip" class="data-section-input" style="border-left-width: 0; border-right-width: 0; border-top-width: 0; border-bottom-width: 0;" />
19
<ui:Label text="Your Game&apos;s IP Address" display-tooltip-when-elided="True" name="game-ip-label" class="data-section-label" />
20
</ui:VisualElement>
21
<ui:VisualElement name="player-name-container" class="data-section">
22
<ui:TextField picking-mode="Ignore" value="PlayerNameValue" text="PlayerName" name="player-name" readonly="false" class="data-section-input" style="border-left-color: rgb(150, 191, 208); border-right-color: rgb(150, 191, 208); border-top-color: rgb(150, 191, 208); border-bottom-color: rgb(150, 191, 208);" />
23
<ui:Label text="Your Player Name" display-tooltip-when-elided="True" name="player-name-label" class="data-section-label" style="color: rgb(150, 191, 208);" />
24
</ui:VisualElement>
25
<ui:Button text="Host Game" display-tooltip-when-elided="True" name="launch-host-game" class="green-button" />
26
</ui:VisualElement>
27
</HostGameScreen>
28
</ui:ScrollView>
29
</ui:UXML>
Copied!
Updating HostGameScreen uxml to have an IP address as a label
    Now let's update the HostGameScreen custom VisualElement (cVE)
      We will pull the host name data and place it in both our Game Name field and our Player Name field
      We will pull the host IP address and place it in our IP Address label
    Paste the code snippet below into HostGameScreen.cs (cVE):
1
using System;
2
using System.Text;
3
using System.Net;
4
using System.Net.Sockets;
5
using System.Net.NetworkInformation;
6
using System.Collections;
7
using System.Threading.Tasks;
8
using System.Threading;
9
using System.Collections.Generic;
10
using UnityEngine;
11
using UnityEngine.UIElements;
12
using Unity.Entities;
13
using Unity.NetCode;
14
using UnityEngine.SceneManagement;
15
​
16
public class HostGameScreen : VisualElement
17
{
18
//We will update these fields with system data
19
TextField m_GameName;
20
Label m_GameIp;
21
TextField m_PlayerName;
22
​
23
//These are the system data variables we will be using
24
String m_HostName = "";
25
IPAddress m_MyIp;
26
​
27
public new class UxmlFactory : UxmlFactory<HostGameScreen, UxmlTraits> { }
28
​
29
public HostGameScreen()
30
{
31
this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
32
}
33
​
34
void OnGeometryChange(GeometryChangedEvent evt)
35
{
36
//
37
// PROVIDE ACCESS TO THE FORM ELEMENTS THROUGH VARIABLES
38
//
39
m_GameName = this.Q<TextField>("game-name");
40
m_GameIp = this.Q<Label>("game-ip");
41
m_PlayerName = this.Q<TextField>("player-name");
42
​
43
//
44
// INITIALIZE ALL THE TEXT FIELD WITH NETWORK INFORMATION
45
//
46
m_HostName = Dns.GetHostName();
47
// "best tip of all time award" to MichaelBluestein
48
// https://forums.xamarin.com/discussion/comment/1206/#Comment_1206
49
// somehow this is the best way to get your IP address on all the internet
50
foreach (var netInterface in NetworkInterface.GetAllNetworkInterfaces()) {
51
if (netInterface.OperationalStatus == OperationalStatus.Up &&
52
netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
53
netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet) {
54
foreach (var addrInfo in netInterface.GetIPProperties().UnicastAddresses) {
55
if (addrInfo.Address.AddressFamily == AddressFamily.InterNetwork) {
56
​
57
m_MyIp = addrInfo.Address;
58
}
59
}
60
}
61
}
62
​
63
//Now we set our VisualElement fields
64
m_GameName.value = m_HostName;
65
m_GameIp.text = m_MyIp.ToString();
66
m_PlayerName.value = m_HostName;
67
​
68
this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
69
}
70
}
71
​
Copied!
Updating HostGameScreen cVE to populate fields
    We also need to update our ManualConnectScreen uxml to have default data of 127.0.0.1
    Paste the code snippet below into ManualConnectScreen.uxml:
1
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
2
<Style src="TitleScreenUI.uss" />
3
<ui:ScrollView class="screen-scroll-container">
4
<ManualConnectScreen name="ManualConnectScreen" class="screen ManualConnectScreen">
5
<ui:VisualElement name="header" class="header">
6
<ui:Button text="Back Button" display-tooltip-when-elided="True" name="back-button" class="quit-button main-menu-button" />
7
</ui:VisualElement>
8
<ui:VisualElement name="main-content" class="main-content">
9
<ui:Label text="3D XR Asteroids" display-tooltip-when-elided="True" name="title" class="title" />
10
<ui:VisualElement name="section-title-container" class="section-title-container">
11
<ui:Label text="Manually Connect" display-tooltip-when-elided="True" name="section-title" class="section-title" />
12
</ui:VisualElement>
13
<ui:VisualElement name="game-ip-container" class="data-section">
14
<ui:TextField picking-mode="Ignore" value="127.0.0.1" text="127.0.0.1" name="game-ip" readonly="false" class="data-section-input" style="background-color: rgb(255, 255, 255);" />
15
<ui:Label text="Game&apos;s IP Address" display-tooltip-when-elided="True" name="game-ip-label" class="data-section-label" />
16
</ui:VisualElement>
17
<ui:VisualElement name="player-name-container" class="data-section">
18
<ui:TextField picking-mode="Ignore" value="PlayerNameValue" text="PlayerName" name="player-name" readonly="false" class="data-section-input" style="border-left-color: rgb(150, 191, 208); border-right-color: rgb(150, 191, 208); border-top-color: rgb(150, 191, 208); border-bottom-color: rgb(150, 191, 208);" />
19
<ui:Label text="Your Player Name" display-tooltip-when-elided="True" name="player-name-label" class="data-section-label" style="color: rgb(150, 191, 208);" />
20
</ui:VisualElement>
21
<ui:Button text="Join Game" display-tooltip-when-elided="True" name="launch-connect-game" class="blue-button" style="height: 120px;" />
22
</ui:VisualElement>
23
</ManualConnectScreen>
24
</ui:ScrollView>
25
</ui:UXML>
Copied!
Updating ManualConnectScreen uxml IP address default value
    Previously ManualConnectScreen uxml had a "Value" of "HostIPValue" even though the text read "127.0.0.1"
      This illustrates that there can be a difference between what TextField "shows" and what "value" is saved
        Once you update the TextField the value updates to what text is entered (automatically)
    Next, update our ManualConnectScreen cVE
      Here we only set our Player Name
      We will not automatically set the IP address using any information
        We leave the local host IP address as the default to hopefully inform our user that if this address is not updated, the client will try and connect to itself without a running server, which will not work
    Paste the code snippet below into ManualConnectScreen.cs (cVE):
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using UnityEngine.UIElements;
5
using System.Net;
6
using System.Net.NetworkInformation;
7
using System.Net.Sockets;
8
​
9
public class ManualConnectScreen : VisualElement
10
{
11
//We will update these fields with system data
12
TextField m_GameIp;
13
TextField m_PlayerName;
14
​
15
//These are the system data variables we will be using
16
string m_HostName = "";
17
​
18
public new class UxmlFactory : UxmlFactory<ManualConnectScreen, UxmlTraits> { }
19
​
20
public ManualConnectScreen()
21
{
22
this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
23
}
24
​
25
void OnGeometryChange(GeometryChangedEvent evt)
26
{
27
//
28
// PROVIDE ACCESS TO THE FORM ELEMENTS THROUGH VARIABLES
29
//
30
m_GameIp = this.Q<TextField>("game-ip");
31
m_PlayerName = this.Q<TextField>("player-name");
32
​
33
//
34
// INITIALIZE ALL THE TEXT FIELD WITH NETWORK INFORMATION
35
//
36
m_HostName = Dns.GetHostName();
37
​
38
//Now we set our VisualElement fields
39
m_PlayerName.value = m_HostName;
40
​
41
this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
42
}
43
}
Copied!
Updating ManualConnectScreen cVE to populate with system data
    With NavigationScene section, hit play
    Navigate to the Host Game view and the Manual Connect view
Checking out Host and Manual screen values population
    Great, our system data populates in the appropriate fields
    Now we need to update the data we will be passing through to MainScene in these scripts:
      ClientLaunchObjectData
      ServerLaunchObjectData
    First start by pasting the code snippet below into ClientLaunchObjectData.cs:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using System.Net;
5
​
6
public class ClientLaunchObjectData : MonoBehaviour
7
{
8
//This will be set by ClientServerLauncher in NavigationScene
9
//It will then be pulled out in MainScene and put into ClientServerInfo
10
public string PlayerName;
11
public string IPAddress;
12
​
13
// Start is called before the first frame update
14
void Start()
15
{
16
17
}
18
​
19
// Update is called once per frame
20
void Update()
21
{
22
23
}
24
}
Copied!
    Next, paste this code snippet into ServerLaunchObjectData.cs:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using System.Net;
5
​
6
public class ServerLaunchObjectData : MonoBehaviour
7
{
8
//This will be set by ClientServerLauncher in NavigationScene
9
//It will then be pulled out in MainScene and put into ClientServerInfo
10
public string GameName;
11
public string BroadcastIpAddress;
12
public ushort BroadcastPort;
13
​
14
// Start is called before the first frame update
15
void Start()
16
{
17
18
}
19
​
20
// Update is called once per frame
21
void Update()
22
{
23
24
}
25
}
Copied!
Updating our launch objects' data to hold new values
    Now go to Main Scene
    Drag our ClientLaunchObject and ServerLaunchObject from MainScene into our Scripts and Prefabs folder to make them prefabs
    Then delete them from MainScene
      We will now be able to reference them in our ClientServerLauncher script
    Great, now let's update ClientServerLauncher to grab data from the views and populate them in our launch objects
    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 will be used to grab the broadcasting port and address
15
public LocalGamesFinder GameBroadcasting;
16
private string m_BroadcastIpAddress;
17
private ushort m_BroadcastPort;
18
​
19
//These are the variables that will get us access to the UI views
20
//This is how we can grab active UI into a script
21
//If this is confusing checkout the "Making a List" page in the gitbook
22
23
//This is the UI Document from the Hierarchy in NavigationScene
24
public UIDocument m_TitleUIDocument;
25
private VisualElement m_titleScreenManagerVE;
26
//These variables we will set by querying the parent UI Document
27
private HostGameScreen m_HostGameScreen;
28
private JoinGameScreen m_JoinGameScreen;
29
private ManualConnectScreen m_ManualConnectScreen;
30
​
31
//These will persist through the scene transition
32
//MainScene will look for 1 or both of the objects
33
//Based on what MainScene finds it will initialize as Server/Client
34
public GameObject ServerLauncherObject;
35
public GameObject ClientLauncherObject;
36
​
37
//These pieces of data will be taken from the views
38
//and put into the launch objects that persist between scenes
39
public TextField m_GameName;
40
public TextField m_GameIp;
41
public Label m_GameIpLabel;
42
public TextField m_PlayerName;
43
​
44
​
45
void OnEnable()
46
{
47
​
48
//Here we set our variables for our different views so we can then add call backs to their buttons
49
m_titleScreenManagerVE = m_TitleUIDocument.rootVisualElement;
50
m_HostGameScreen = m_titleScreenManagerVE.Q<HostGameScreen>("HostGameScreen");
51
m_JoinGameScreen = m_titleScreenManagerVE.Q<JoinGameScreen>("JoinGameScreen");
52
m_ManualConnectScreen = m_titleScreenManagerVE.Q<ManualConnectScreen>("ManualConnectScreen");
53
​
54
//Host Game Screen callback
55
m_HostGameScreen.Q("launch-host-game")?.RegisterCallback<ClickEvent>(ev => ClickedHostGame());
56
//Join Game Screen callback
57
m_JoinGameScreen.Q("launch-join-game")?.RegisterCallback<ClickEvent>(ev => ClickedJoinGame());
58
//Manual Connect Screen callback
59
m_ManualConnectScreen.Q("launch-connect-game")?.RegisterCallback<ClickEvent>(ev => ClickedConnectGame());
60
}
61
62
// Start is called before the first frame update
63
void Start()
64
{
65
//We are grabbing the broadcasting information from the discover script
66
//We are going to bundle it with the server launch object so it can broadcast at that information
67
m_BroadcastIpAddress = GameBroadcasting.BroadcastIpAddress;
68
m_BroadcastPort = GameBroadcasting.BroadcastPort;
69
}
70
​
71
void ClickedHostGame()
72
{
73
//This gets the latest values on the screen
74
//Our HostGameScreen cVE defaults these values but player name and game name can be updated
75
//We set these VisualElement variables OnClick instead of OnEnable because this way
76
//we don't need to make a variable for player name for every view, just 1 and set which view
77
//we get it from OnClick (which is when we need it)
78
m_GameName = m_HostGameScreen.Q<TextField>("game-name");
79
m_GameIpLabel = m_HostGameScreen.Q<Label>("game-ip");
80
m_PlayerName = m_HostGameScreen.Q<TextField>("player-name");
81
​
82
//Now we grab the values from the VisualElements
83
var gameName = m_GameName.value;
84
var gameIp = m_GameIpLabel.text;
85
var playerName = m_PlayerName.value;
86
​
87
//When we click "Host Game" that means we want to be both a server and a client
88
//So we will trigger both functions for the server and client
89
ServerLauncher(gameName);
90
ClientLauncher(playerName, gameIp);
91
​
92
//This function will trigger the MainScene
93
StartGameScene();
94
}
95
​
96
void ClickedJoinGame()
97
{
98
//This gets the latest values on the screen
99
//Our JoinGameScreen cVE defaults these values but player name can be updated
100
//We set these VisualElement variables OnClick instead of OnEnable because this way
101
//we don't need to make a variable for player name for every view, just 1 and set which view
102
//we get it from OnClick (which is when we need it)
103
m_GameIpLabel = m_JoinGameScreen.Q<Label>("game-ip");
104
m_PlayerName = m_JoinGameScreen.Q<TextField>("player-name");
105
​
106
//Now we grab the values from the VisualElements
107
var gameIp = m_GameIpLabel.text;
108
var playerName = m_PlayerName.value;
109
​
110
//When we click "Join Game" that means we want to be only a client
111
ClientLauncher(playerName, gameIp);
112
​
113
//This function will trigger the MainScene
114
StartGameScene();
115
}
116
​
117
void ClickedConnectGame()
118
{
119
//This gets the latest values on the screen
120
//Our ManualConnectScreen cVE defaults these values but player name and IP address be updated
121
//We set these VisualElement variables OnClick instead of OnEnable because this way
122
//we don't need to make a variable for player name for every view, just 1 and set which view
123
//we get it from OnClick (which is when we need it)
124
m_GameIp = m_ManualConnectScreen.Q<TextField>("game-ip");
125
m_PlayerName = m_ManualConnectScreen.Q<TextField>("player-name");
126
​
127
//Now we grab the values from the VisualElements
128
var gameIp = m_GameIp.value;
129
var playerName = m_PlayerName.value;
130
​
131
//When we click "Join Game" that means we want to be only a client
132
ClientLauncher(playerName, gameIp);
133
​
134
//This function will trigger the MainScene
135
StartGameScene();
136
}
137
​
138
​
139
​
140
public void ServerLauncher(string gameName)
141
{
142
//Here we create the launch GameObject and load it with necessary data
143
GameObject serverObject = Instantiate(ServerLauncherObject);
144
DontDestroyOnLoad(serverObject);
145
​
146
//This sets up the server object with all its necessary data
147
serverObject.GetComponent<ServerLaunchObjectData>().GameName = gameName;
148
serverObject.GetComponent<ServerLaunchObjectData>().BroadcastIpAddress = m_BroadcastIpAddress;
149
serverObject.GetComponent<ServerLaunchObjectData>().BroadcastPort = m_BroadcastPort;
150
​
151
//CreateServerWorld is a method provided by ClientServerBootstrap for precisely this reason
152
//Manual creation of worlds
153
​
154
//We must grab the DefaultGameObjectInjectionWorld first as it is needed to create our ServerWorld
155
var world = World.DefaultGameObjectInjectionWorld;
156
#if !UNITY_CLIENT || UNITY_SERVER || UNITY_EDITOR
157
ClientServerBootstrap.CreateServerWorld(world, "ServerWorld");
158
​
159
#endif
160
}
161
​
162
public void ClientLauncher(string playerName, string ipAddress)
163
{
164
//Here we create the launch GameObject and load it with necessary data
165
GameObject clientObject = Instantiate(ClientLauncherObject);
166
DontDestroyOnLoad(clientObject);
167
clientObject.GetComponent<ClientLaunchObjectData>().PlayerName = playerName;
168
clientObject.GetComponent<ClientLaunchObjectData>().IPAddress = ipAddress;
169
​
170
//We grab the DefaultGameObjectInjectionWorld because it is needed to create ClientWorld
171
var world = World.DefaultGameObjectInjectionWorld;
172
​
173
//We have to account for the fact that we may be in the Editor and using ThinClients
174
//We initially start with 1 client world which will not change if not in the editor
175
int numClientWorlds = 1;
176
int totalNumClients = numClientWorlds;
177
​
178
//If in the editor we grab the amount of ThinClients from ClientServerBootstrap class (it is a static variable)
179
//We add that to the total amount of worlds we must create
180
#if UNITY_EDITOR
181
int numThinClients = ClientServerBootstrap.RequestedNumThinClients;
182
totalNumClients += numThinClients;
183
#endif
184
//We create the necessary number of worlds and append the number to the end
185
for (int i = 0; i < numClientWorlds; ++i)
186
{
187
ClientServerBootstrap.CreateClientWorld(world, "ClientWorld" + i);
188
}
189
#if UNITY_EDITOR
190
for (int i = numClientWorlds; i < totalNumClients; ++i)
191
{
192
var clientWorld = ClientServerBootstrap.CreateClientWorld(world, "ClientWorld" + i);
193
clientWorld.EntityManager.CreateEntity(typeof(ThinClientComponent));
194
}
195
#endif
196
}
197
​
198
void StartGameScene()
199
{
200
//Here we trigger MainScene
201
#if UNITY_EDITOR
202
if(Application.isPlaying)
203
#endif
204
SceneManager.LoadSceneAsync("MainScene");
205
#if UNITY_EDITOR
206
else
207
Debug.Log("Loading: " + "MainScene");
208
#endif
209
}
210
}
Copied!
Updating ClientServerLauncher to pass through new data
    Now let's drag our ClientLaunchObject and ServerLaunchObject prefabs from the Scripts and Prefabs folder into the appropriate fields in our ClientServerLauncher GameObject in NavigationScene
      As a reminder you can find these fields in Inspector when ClientServerLauncher is selected in Hierarchy
      Lastly, let's also drag our LocalGamesDiscovery GameObject (in Hierarchy) into the Game Broadcasting field
Updating our ClientServerLauncher GameObject
    Let's hit play, navigate to Host Game, click Host, and check it out
Checking out hosting configuration through launch objects
    Now we are able to create our launch objects and our proper worlds are created πŸ‘
    Now let's go to Manual Connect screen and join an IP address
Checking out joining configuration through launch objects
    We can see from the logs that we have attempted to connect to the proper IP address
We are now able to take configurations from our NavigationScene and use them to create launch objects that are interpreted by our MainScene
    We updated LocalGamesFinder
    We updated our HostGameScreen uxml
    We updated HostGameScreen and ManualConnectScreen cVEs to populate with default system data
    We updated ClientLaunchObjectData and ServerLaunchObject data to take in more configuration data
    We turned ClientLaunchObjectData and ServerLaunchObject into prefabs, and removed them from MainScene
    We updated ClientServerLauncher to pull data and put them into our launch objects

Updating our Game UI

    Now let's update ClientServerInfo to be able to take in the additional information provided by the launch objects
    Paste the code snippet below into ClientServerInfo.cs:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using Unity.Collections;
5
using System;
6
7
public class ClientServerInfo : MonoBehaviour
8
{
9
public bool IsServer = false;
10
public bool IsClient = false;
11
public string ConnectToServerIp;
12
public ushort GamePort = 5001;
13
​
14
public string GameName;
15
public string PlayerName;
16
​
17
public string BroadcastIpAddress;
18
public ushort BroadcastPort;
19
​
20
// Start is called before the first frame update
21
void Start()
22
{
23
24
}
25
​
26
// Update is called once per frame
27
void Update()
28
{
29
30
}
31
}
Copied!
Updating ClientServerInfo to take in more data
    Let's also update our ServerDataComponent to take in additional information: game name
      We want our server to send the game name data to our client
      A client cannot manually connects to an IP address; they must be sent the name
    Paste the code snippet below into ServerDataComponent.cs:
1
using Unity.Entities;
2
using Unity.Collections;
3
​
4
​
5
public struct ServerDataComponent : IComponentData
6
{
7
public FixedString64 GameName;
8
public ushort GamePort;
9
}
Copied!
    Now we will update ClientDataComponent to take in additional information: player name
      We will make use of this in the "Scorekeeping" section when the player name needs to be sent to the server in order to keep score
    Paste the code snippet below into ClientDataComponent.cs:
1
using System;
2
using Unity.Entities;
3
using Unity.Collections;
4
​
5
public struct ClientDataComponent : IComponentData
6
{
7
//Must used "FixedStringN" instead of stirng in IComponentData
8
//This is a DOTS requirement because IComponentData must be a struct
9
public FixedString64 ConnectToServerIp;
10
public ushort GamePort;
11
public FixedString64 PlayerName;
12
}
13
​
Copied!
    We must also create a new component called GameNameComponent in Assets/Multiplayer Setup to store the game name on the client
      Right-click in the Multiplayer Setup folder > Create > C# Script > name it "GameNameComponent"
      It may seem weird to create a component for game name because we already have that data set in ClientServerInfo when hosting a session. However, please remember that we are building both a host build and a client build, and the client will not immediately have this information if they manually connect to an IP address
      OK, then why aren't we just putting GameName inside ClientDataComponent? Why are we making a new component? Don't we have enough of these components already?!
      Let us explain ourselves: .GameName is a FixedString64 string, which means that it faces a limitation if and when it's used in a component. The limitation of using FixedString in a component is that you cannot "tell" if the value of the FixedString has been set if it's in a component
        FixedString's default value is equal to an empty string
        So to circumvent this limitation, we need to create an entirely separate component just to store the value of GameName. To see if its value has been set, we check for the existence of the entire component
        This mouthful is just to explain and show you that you cannot check to see if a FixedString field has been updated in a component
    Paste the code snippet below into GameNameComponent.cs:
1
using Unity.Entities;
2
using Unity.Collections;
3
​
4
public struct GameNameComponent : IComponentData
5
{
6
//Must used "FixedStringN" instead of stirng in IComponentData
7
//This is a DOTS requirement because IComponentData must be a struct
8
public FixedString64 GameName;
9
}
Copied!
Creating GameNameComponent
    Now let's update our ClientServerConnectionHandler to pass through more data from the launch objects to ClientServerInfo as well as ClientDataComponent and ServerDataComponent
    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
//If it exists it creates ServerDataComponent InitializeServerComponent and
38
//passes through server data to ClientServerInfo
39
//
40
if(launchObject.GetComponent<ServerLaunchObjectData>() != null)
41
{
42
//This sets the gameobject server data in ClientServerInfo (mono)
43
ClientServerInfo.IsServer = true;
44
ClientServerInfo.GameName = launchObject.GetComponent<ServerLaunchObjectData>().GameName;
45
ClientServerInfo.BroadcastIpAddress = launchObject.GetComponent<ServerLaunchObjectData>().BroadcastIpAddress;
46
ClientServerInfo.BroadcastPort = launchObject.GetComponent<ServerLaunchObjectData>().BroadcastPort;
47
​
48
//This sets the component server data in server world(dots)
49
//ClientServerConnectionControl (server) will run in server world
50
//it will pick up this component and use it to listen on the port
51
foreach (var world in World.All)
52
{
53
//we cycle through all the worlds, and if the world has ServerSimulationSystemGroup
54
//we move forward (because that is the server world)
55
if (world.GetExistingSystem<ServerSimulationSystemGroup>() != null)
56
{
57
var ServerDataEntity = world.EntityManager.CreateEntity();
58
world.EntityManager.AddComponentData(ServerDataEntity, new ServerDataComponent
59
{
60
GameName = ClientServerInfo.GameName,
61
GamePort = ClientServerInfo.GamePort
62
});
63
//Create component that allows server initialization to run
64
world.EntityManager.CreateEntity(typeof(InitializeServerComponent));
65
}
66
}
67
}
68
​
69
//
70
//Checks for client launch object
71
//If it exists it creates ClientDataComponent, InitializeServerComponent and
72
// passes through client data to ClientServerInfo
73
//
74
if(launchObject.GetComponent<ClientLaunchObjectData>() != null)
75
{
76
//This sets the gameobject data in ClientServerInfo (mono)
77
ClientServerInfo.IsClient = true;
78
ClientServerInfo.ConnectToServerIp = launchObject.GetComponent<ClientLaunchObjectData>().IPAddress;
79
ClientServerInfo.PlayerName = launchObject.GetComponent<ClientLaunchObjectData>().PlayerName;
80
​
81
//This sets the component client data in server world (dots)
82
//ClientServerConnectionControl (client) will run in client world
83
//it will pick up this component and use it connect to IP and port
84
foreach (var world in World.All)
85
{
86
//We cycle through all the worlds, and if the world has ClientSimulationSystemGroup
87
//we move forward (because that is the client world)
88
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
89
{
90
var ClientDataEntity = world.EntityManager.CreateEntity();
91
world.EntityManager.AddComponentData(ClientDataEntity, new ClientDataComponent
92
{
93
PlayerName = ClientServerInfo.PlayerName,
94
ConnectToServerIp = ClientServerInfo.ConnectToServerIp,
95
GamePort = ClientServerInfo.GamePort
96
});
97
//Create component that allows client initialization to run
98
world.EntityManager.CreateEntity(typeof(InitializeClientComponent));
99
}
100
}
101
}
102
}
103
}
104
​
105
// Start is called before the first frame update
106
void Start()
107
{
108
109
}
110
​
111
// Update is called once per frame
112
void Update()
113
{
114
115
}
116
//This function will navigate us to NavigationScene
117
void ClickedQuitGame()
118
{
119
​
120
#if UNITY_EDITOR
121
if(Application.isPlaying)
122
#endif
123
SceneManager.LoadSceneAsync("NavigationScene");
124
#if UNITY_EDITOR
125
else
126
Debug.Log("Loading: " + "NavigationScene");
127
#endif
128
}
129
​
130
//When the OnDestroy method is called (because of our transition to NavigationScene) we
131
//must delete all our entities and our created worlds to go back to a blank state
132
//This way we can move back and forth between scenes and "start from scratch" each time
133
void OnDestroy()
134
{
135
//This query deletes all entities
136
World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(World.DefaultGameObjectInjectionWorld.EntityManager.UniversalQuery);
137
//This query deletes all worlds
138
World.DisposeAllWorlds();
139
​
140
//We return to our initial world that we started with, defaultWorld
141
var bootstrap = new NetCodeBootstrap();
142
bootstrap.Initialize("defaultWorld");
143
​
144
}
145
}
Copied!
Why do we duplicate data in ServerDataComponent/ClientDataComponent and ClientServerInfo? That seems super redundant...
Sometimes we want to use data in ECS, and because of that we save data in components so our systems can easily access it. At other times we want to use the data in a MonoBehaviour, so we save it in a GameObject to make it easily accessible to scripts.
This is not a rock-solid approach because it can be easy to "forget" to update data in one of the places we store it and not the other. The approach we take in this gitbook is to first always save any updates to ClientServerInfo and only push data into ECS from thereafter, as you might notice in ClientServerConnectionHandler.
Updating ClientServerConnectionHandler to pass through more data
    Now let's hit play and join through Host Game and Manual Connect and check out the updates to ClientServerInfo
      Play around! For example, go ahead and change up the input fields on the view to see the updates in ClientServerInfo (see gif below for ideas)
Changing hosting and joining values and seeing the data in ClientServerInfo
    Now let's update our SendClientGameRpc to include the game name
    Paste the code snippet below into SendClientGameRpc:
1
using AOT;
2
using Unity.Burst;
3
using Unity.Networking.Transport;
4
using Unity.NetCode;
5
using Unity.Entities;
6
using Unity.Collections;
7
using System.Collections;
8
using System;
9
​
10
public struct SendClientGameRpc : IRpcCommand
11
{
12
public int levelWidth;
13
public int levelHeight;
14
public int levelDepth;
15
public float playerForce;
16
public float bulletVelocity;
17
public FixedString64 gameName;
18
}
Copied!
Update SendClientGameRpc
    We need to include the updated information in ServerSendGameSystem
    Paste the code snippet below into ServerSendGameSystem.cs:
1
using Unity.Entities;
2
using Unity.Jobs;
3
using Unity.Collections;
4
using Unity.NetCode;
5
using UnityEngine;
6
​
7
//This component is only used by this system so we define it in this file
8
public struct SentClientGameRpcTag : IComponentData
9
{
10
}
11
​
12
//This system should only be run by the server (because the server sends the game settings)
13
//By sepcifying to update in group ServerSimulationSystemGroup it also specifies that it must
14
//be run by the server
15
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
16
[UpdateBefore(typeof(RpcSystem))]
17
public class ServerSendGameSystem : SystemBase
18
{
19
private BeginSimulationEntityCommandBufferSystem m_Barrier;
20
​
21
protected override void OnCreate()
22
{
23
m_Barrier = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
24
RequireSingletonForUpdate<GameSettingsComponent>();
25
RequireSingletonForUpdate<ServerDataComponent>();
26
}
27
​
28
protected override void OnUpdate()
29
{
30
var commandBuffer = m_Barrier.CreateCommandBuffer();
31
​
32
var serverData = GetSingleton<GameSettingsComponent>();
33
var gameNameData = GetSingleton<ServerDataComponent>();
34
​
35
Entities
36
.WithNone<SentClientGameRpcTag>()
37
.ForEach((Entity entity, in NetworkIdComponent netId) =>
38
{
39
commandBuffer.AddComponent(entity, new SentClientGameRpcTag());
40
var req = commandBuffer.CreateEntity();
41
commandBuffer.AddComponent(req, new SendClientGameRpc
42
{
43
levelWidth = serverData.levelWidth,
44
levelHeight = serverData.levelHeight,
45
levelDepth = serverData.levelDepth,
46
playerForce = serverData.playerForce,
47
bulletVelocity = serverData.bulletVelocity,
48
gameName = gameNameData.GameName
49
});
50
​
51
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent {TargetConnection = entity});
52
}).Schedule();
53
​
54
m_Barrier.AddJobHandleForProducer(Dependency);
55
}
56
}
Copied!
Updating ServerSendGameSystem with new data to send
    Now let's update the ClientLoadGameSystem to update ClientDataComponent with the game name. This will update our Game UI with the game name
      Paste the code snippet below into ClientLoadGameSystem.cs:
1
using Unity.Entities;
2
using Unity.NetCode;
3
using UnityEngine;
4
​
5
//This will only run on the client because it updates in ClientSimulationSystemGroup (which the server does not have)
6
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
7
[UpdateBefore(typeof(RpcSystem))]
8
public class ClientLoadGameSystem : SystemBase
9
{
10
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
11
​
12
protected override void OnCreate()
13
{
14
//We will be using the BeginSimECB
15
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
16
​
17
//Requiring the ReceiveRpcCommandRequestComponent ensures that update is only run when an NCE exists
18
RequireForUpdate(GetEntityQuery(ComponentType.ReadOnly<SendClientGameRpc>(), ComponentType.ReadOnly<ReceiveRpcCommandRequestComponent>()));
19
//This is just here to make sure the Sub Scene is streamed in before the client sets up the level data
20
RequireSingletonForUpdate<GameSettingsComponent>();
21
RequireSingletonForUpdate<ClientDataComponent>();
22
}
23
​
24
protected override void OnUpdate()
25
{
26
​
27
//We must declare our local variables before using them within a job (.ForEach)
28
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
29
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
30
var gameSettingsEntity = GetSingletonEntity<GameSettingsComponent>();
31
var getGameSettingsComponentData = GetComponentDataFromEntity<GameSettingsComponent>();
32
​
33
Entities
34
.ForEach((Entity entity, in SendClientGameRpc request, in ReceiveRpcCommandRequestComponent requestSource) =>
35
{
36
//This destroys the incoming RPC so the code is only run once
37
commandBuffer.DestroyEntity(entity);
38
​
39
//Check for disconnects before moving forward
40
if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
41
return;
42
​
43
//Set the game size (unnecessary right now but we are including it to show how it is done)
44
getGameSettingsComponentData[gameSettingsEntity] = new GameSettingsComponent
45
{
46
levelWidth = request.levelWidth,
47
levelHeight = request.levelHeight,
48
levelDepth = request.levelDepth,
49
playerForce = request.playerForce,
50
bulletVelocity = request.bulletVelocity
51
};
52
​
53
​
54
//Here we create a new singleton entity for GameNameComponent
55
//We could add this component to the singleton entity that has the GameSettingsComponent
56
//but we will keep them separate in case we want to change workflows in the future and don't
57
//want these components to be dependent on the same entity
58
var gameNameEntity= commandBuffer.CreateEntity();
59
commandBuffer.AddComponent(gameNameEntity, new GameNameComponent {
60
GameName = request.gameName
61
});
62
​
63
//These update the NCE with NetworkStreamInGame (required to start receiving snapshots) and
64
//PlayerSpawningStateComponent, which we will use when we spawn players
65
commandBuffer.AddComponent(requestSource.SourceConnection, new PlayerSpawningStateComponent());
66
commandBuffer.AddComponent(requestSource.SourceConnection, default(NetworkStreamInGame));
67
68
//This tells the server "I loaded the level"
69
//First we create an entity called levelReq that will have 2 necessary components
70
//Next we add the RPC we want to send (SendServerGameLoadedRpc) and then we add
71
//SendRpcCommandRequestComponent with our TargetConnection being the NCE with the server (which will send it to the server)
72
var levelReq = commandBuffer.CreateEntity();
73
commandBuffer.AddComponent(levelReq, new SendServerGameLoadedRpc());
74
commandBuffer.AddComponent(levelReq, new SendRpcCommandRequestComponent {TargetConnection = requestSource.SourceConnection});
75
​
76
}).Schedule();
77
​
78
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
79
}
80
}
Copied!
Updating ClientLoadGameSystem to pass through new data
    Right-click in the Assets/UI folder and Create a new C# Script named GameOverlayUpdater
    GameOverlapUpdater will be responsible for updating our GameUI overlay and pulling the ClientDataComponent and setting ClientServerInfo GameName
      It will update the game name and player name shown in the game UI
      It will also be responsible for updating player scores
    Paste the code snippet below into the newly created GameOverlayUpdater.cs:
1
using System.Collections;
2
using System.Collections.Generic;
3
using UnityEngine;
4
using UnityEngine.UIElements;
5
using Unity.Entities;
6
using Unity.NetCode;
7
using Unity.Collections;
8
using Unity.Jobs;
9
​
10
public class GameOverlayUpdater : MonoBehaviour
11
{
12
//This is how we will grab access to the UI elements we need to update
13
public UIDocument m_GameUIDocument;
14
private VisualElement m_GameManagerUIVE;
15
private Label m_GameName;
16
private Label m_GameIp;
17
private Label m_PlayerName;
18
private Label m_CurrentScoreText;
19
private Label m_HighScoreText;
20
private Label m_HighestScoreText;
21
22
//We will need ClientServerInfo to update our VisualElements with appropriate valuess
23
public ClientServerInfo ClientServerInfo;
24
private ClientSimulationSystemGroup m_ClientWorldSimulationSystemGroup;
25
​
26
//Will check for GameNameComponent
27
private EntityQuery m_GameNameComponentQuery;
28
private bool gameNameIsSet = false;
29
​
30
void OnEnable()
31
{
32
​
33
//We set the labels that we will need to update
34
m_GameManagerUIVE = m_GameUIDocument.rootVisualElement;
35
m_GameName = m_GameManagerUIVE.Q<Label>("game-name");
36
m_GameIp = m_GameManagerUIVE.Q<Label>("game-ip");
37
m_PlayerName = m_GameManagerUIVE.Q<Label>("player-name");
38
​
39
//Scores will be updated in a future section
40
m_CurrentScoreText = m_GameManagerUIVE.Q<Label>("current-score");
41
m_HighScoreText = m_GameManagerUIVE.Q<Label>("high-score");
42
m_HighestScoreText = m_GameManagerUIVE.Q<Label>("highest-score");
43
}
44
​
45
// Start is called before the first frame update
46
void Start()
47
{
48
//We set the initial client data we already have as part of ClientDataComponent
49
m_GameIp.text = ClientServerInfo.ConnectToServerIp;
50
m_PlayerName.text = ClientServerInfo.PlayerName;
51
52
//If it is not the client, stop running this script (unnecessary)
53
if (!ClientServerInfo.IsClient)
54
{
55
this.enabled = false;
56
}
57
58
//Now we search for the client world and the client simulation system group
59
//so we can communicated with ECS in this MonoBehaviour
60
foreach (var world in World.All)
61
{
62
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
63
{
64
m_ClientWorldSimulationSystemGroup = world.GetExistingSystem<ClientSimulationSystemGroup>();
65
m_GameNameComponentQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<GameNameComponent>());
66
}
67
}
68
}
69
​
70
​
71
// Update is called once per frame
72
void Update()
73
{
74
//We do not need to continue if we do not have a GameNameComponent yet
75
if(m_GameNameComponentQuery.IsEmptyIgnoreFilter)
76
return;
77
​
78
//If we have a GameNameComponent we need to update ClientServerInfo and then our UI
79
//We only need to do this once so we have a boolean flag to prevent this from being ran more than once
80
if(!gameNameIsSet)
81
{
82
ClientServerInfo.GameName = m_ClientWorldSimulationSystemGroup.GetSingleton<GameNameComponent>().GameName.ToString();
83
m_GameName.text = ClientServerInfo.GameName;
84
gameNameIsSet = true;
85
}
86
}
87
}
Copied!
    We already covered most of the techniques used here (grabbing UI Document, querying for Visual Elements, setting them to values) in the "Creating a ListView" page in the "UI Builder and UI Toolkit" section of the gitbook
      So, if what we are doing here in this section is blowing your mind and you don't feel comfortable moving forward, we implore you to visit our earlier section, "UI Builder and UI Toolkit," where we take you step-by-step through UI Builder and UI Toolkit
Creating GameOverlayUpdater
    Let's add GameOverlayUpdater as a component to the GameUI GameObject in MainScene
      Click Add Component in Inspector while GameUI is selected in Hierarchy and add GameOverlayUpdater
    Now let's drag the GameUI GameObject and the ClientServerInfo GameObjects into the appropriate fields on the component and save the scene, then return to NavigationScene
Updating GameUI GameObject with GameOverlayUpdater
    Let's hit play, join a game, and see how our game UI gets updated
Game UI updating when hosting or joining
We now have our Game UI updated with player and game information
    We updated ClientServerInfo to take in additional data
    We updated ServerDataComponent and ClientDataComponent to take in additional data
    We created GameNameComponent
    We updated ClientServerConnectionHandler to pass through more data from the launch objects
    We updated SendClientGameRpc to include the game name
    Updated ServerSendGameSystem to add the game name to the RPC
    We updated ClientLoadGameSystem to create GameNameComponent when receiving the RPC
    We created GameOverLayUpdater to update the Game UI

Updating build configurations

Now that we have 2 scenes, we need to update our build configurations so that we can test how our game responds to clients/hosts leaving games.
    Select BaseBuildConfiguration in the Assets/BuildSettings folder in your Project
    Go the Inspector. Under "Scene List" click the drop down icon next to "Scene Infos" and click "+ Add Element"
    Update Element 0 to be NavigationScene (drag "NavigationScene" from your Scenes folder into the Scene field)