Load a Game using DOTS NetCode
Code and workflow to send server game data to client and have the client confirm loading the data

What you'll develop on this page

Server sending updates and NCEs updated as expected
We will send server game data to the client; the client will load the data and send back another RPC.

Loading a game

First, some background:

When a socket connection is made in NetCode the server and client both have the ability to send each other RPCs.
NetCode uses a limited form of RPCs to handle events. A job on the sending side can issue RPCs, and they then execute on a job on the receiving side. This limits what you can do in an RPC; such as what data you can read and modify, and what calls you are allowed to make from the engine.
...
To send the command you need to create an entity and add the command and the special component SendRpcCommandRequestComponent to it. This component has a member called TargetConnection that refers to the remote connection you want to send this command to.
NOTE
If TargetConnection is set to Entity.Null you will broadcast the message. On a client you don't have to set this value because you will only send to the server.
...
The RpcSystem automatically finds all of the requests, sends them, and then deletes the send request. On the remote side they show up as entities with the same IRpcCommand and a ReceiveRpcCommandRequestComponent which you can use to identify which connection the request was received from.
...
The code generation for RPCs is optional, if you do not wish to use it you need to create a component and a serializer. These can be the same struct or two different ones. To create a single struct which is both the component and the serializer you would need to add:
1
[BurstCompile]
2
public struct OurRpcCommand : IComponentData, IRpcCommandSerializer<OurRpcCommand>
3
{
4
public void Serialize(ref DataStreamWriter writer, in OurRpcCommand data)
5
{
6
}
7
8
public void Deserialize(ref DataStreamReader reader, ref OurRpcCommand data)
9
{
10
}
11
12
public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
13
{
14
}
15
16
[BurstCompile]
17
private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
18
{
19
}
20
21
static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer = new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
22
}
Copied!
The IRpcCommandSerializer interface has three methods: Serialize, Deserialize, and CompileExecute. Serialize and Deserialize store the data in a packet, while CompileExecute uses Burst to create a FunctionPointer. The function it compiles takes a RpcExecutor.Parameters by ref that contains:
    DataStreamReader reader
    Entity connection
    EntityCommandBuffer.Concurrent commandBuffer
    int jobIndex
Because the function is static, it needs to use Deserialize to read the struct data before it executes the RPC. The RPC then either uses the command buffer to modify the connection entity, or uses it to create a new request entity for more complex tasks. It then applies the command in a separate system at a later time. This means that you don’t need to perform any additional operations to receive an RPC; its Execute method is called on the receiving end automatically.
Not sure how you feel about it, but that RPCs documentation is TOUGH 🤯
Basically here's the gist: once you have a client/server connection you can send RPCs back and forth.
If you want to send RPCs with "just" data, NetCode has solid code generation that makes it pretty easy to do. We will do that in this project when the server sends the client the game settings.
If you want your RPCs to also execute code when they get to their intended receiver, then boilerplate code from the (confusing) explanation is involved. You'll see how this works at the point in our project when the client sends back "I have loaded the game" confirmation to the server and the client-sent RPC automatically updates the the server's NCE.
There are 2 more data streams that can be sent between NCEs: (1) Snapshots and (2) Commands. Snapshots send state data and Commands send inputs.
Currently, by creating a NCE, we have "unlocked" RPCs. Once we put a special NetCode component, "NetworkStreamInGame", on the NCE it signals to NetCode to begin sending game data. Then Snapshots and Commands will be unlocked.
Part of the flow of loading the level will be to send the GameSettings data to the client, the second part is to add the NetworkStreamInGame component to the NCE on both the client and server. Because the GameSettings data is set from the fields on the GameSettings GameObject in the Sub Scene, which is accessible to both the client and server, it is actually unnecessary to send GameSettings from the server to the client (the server would be sending the client data that the client already knows). However, in our project we implement this data transfer even though it is unnecessary (currently) to show an example of how to send data. In the Multiplayer section we will be sending data that the client actually does not know.

Now let's implement

Overview of 'Loading a game' flow we implement in this section
    We are going to implement a flow where the server sends an RPC to a newly connected client and the client responds by sending back an RPC
    Let's start by creating a folder in "Mixed" called "Commands" where we will store our RPCs
      The RPCs need to be in the Mixed folder because both the server and client utilize these RPCs
    Make another folder in Mixed called "Components"
      In this folder we will put the components that both the server and client touch
    Put the GameSettingsComponent in Mixed/Components
      Both the server and client will need the GameSettingsComponent in this flow
Creating Mixed/Commands and Mixed/Components and moving in GameSettingsComponent
    Now create PlayerSpawningStateComponent and put it in Server/Components
      So make a Components folder in Server folder :)
      PlayerSpawningStateComponent will be used by the server to know when a player is spawning
    Paste the code snippet below into PlayerSpawningStateComponentData.cs:
1
using Unity.Entities;
2
using Unity.NetCode;
3
4
public struct PlayerSpawningStateComponent : IComponentData
5
{
6
public int IsSpawning;
7
}
Copied!
Creating PlayerSpawningStateComponent (gif out of date, put it in Server/Components not Mixed/Components)
    Now in Mixed/Commands create two 2 RPCs
    First one is SendClientGameRpc
    Paste the code snippet below in SendClientGameRpc.cs:
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
}
Copied!
    Second one is SendServerGameLoadedRpc
    Paste the code snippet below in SendServerGameLoadedRpc.cs:
1
using AOT;
2
using Unity.Burst;
3
using Unity.Networking.Transport;
4
using Unity.NetCode;
5
using Unity.Entities;
6
using UnityEngine;
7
using Unity.Collections;
8
9
[BurstCompile]
10
public struct SendServerGameLoadedRpc : IComponentData, IRpcCommandSerializer<SendServerGameLoadedRpc>
11
{
12
//Necessary boilerplate
13
public void Serialize(ref DataStreamWriter writer, in RpcSerializerState state, in SendServerGameLoadedRpc data)
14
{
15
}
16
//Necessary boilerplate
17
public void Deserialize(ref DataStreamReader reader, in RpcDeserializerState state, ref SendServerGameLoadedRpc data)
18
{
19
}
20
21
[BurstCompile]
22
[MonoPInvokeCallback(typeof(RpcExecutor.ExecuteDelegate))]
23
private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
24
{
25
//Within here is where
26
var rpcData = default(SendServerGameLoadedRpc);
27
28
//Here we deserialize the received data
29
rpcData.Deserialize(ref parameters.Reader, parameters.DeserializerState, ref rpcData);
30
31
//Here we add 3 components to the NCE
32
//The first, PlayerSpawningStateComonent will be used during our player spawn flow
33
parameters.CommandBuffer.AddComponent(parameters.JobIndex, parameters.Connection, new PlayerSpawningStateComponent());
34
//NetworkStreamInGame must be added to an NCE to start receiving Snapshots
35
parameters.CommandBuffer.AddComponent(parameters.JobIndex, parameters.Connection, default(NetworkStreamInGame));
36
//GhostConnectionPosition is added to be used in conjunction with GhostDistanceImportance (from the socket section)
37
parameters.CommandBuffer.AddComponent(parameters.JobIndex, parameters.Connection, default(GhostConnectionPosition));
38
39
//We add a log that we will remove later to show that this RPC has been executed
40
//iOS will crash if Debug.Log is used within an RPC so we will remove this in the ARFoundation section
41
Debug.Log("Server acted on confirmed game load");
42
}
43
44
//Necessary boilerplate
45
static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer =
46
new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
47
public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
48
{
49
return InvokeExecuteFunctionPointer;
50
}
51
}
52
53
//Necessary boilerplate
54
class SendServerGameLoadedRpcCommandRequestSystem : RpcCommandRequestSystem<SendServerGameLoadedRpc, SendServerGameLoadedRpc>
55
{
56
[BurstCompile]
57
protected struct SendRpc : IJobEntityBatch
58
{
59
public SendRpcData data;
60
public void Execute(ArchetypeChunk chunk, int orderIndex)
61
{
62
data.Execute(chunk, orderIndex);
63
}
64
}
65
protected override void OnUpdate()
66
{
67
var sendJob = new SendRpc{data = InitJobData()};
68
ScheduleJobData(sendJob);
69
}
70
}
Copied!
    You'll notice that SendServerGameLoadedRpc is more complicated than SendClientGameRpc
      SendClientGameRpc is used to send data from the server to the client. There's no other funny business going on, just an RPC being used as a way to send data
      SendServerGameLoadedRpc does not send any data. Instead, when it reaches the server the InvokeExecute method is called, which updates the receiving NCE
        The RPC is able to update the NCE itself, without us needing to implement a system to get it done
Creating two RPCs in Mixed/Commands
    Create a folder in Server named "Systems" and within that folder create ServerSendGameSystem
    Paste this 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
}
26
27
protected override void OnUpdate()
28
{
29
var commandBuffer = m_Barrier.CreateCommandBuffer();
30
31
var serverData = GetSingleton<GameSettingsComponent>();
32
33
Entities
34
.WithNone<SentClientGameRpcTag>()
35
.ForEach((Entity entity, in NetworkIdComponent netId) =>
36
{
37
commandBuffer.AddComponent(entity, new SentClientGameRpcTag());
38
var req = commandBuffer.CreateEntity();
39
commandBuffer.AddComponent(req, new SendClientGameRpc
40
{
41
levelWidth = serverData.levelWidth,
42
levelHeight = serverData.levelHeight,
43
levelDepth = serverData.levelDepth,
44
playerForce = serverData.playerForce,
45
bulletVelocity = serverData.bulletVelocity,
46
});
47
48
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent {TargetConnection = entity});
49
}).Schedule();
50
51
m_Barrier.AddJobHandleForProducer(Dependency);
52
}
53
}
Copied!
Creating ServerSendGameSystem in Server/Systems
    Create a folder in Client named "Systems" and within that folder create ClientLoadGameSystem
    Paste the code snippet below into ClientLoadGameSystem:
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
}
22
23
protected override void OnUpdate()
24
{
25
26
//We must declare our local variables before using them within a job (.ForEach)
27
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
28
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
29
var gameSettingsEntity = GetSingletonEntity<GameSettingsComponent>();
30
var getGameSettingsComponentData = GetComponentDataFromEntity<GameSettingsComponent>();
31
32
Entities
33
.ForEach((Entity entity, in SendClientGameRpc request, in ReceiveRpcCommandRequestComponent requestSource) =>
34
{
35
//This destroys the incoming RPC so the code is only run once
36
commandBuffer.DestroyEntity(entity);
37
38
//Check for disconnects before moving forward
39
if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
40
return;
41
42
//Set the game size (unnecessary right now but we are including it to show how it is done)
43
getGameSettingsComponentData[gameSettingsEntity] = new GameSettingsComponent
44
{
45
levelWidth = request.levelWidth,
46
levelHeight = request.levelHeight,
47
levelDepth = request.levelDepth,
48
playerForce = request.playerForce,
49
bulletVelocity = request.bulletVelocity
50
};
51
52
//These update the NCE with NetworkStreamInGame (required to start receiving snapshots)
53
commandBuffer.AddComponent(requestSource.SourceConnection, default(NetworkStreamInGame));
54
55
//This tells the server "I loaded the level"
56
//First we create an entity called levelReq that will have 2 necessary components
57
//Next we add the RPC we want to send (SendServerGameLoadedRpc) and then we add
58
//SendRpcCommandRequestComponent with our TargetConnection being the NCE with the server (which will send it to the server)
59
var levelReq = commandBuffer.CreateEntity();
60
commandBuffer.AddComponent(levelReq, new SendServerGameLoadedRpc());
61
commandBuffer.AddComponent(levelReq, new SendRpcCommandRequestComponent {TargetConnection = requestSource.SourceConnection});
62
63
Debug.Log("Client loaded game");
64
}).Schedule();
65
66
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
67
}
68
}
69
Copied!
Creating ClientLoadGameSystem in Client/Systems
    Let's hit play and then go take a look the NCE in Server World in the Entity Debugger
Snapshots being sent and NCE is updated as expected
    Our asteroids now have movement because we are "in game"
      The server can send Snapshots of the asteroids
    We see our NCEs have been updated by our RPCs and systems
      Look at the components added to the NCEs
We can now load data on the client sent from the server through an RPC
    We created a new component
      PlayerSpawningStateComponent
    We created 2 RPCs
      SendClientGameRpc
      SendServerGameLoadedRpc
    We created 2 systems
      ServerSendGameSystem
      ClientLoadGameSystem
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Loading-a-Game'
Last modified 7mo ago