Create a Network Connection using DOTS NetCode

Workflows and code to create a server/client socket connection using NetCode

What you'll develop on this page

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

Creating a socket connection

First, some background:

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.

From ECS Worlds documentation

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. 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");.

From NetCode Client server Worlds documentation

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.

Now, let's implement:

  • 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)

"com.unity.netcode": "0.50.1-preview.19",
  • 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:

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Networking.Transport;
using Unity.NetCode;
using UnityEngine;
using Unity;
using System;

#if UNITY_EDITOR
using Unity.NetCode.Editor;
#endif


//ServerConnectionControl is run in ServerWorld and starts listening on a port
//The port is provided by the ServerDataComponent
[UpdateInWorld(TargetWorld.Server)]
public partial class ServerConnectionControl : SystemBase
{
    private ushort m_GamePort = 5001;

    private struct InitializeServerComponent : IComponentData
    {   
    }

    protected override void OnCreate()
    {
        // We require the InitializeServerComponent to be created before OnUpdate runs
        RequireSingletonForUpdate<InitializeServerComponent>();

        //We create a component which will get immediatly destroyed so this system runs once
        EntityManager.CreateEntity(typeof(InitializeServerComponent));
        
    }

    protected override void OnUpdate()
    {
        //We destroy the InitializeServerComponent so this system only runs once
        EntityManager.DestroyEntity(GetSingletonEntity<InitializeServerComponent>());

        // This is used to split up the game's "world" into sections ("tiles")
        // The client is in a "tile" and networked objects are in "tiles"
        // the client is streamed data based on tiles that are near them
        //https://docs.unity3d.com/Packages/com.unity.netcode@0.5/manual/ghost-snapshots.html
        //check out "Distance based importance" in the link above
        var grid = EntityManager.CreateEntity();
        EntityManager.AddComponentData(grid, new GhostDistanceImportance
        {
            ScaleImportanceByDistance = GhostDistanceImportance.DefaultScaleFunctionPointer,
            TileSize = new int3(80, 80, 80),
            TileCenter = new int3(0, 0, 0),
            TileBorderWidth = new float3(1f, 1f, 1f)
        });

        //Here is where the server creates a port and listens
        NetworkEndPoint ep = NetworkEndPoint.AnyIpv4;
        ep.Port = m_GamePort;
        World.GetExistingSystem<NetworkStreamReceiveSystem>().Listen(ep);
        Debug.Log("Server is listening on port: " + m_GamePort.ToString());
    }
}

//ServerConnectionControl is run in ServerWorld and starts listening on a port
//The port is provided by the ServerDataComponent
[UpdateInWorld(TargetWorld.Client)]
public partial class ClientConnectionControl : SystemBase
{
    public string m_ConnectToServerIp = "127.0.0.1";
    public ushort m_GamePort = 5001;

    private struct InitializeClientComponent : IComponentData
    {   
    }

    protected override void OnCreate()
    {
        // We require the component to be created before OnUpdate runs
        RequireSingletonForUpdate<InitializeClientComponent>();

        //We create a component which will get immediatly destroyed so this system runs once
        EntityManager.CreateEntity(typeof(InitializeClientComponent));
    }

    protected override void OnUpdate()
    {
        // As soon as this runs, the component is destroyed so it doesn't happen twice
        EntityManager.DestroyEntity(GetSingletonEntity<InitializeClientComponent>());

        NetworkEndPoint ep = NetworkEndPoint.Parse(m_ConnectToServerIp, m_GamePort);
        World.GetExistingSystem<NetworkStreamReceiveSystem>().Connect(ep);
        Debug.Log("Client connecting to ip: " + m_ConnectToServerIp + " and port: " + m_GamePort.ToString());
    }
}
  • 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 importance

You 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. This GhostSendSystem passes this information to the ScaleImportanceByDistance in GhostDistanceImportance 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.

From NetCode "Distance based importance" documentation

  • 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

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.

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.

From NetCode Client server Worlds documentation

  • 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

Creating a configurable socket connection

First, some background:

  • 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!

Now, let's implement:

  • 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:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using System;
 
public class ClientServerInfo : MonoBehaviour
{
    public bool IsServer = false;
    public bool IsClient = false;
    public string ConnectToServerIp;
    public ushort GamePort = 5001;
}
  • 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:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;

public class ClientLaunchObjectData : MonoBehaviour
{
    public string IPAddress = "127.0.0.1";
}
  • 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:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;

public class ServerLaunchObjectData : MonoBehaviour
{
}
  • 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:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine.UIElements;
using UnityEngine.SceneManagement;

public class ClientServerConnectionHandler : MonoBehaviour
{
    //this is the store of server/client info
    public ClientServerInfo ClientServerInfo;

    // these are the launch objects from Navigation scene that tells what to set up
    private GameObject[] launchObjects;

    void Awake()
    {
        launchObjects = GameObject.FindGameObjectsWithTag("LaunchObject");
        foreach(GameObject launchObject in launchObjects)
        {
            //  
            // checks for server launch object
            // does set up for the server for listening to connections and player scores
            //
            if(launchObject.GetComponent<ServerLaunchObjectData>() != null)
            {
                //sets the gameobject server data (mono)
                ClientServerInfo.IsServer = true;
                
                //sets the component server data in server world(dots)
                //ClientServerConnectionControl (server) will run in server world
                //it will pick up this component and use it to listen on the port
                foreach (var world in World.All)
                {
                    //we cycle through all the worlds, and if the world has ServerSimulationSystemGroup
                    //we move forward (because that is the server world)
                    if (world.GetExistingSystem<ServerSimulationSystemGroup>() != null)
                    {
                        var ServerDataEntity = world.EntityManager.CreateEntity();
                        world.EntityManager.AddComponentData(ServerDataEntity, new ServerDataComponent
                        {
                            GamePort = ClientServerInfo.GamePort
                        });
                        //create component that allows server initialization to run
                        world.EntityManager.CreateEntity(typeof(InitializeServerComponent));
                    }
                }
            }

            // 
            // checks for client launch object
            //  does set up for client for dots and mono
            // 
            if(launchObject.GetComponent<ClientLaunchObjectData>() != null)
            {
                //sets the gameobject data in ClientServerInfo (mono)
                //sets the gameobject data in ClientServerInfo (mono)
                ClientServerInfo.IsClient = true;
                ClientServerInfo.ConnectToServerIp = launchObject.GetComponent<ClientLaunchObjectData>().IPAddress;                

                //sets the component client data in server world(dots)
                //ClientServerConnectionControl (client) will run in client world
                //it will pick up this component and use it connect to IP and port
                foreach (var world in World.All)
                {
                    //we cycle through all the worlds, and if the world has ClientSimulationSystemGroup
                    //we move forward (because that is the client world)
                    if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
                    {
                        var ClientDataEntity = world.EntityManager.CreateEntity();
                        world.EntityManager.AddComponentData(ClientDataEntity, new ClientDataComponent
                        {
                            ConnectToServerIp = ClientServerInfo.ConnectToServerIp,
                            GamePort = ClientServerInfo.GamePort
                        });
                        //create component that allows client initialization to run
                        world.EntityManager.CreateEntity(typeof(InitializeClientComponent));
                    }
                }
            }
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
  • 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:

using System;
using Unity.Entities;
using Unity.Collections;

public struct ClientDataComponent : IComponentData
{
    //Must used "FixedStringNBytes" instead of string in IComponentData
    //This is a DOTS requirement because IComponentData must be a struct
    public FixedString64Bytes ConnectToServerIp;
    public ushort GamePort;
}
  • InitializeClientComponent

using Unity.Entities;

public struct InitializeClientComponent : IComponentData
{   
}
  • ServerDataComponent

using Unity.Entities;
using Unity.Collections;


 public struct ServerDataComponent : IComponentData
{
    public ushort GamePort;
}
  • InitializeServerComponent

using Unity.Entities;

public struct InitializeServerComponent : IComponentData
{
    
}
  • 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:

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Networking.Transport;
using Unity.NetCode;
using UnityEngine;
using Unity;
using System;

#if UNITY_EDITOR
using Unity.NetCode.Editor;
#endif


//ServerConnectionControl is run in ServerWorld and starts listening on a port
//The port is provided by the ServerDataComponent
[UpdateInWorld(TargetWorld.Server)]
public partial class ServerConnectionControl : SystemBase
{
    private ushort m_GamePort;

    protected override void OnCreate()
    {
        // We require the InitializeServerComponent to be created before OnUpdate runs
        RequireSingletonForUpdate<InitializeServerComponent>();
        
    }

    protected override void OnUpdate()
    {
        //load up data to be used OnUpdate
        var serverDataEntity = GetSingletonEntity<ServerDataComponent>();
        var serverData = EntityManager.GetComponentData<ServerDataComponent>(serverDataEntity);
        m_GamePort = serverData.GamePort;

        //We destroy the InitializeServerComponent so this system only runs once
        EntityManager.DestroyEntity(GetSingletonEntity<InitializeServerComponent>());

        // This is used to split up the game's "world" into sections ("tiles")
        // The client is in a "tile" and networked objects are in "tiles"
        // the client is streamed data based on tiles that are near them
        //https://docs.unity3d.com/Packages/com.unity.netcode@0.5/manual/ghost-snapshots.html
        //check out "Distance based importance" in the link above
        var grid = EntityManager.CreateEntity();
        EntityManager.AddComponentData(grid, new GhostDistanceImportance
        {
            ScaleImportanceByDistance = GhostDistanceImportance.DefaultScaleFunctionPointer,
            TileSize = new int3(80, 80, 80),
            TileCenter = new int3(0, 0, 0),
            TileBorderWidth = new float3(1f, 1f, 1f)
        });

        //Here is where the server creates a port and listens
        NetworkEndPoint ep = NetworkEndPoint.AnyIpv4;
        ep.Port = m_GamePort;
        World.GetExistingSystem<NetworkStreamReceiveSystem>().Listen(ep);
        Debug.Log("Server is listening on port: " + m_GamePort.ToString());
    }
}

//ClientConnectionControl is run in ClientWorld and connects to an IP address and port
//The IP address and port is provided by the ClientDataComponent
[UpdateInWorld(TargetWorld.Client)]
public partial class ClientConnectionControl : SystemBase
{
    private string m_ConnectToServerIp;
    private ushort m_GamePort;

    protected override void OnCreate()
    {
        // We require the component to be created before OnUpdate runs
        RequireSingletonForUpdate<InitializeClientComponent>();

    }

    protected override void OnUpdate()
    {
        //load up data to be used OnUpdate
        var clientDataEntity = GetSingletonEntity<ClientDataComponent>();
        var clientData = EntityManager.GetComponentData<ClientDataComponent>(clientDataEntity);
        
        m_ConnectToServerIp = clientData.ConnectToServerIp.ToString();
        m_GamePort = clientData.GamePort;

        // As soon as this runs, the component is destroyed so it doesn't happen twice
        EntityManager.DestroyEntity(GetSingletonEntity<InitializeClientComponent>());

        NetworkEndPoint ep = NetworkEndPoint.Parse(m_ConnectToServerIp, m_GamePort);
        World.GetExistingSystem<NetworkStreamReceiveSystem>().Connect(ep);
        Debug.Log("Client connecting to ip: " + m_ConnectToServerIp + " and port: " + m_GamePort.ToString());
    }
}
  • 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'

Last updated