Create a Network Connection using DOTS NetCode
Workflows and code to create a server/client socket connection using NetCode
Last updated
Workflows and code to create a server/client socket connection using NetCode
Last updated
In our project, we make a configurable client/server socket connection using Unity NetCode.
Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Creating-a-Socket-Connection
If you think back to the "Spawning and Moving Player Prefabs" page in the ECS section of this gitbook, our project has been using the default World creation provided by Unity ECS.
A World organizes entities into isolated groups. A world owns both an EntityManager and a set of Systems. Entities created in one world only have meaning in that world, but can be transfered to other worlds (with EntityManager.MoveEntitiesFrom). Systems can only access entities in the same world. You can create as many worlds as you like.
By default Unity creates a default World when your application starts up (or you enter Play Mode). Unity instantiates all systems (classes that extend ComponentSystemBase) and adds them to this default world. Unity also creates specialized worlds in the Editor. For example, it creates an Editor world for entities and systems that run only in the Editor, not in playmode and also creates conversion worlds for managing the conversion of GameObjects to entities. See WorldFlags for examples of different types of worlds that can be created.
Use World.DefaultGameObjectInjectionWorld to access the default world.
If we take a look at our DOTS Windows we see that we are only running a single world called "Default World".
Now that we are using NetCode in our Project, we will start working in "Server World" and "Client World."
NetCode has a separation of client and server logic, and both the client and server logic are in separate Worlds (the client World, and the server World), based on the hierarchical update system of Unity’s Entity Component System (ECS).
By default, NetCode places systems in both client and server Worlds, but not in the default World.
NOTE
Systems that update in the
PresentationSystemGroup
are only added to the client World.To override this default behavior, use the UpdateInWorld attribute, or the
UpdateInGroup
attribute with an explicit client or server system group. The available explicit client server groups are as follows:
NOTE
There is no server presentation system group.
...
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. ImplementInitialize
and create the default World. To create the client and server worlds manually, callClientServerBootstrap.CreateClientWorld(defaultWorld, "WorldName");
orClientServerBootstrap.CreateServerWorld(defaultWorld, "WorldName");
.
When we add the NetCode package to our project its default is to create a Client and Server world, which we can override by using ClientServerBootstrap.
At the end of this code-along, user inputs will be ingested in "Client World" and stored as "Commands," which are then sent and played back in the "Server World" as well as played back in the "Client World" to do prediction.
The server is the authoritative source of the game. So if the client "predicts" a response to an input and the server disagrees, the server wins.
It is important to note that because Client and Server worlds get automatically created with NetCode, our project will no longer function as expected. All our systems and components will "automatically" put our ECS into client and server worlds.
So when our server creates asteroids and sends them to the client, the asteroids will appear "frozen" to the client. This is because although a server-spawned entity will reach the client, we haven't updated our Asteroid prefab to also transmit its updated location, so that's why they'll appear frozen.
From the "Overview" page we know that Unity is running an authoritative server client-predicted network architecture.
First, In the Scripts and Prefabs folder create three new folders:
Client (where we will store files only the client needs)
Mixed (where we will store files both the client and server need)
Server (where we will store files only the server needs)
This is just to help with organization. Our Scripts and Prefabs folder is already hard to navigate, and as we add even more it will soon become a mess
As we continue to update or add more files, let's move our existing files into these folders
This will also help us understand what components and systems are run by the server versus the client
Hardcore developers use assembly definition files along with Client/Mixed/Server folder separation so that they can deploy Client, Mixed, or Server only builds.
This is also helpful because when the client-only portion of the code base is adjusted the entire project does not need to be rebuilt, only the client-only code, which helps with development time.
If you're interested in being hardcore, check out Unity's Asteroids sample to see how they use assembly definition files to separate out code and logic. Their approach is a bit overkill for this gitbook, so to keep it simple our approach here is just to separate out the files to just give an idea of what files are expected to be run by the client/server.
Now lets add NetCode to our manifest.json file (found in the Project Folder)
You will get an error that reads:
This is because NetCode currently does not like if we implement a generic IBufferElementData (which we did to create StatefulEventTriggers)
We will edit:
StatefulCollisionEvent.cs
public struct StatefulCollisionEvent : IBufferElementData, IStatefulSimulationEvent
to
public struct StatefulCollisionEvent : IStatefulSimulationEvent
and StatefulTriggerEvent.cs
public struct StatefulTriggerEvent : IBufferElementData, IStatefulSimulationEvent
to
public struct StatefulTriggerEvent : IStatefulSimulationEvent
Make those changes to the definitions at line 7/8, hit save, and return to Editor
The error will be gone
Let's hit play and take a look at the DOTS Window and checkout the Worlds
We can see that in addition to Default World we now have "ClientWorld0" and "ServerWorld"
The reason we have "ClientWorld0" (with a number appended at the end) is because the Unity Editor supports "PlayMode Tools" for NetCode and you can "simulate" having multiple clients in playmode
Unity appends the client # to the end of the world
So if there were 2 clients set in "PlayMode Tools" we would see "ClientWorld0" and "ClientWorld1"
Click through Default World, ClientWorld0 and ServerWorld and check out the different systems in each of the worlds
Take time and review the image above; understand which systems are run in ClientWorld0 and which are run in ServerWorld
Seriously, it will save you a lot of heartache if you get comfortable with the difference SystemGroup setups between Server and Client worlds
We found it best to always imagine that ServerWorld and ClientWorld are run on entirely different machines (which happens in server-only builds where clients connect to a dedicated server)
When initially starting out with NetCode it is easy to sometimes forget this and think that a component created in ClientWorld should be available to be acted on in ServerWorld
It gets especially tricky during development because both the client and server are on the same machine (in the editor) so it "feels" like the data should be available
Notice that there are some systems that are only available on the client like "GhostInputSystem"
This system is where we will place our updated "InputMovementSystem"
Only client creates inputs, which is why the ServerWorld does not have this system
Another system is the PresentationSystemGroup which does not exist on the server
It doesn't exist on the server because the server doesn't need to render data in a presentation layer' the server is just the authoritative store of "state"
Alright, let's get into it!
Create ClientServerConnectionControl in Scripts and Prefabs
Keep this file in Scripts and Prefabs because it contains systems for both client and server (we'll move it into a better folder later)
We could split this file into two separate files (because there is a server-specific system and a client-specific system in the file) but for the sake of starting out easy in this gitbook, we're keeping it in one file
Paste the code snippet below into ClientServerConnectionControl.cs:
We can see within the file there is ServerConnectionControl and ClientConnectionControl
ServerConnectionControl only runs in the Server world (check it out in the DOTS Windows) because of the decoration at the top of the system
ClientConnectionControl only runs in the Client world (check it out in the DOTS Windows) because of the decoration at the top of the system
We can see that in ServerConnectionControl we create an Entity and add a "GhostImportanceDistance" component
Distance based importanceYou can use a custom function to scale the importance per chunk. For example, if a singleton entity with the
GhostDistanceImportance
component on it exists on the server, the netcode makes sure that all the ghosts in the World are split into groups based on the tile size in that singleton.You must add a
GhostConnectionPosition
component to each connection to determine which tile the connection should prioritize. ThisGhostSendSystem
passes this information to theScaleImportanceByDistance
inGhostDistanceImportance
which then uses it to scale the importance of a chunk based on its distance in tiles or any other metric you define in your code.
GhostImportanceDistance is a powerful functionality available in NetCode
This allows clients to selectively receive Snapshot data based on their proximity to different ghosts
What does this mean in context of a game? In a large-scale map, does a player really need to get the Snapshot data of a grenade thrown on the other side of the map just as fast as the Snapshot data of a grenade thrown right in front of them?
Probably not.
In Moetsi's case, we build city-scale live Reality Models
So sending each client all the data of all network objects in the model is not feasible and also unnecessary
GhostDistanceImportance allows for the most relevant Snapshot data be sent automatically, which is great stuff!
Both in ServerConnectionControl and ClientConnectionControl we use NetworkStreamReceiveSystem to Listen/Connect
The server operating in ServerWorld "listens" on the defined port
The client operating in ClientWorld "connects" to the defined IP address and port
Let's hit play and see what happens
We can see that our systems ran and logged their output
But, what is up with the frozen asteroids?!
So when we create these asteroids, both the server and client instantiate (because both client and server are running AsteroidSpawnSystem (which you can see yourself by checking the Systems for both worlds in the DOTS Windows)
The server is "authoritative," so it decides where the asteroids go, but currently in our Project, we are not yet sending game Snapshot data to the client
So the client creates Asteroids, but because we haven't done "NetCode magic" to make the client run the physics on these Asteroids their positions do not get updated, they require updates from the Server called "Snapshots"
In order to send Snapshot data, the client must go "in game" by adding a special NetCode component, "NetworkStreamInGame"
Then the client will receive updates
We will do this in the next section "Loading a Game"
Hit play then checkout the DOTS Windows, select ServerWorld, and select the NetworkConnection (1) entity to see it in the Inspector
This is the "NetworkConnectionEntity" (NCE)
this is not a Unity term, but it is the term we will be using in this gitbook to describe the entity created after making a client/server connection
When the server makes a connection with a client it creates a NetworkConnectionEntity for each client it connects with
So a server will have as many NCEs as it has connected clients
Let's now navigate to "ClientWorld0" and find the NCE
We can see the client also has a NCE
Navigate to the "Multiplayer" menu at the top and select "PlayMode Tools"
PlayMode Tools
When you enter Play Mode, from this window you can also disconnect clients and choose which client Unity should present if there are multiple. When you change a client that Unity is presenting, it stops calling the update on the
ClientPresentationSystemGroup
for the Worlds which it should no longer present. As such, your code needs to be able to handle this situation, or your presentation code won’t run and all rendering objects you’ve created still exist.
We are going to change the "Num Thin Clients" to 3
Hit play and navigate to the Entity Debugger
Notice that there are now 4 client worlds being handled by the Editor (one for each client)
ClientWorld0
ClientWorld1
ClientWorld2
ClientWorld3
It is important to note that in an actual deployed project, the server would not have a client world for every client connected
This behavior is just in the Editor when using PlayMode Tools (to help with creating games)
Navigate to ServerWorld in the DOTS Hierarchy and checkout the 4 NCEs
A NCE for every connection with a client
Click through the NCEs and see how the NetworkIdComponent starts at 1 and increases by 1 for every client connection
Now navigate to ClientWorld0 and checkout the NCE
Notice there is only 1 NCE in ClientWorld0 because there is only 1 NCE on the client
Clients only have a connection to the server; they do not have connections to other clients
Servers have connections to each and every client
Because we will be working with NCEs very heavily it is worth taking a harder look at what components and values the inspector shows for an NCE immediately upon connection
Change the "Num Thin Clients" back to 0
No gif here, we believe in you 💪
We now have a client/server socket connection which creates an NCE on both the client and server
We added the NetCode package to manifest.json
We created ClientServerConnectionControl which results in a NCE on both the client and server
Now we are going to make things more complicated...
For good reason!
We need to prepare for the upcoming "Multiplayer section" where we will have the ability to select whether we join a game as client-only or we host a game and are a client-server
Right now the server IP address and port are hardwired into our systems
They are defined in ServerConnectionControl and ClientConnectionControl
We will now update this so that the IP address and port are provided by GameObjects
We will be adding four GameObjects to SampleScene
ClientServerInfo (with ClientServerInfo script)
ClientServerConnectionHandler (with ClientServerConnectionHandler script)
ClientLaunchObject (with ClientLaunchObjectData script)
ServerLaunchObject (with ServerLaunchObjectData script)
ClientServerInfo
This is where we will set what port our game should run on
We will update it with the IP address provided by the ClientLaunchObject
Think of it as a store of client and server info
ClientServerConnectionHandler
Its script will look for GameObjects with "LaunchObject" tags and take data from those GameObjects to create entities, which will trigger our ClientServerConnectionControl
ClientLaunchObjectData
This will have a "LaunchObject" tag and store the IP address the client is connecting to
ServerGameObject
This will have a "LaunchObject" tag
We will update our ClientServerConnectionControl to ingest the component data created by ClientServerConnectionHandler
We will also create these 4 new components:
ClientDataComponent (will provide the IP address and port to ClientConnectionControl)
InitializeClientComponent (will trigger ClientConnectionControl to run)
ServerDataComponent (will provide the port to ServerConnectionControl)
InitializeServerComponent (will trigger ServerConnectionControl to run)
Note that the "source of truth" of what IP address our client will connect to is on the "ClientLaunchObject"
This is how we be able to configure what server to connect to from our "Navigation" scene, later on in the Multiplayer section of this gitbook
Note that the "source of truth" of what port our server listens on and client connects to is in "ClientServerInfo"
This was an opinionated choice by Moetsi specifically for this gitbook
We could provide the port in the ServerLaunchObject and ClientLaunchObject as well
But we decided that the port number will be set "before" runtime (aka baked into your build)
If you want your ports to be dynamic, go ahead and do you!
Right-click on the Hierarchy in SampleScene and create an empty GameObject named ClientServerInfo
Create a new script named ClientServerInfo
Paste the code snippet below into ClientServerInfo.cs:
Click "Add Component" in Inspector on ClientServerInfo and add the ClientServerInfo script
Navigate to SampleScene, right click in the Hierarchy and create an empty GameObject and name it ClientLaunchObject
Add a tag called "LaunchObject"
You do this by selecting the drop down menu next to "Tag" in the Inspector when ClientLaunchObject is highlighted in Hierarchy and choosing the last option "Add Tag...". Hit the + button and type in the name of the tag ("LaunchObject" for this one)
Create a new script called ClientLaunchObjectData
Paste the code snippet below into ClientLaunchObjectData.cs:
Click "Add Component" in Inspector on ClientLaunchObject and add ClientLaunchObjectData script
Navigate to SampleScene, right click in the Hierarchy and create an empty GameObject and name it ServerLaunchObject
add the "LaunchObject" tag (the tag you just made)
Create a new script called ServerLaunchObjectData
Paste the code snippet below into ServerLaunchObjectData.cs:
Click "Add Component" in Inspector on ServerLaunchObject and add the ServerLaunchObjectData script
Right click in the Hierarchy in SampleScene and create an empty GameObject called ClientServerConnectionHandler
Create a new script called ClientServerConnectionHandler
Paste the code snippet below into ClientServerConnectionHandler.cs:
You will get a 4 errors because ClientServerConnectionHandler is referencing components we haven't created yet, so let's make our 4 additional components
Create ClientDataComponent and paste in this code snippet:
InitializeClientComponent
ServerDataComponent
InitializeServerComponent
Now we must update ClientServerConnectionControl to use these new components
This file already exists so just paste over current code with the code snippet below into ClientServerConnectionControl:
Finally let's add ClientServerInfo as the reference in ClientServerConnectionHandler (drag the ClientServerInfo GameObject into the Client Server Info field in the Client Server Connection Handler in Inspector)
Hit play
Our client and server make a connection and update ClientServerInfo
Finally, let's do some housekeeping and create a new folder called "Multiplayer Setup" in Scripts and Prefabs
Drag the following files into "Multiplayer Setup" folder:
ClientServerInfo
ClientLaunchObjectData
ServerLaunchObjectData
ClientServerConnectionHandler
ClientDataComponent
InitializeClientComponent
ServerDataComponent
InitializeServerComponent
ClientServerConnectionControl
No gif here, we believe in you 💪
We can now trigger a client server connection through GameObjects with LaunchObject tags
We created 4 new GameObjects in the scene
ClientServerInfo
ClientLaunchObject
ServerLaunchObject
ClientServerConnectionHandler
We created 4 new scripts
ClientServerInfo
ClientLaunchObjectData
ServerLaunchObjectData
ClientServerConnectionHandler
We created 4 new components
ClientDataComponent
InitializeClientComponent
ServerDataComponent
InitializeServerComponent
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/
git checkout 'Creating-a-Socket-Connection'
Property
Description
PlayMode Type
Choose to make Play Mode either Client only, Server only, or Client & Server.
Num Thin Clients
Set the number of thin clients. Thin clients cannot be presented, and never spawn any entities it receives from the server. However, they can generate fake input to send to the server to simulate a realistic load.
Client send/recv delay
Use this property to emulate high ping. Specify a time (in ms) to delay each outgoing and incoming network packet by.
Client send/recv jitter
Use this property to add a random value to the delay, which makes the delay a value between the delay you have set plus or minus the jitter value. For example, if you set Client send/recv delay to 45 and Client send/recv jitter to 5, you will get a random value between 40 and 50.
Client package drop
Use this property to simulate bad connections where not all packets arrive. Specify a value (as a percentage) and NetCode discards that percentage of packets from the total it receives. For example, set the value to 5 and NetCode discards 5% of all incoming and outgoing packets.
Client auto connect address (Client only)
Specify which server a client should connect to. This field only appears if you set PlayMode Type to Client. The user code needs to read this value and connect because the connection flows are in user code.