Networked cube
Make sure you have set up the project correctly using the installation guide before starting your adventure (of creating a simple client-server based simulation).
This tutorial briefly introduces the most common concepts involved in making client-server based games.
Creating an initial scene
To begin, set up a way to share data between the client and the server. This separation is achieved in Netcode for Entities by creating a different World for the server and each client (via the Entities Package). To share data between the server and the client:
- Right-click within the Hierarchy window in the Unity Editor.
- Select New Subscene > Empty Scene...
- Name the new scene "SharedData".
Once this is set up, spawn a plane in both the client and the server world. To do this, right click the SharedData subscene and select 3D Object > Plane which then creates a plane nested under SharedData.
Scene with a plane
If you select Play, then select Window > Entities > Hierarchy, you can see two worlds (ClientWorld and ServerWorld), each with the SharedData scene with the plane that you just created.
Hierarchy View
Establish a connection
To enable communication between the client and server, you need to establish a connection. In Netcode for Entities, the simplest way of achieving this is to use the auto-connect feature. You can use the auto-connect feature by inheriting from the ClientServerBootstrap
, then setting the AutoConnectPort
to your chosen port.
Create a file called Game.cs in your Assets folder and add the following code to the file:
using System;
using Unity.Entities;
using Unity.NetCode;
// Create a custom bootstrap, which enables auto-connect.
// The bootstrap can also be used to configure other settings as well as to
// manually decide which worlds (client and server) to create based on user input
[UnityEngine.Scripting.Preserve]
public class GameBootstrap : ClientServerBootstrap
{
public override bool Initialize(string defaultWorldName)
{
AutoConnectPort = 7979; // Enabled auto connect
return base.Initialize(defaultWorldName); // Use the regular bootstrap
}
}
Communicate with the server
When you're connected, you can start communicating with the server. A critical concept in Netcode for Entities is the concept of InGame
. When a connection is marked with InGame
it tells the simulation it's ready to start synchronizing.
Before entering InGame
state, the only way to communicate with a Netcode for Entities server is via RPC
s. So to continue, create an RPC that acts as a "Go In Game" message, (for example, tell the server that you are ready to start receiving snapshots).
Create a file called GoInGame.cs in your Assets folder and add the following code to the file.
using UnityEngine;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
/// <summary>
/// This allows sending RPCs between a stand alone build and the editor for testing purposes in the event when you finish this example
/// you want to connect a server-client stand alone build to a client configured editor instance.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
[UpdateInGroup(typeof(InitializationSystemGroup))]
[CreateAfter(typeof(RpcSystem))]
public partial struct SetRpcSystemDynamicAssemblyListSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
SystemAPI.GetSingletonRW<RpcCollection>().ValueRW.DynamicAssemblyList = true;
state.Enabled = false;
}
}
// RPC request from client to server for game to go "in game" and send snapshots / inputs
public struct GoInGameRequest : IRpcCommand
{
}
// When client has a connection with network id, go in game and tell server to also go in game
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct GoInGameClientSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<NetworkId>()
.WithNone<NetworkStreamInGame>();
state.RequireForUpdate(state.GetEntityQuery(builder));
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
foreach (var (id, entity) in SystemAPI.Query<RefRO<NetworkId>>().WithEntityAccess().WithNone<NetworkStreamInGame>())
{
commandBuffer.AddComponent<NetworkStreamInGame>(entity);
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<GoInGameRequest>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequest { TargetConnection = entity });
}
commandBuffer.Playback(state.EntityManager);
}
}
// When server receives go in game request, go in game and delete request
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct GoInGameServerSystem : ISystem
{
private ComponentLookup<NetworkId> networkIdFromEntity;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<GoInGameRequest>()
.WithAll<ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
networkIdFromEntity = state.GetComponentLookup<NetworkId>(true);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var worldName = state.WorldUnmanaged.Name;
var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
networkIdFromEntity.Update(ref state);
foreach (var (reqSrc, reqEntity) in SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<GoInGameRequest>().WithEntityAccess())
{
commandBuffer.AddComponent<NetworkStreamInGame>(reqSrc.ValueRO.SourceConnection);
var networkId = networkIdFromEntity[reqSrc.ValueRO.SourceConnection];
Debug.Log($"'{worldName}' setting connection '{networkId.Value}' to in game");
commandBuffer.DestroyEntity(reqEntity);
}
commandBuffer.Playback(state.EntityManager);
}
}
Create a ghost prefab
To synchronize something across a client/server setup, you need to create a definition of the networked object, called a ghost.
To create a ghost prefab:
- Create a cube in the scene by right-clicking on the scene, then selecting 3D Object > Cube.
- Select the Cube GameObject under the Scene and drag it into the Project’s Asset folder. This creates a prefab of the cube.
- After creating the prefab, you can delete the cube from the scene, but do not delete the prefab.
Create a cube prefab
To identify and synchronize the cube prefab inside Netcode for Entities, you need to create an IComponent
and author it. To do so, create a new file called CubeAuthoring.cs and enter the following:
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
public struct Cube : IComponentData
{
}
[DisallowMultipleComponent]
public class CubeAuthoring : MonoBehaviour
{
class Baker : Baker<CubeAuthoring>
{
public override void Bake(CubeAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<Cube>(entity);
}
}
}
Once you create this component, add it to the Cube.prefab
. Then, in the Inspector, add the Ghost Authoring Component to the prefab.
When you do this, Unity will automatically serialize the Translation and Rotation components.
Before you can move the cube around, you must change some settings in the newly added Ghost Authoring Component:
- Check the Has Owner box. This automatically adds and checks a new property called Support Auto Command Target (more on this later).
- Change the Default Ghost Mode to Owner Predicted. You need to set the NetworkId member of the Ghost Owner Component in your code (more on this later). This makes sure that you predict your own movement.
The Ghost Authoring component
Create a spawner
To tell Netcode for Entities which ghosts to use, you need to reference the prefabs from the subscene. First, create a new component for the spawner: create a file called CubeSpawnerAuthoring.cs and add the following code:
using Unity.Entities;
using UnityEngine;
public struct CubeSpawner : IComponentData
{
public Entity Cube;
}
[DisallowMultipleComponent]
public class CubeSpawnerAuthoring : MonoBehaviour
{
public GameObject Cube;
class Baker : Baker<CubeSpawnerAuthoring>
{
public override void Bake(CubeSpawnerAuthoring authoring)
{
CubeSpawner component = default(CubeSpawner);
component.Cube = GetEntity(authoring.Cube, TransformUsageFlags.Dynamic);
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, component);
}
}
}
- Right-click on SharedData and select Create Empty.
- Rename it to Spawner, then add a CubeSpawner.
- Because both the client and the server need to know about these ghosts, add it to the SharedData Subscene.
- In the Inspector, drag the cube prefab to the cube field of the spawner.
Ghost Spawner settings
Spawning our prefab
To spawn the prefab, you need to update the GoInGame.cs file. As described earlier, you must send a GoInGame RPC
when you're ready to tell the server to start synchronizing. You can update that code to actually spawn our cube as well.
Update GoInGameClientSystem
and GoInGameServerSystem
GoInGameClientSystem
and GoInGameServerSystem
should only run on the entities that have CubeSpawner
component data associated with them. To do this, add a call to SystemState.RequireForUpdate
in both systems' OnCreate
method:
state.RequireForUpdate<CubeSpawner>();
Your GoInGameClientSystem.OnCreate
method should look like this now:
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// Run only on entities with a CubeSpawner component data
state.RequireForUpdate<CubeSpawner>();
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<NetworkId>()
.WithNone<NetworkStreamInGame>();
state.RequireForUpdate(state.GetEntityQuery(builder));
}
Your GoInGameServerSystem.OnCreate
method should look like this now:
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<CubeSpawner>();
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<GoInGameRequest>()
.WithAll<ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
networkIdFromEntity = state.GetComponentLookup<NetworkId>(true);
}
Additionally, for the GoInGameServerSystem.OnUpdate
method we want to:
- Get the prefab to spawn.
- As an added example, get the name of the prefab being spawned to add to the log message.
- For each inbound
ReceiveRpcCommandRequest
message, instantiate an instance of the prefab.- For each prefab instance, set the
GhostOwner.NetworkId
value to theNetworkId
of the requesting client.
- For each prefab instance, set the
- Finally, add the newly instantiated instance to the
LinkedEntityGroup
so when the client disconnects the entity is destroyed.
Update your GoInGameServerSystem.OnUpdate
method to this:
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Get the prefab to instantiate
var prefab = SystemAPI.GetSingleton<CubeSpawner>().Cube;
// Ge the name of the prefab being instantiated
state.EntityManager.GetName(prefab, out var prefabName);
var worldName = new FixedString32Bytes(state.WorldUnmanaged.Name);
var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
networkIdFromEntity.Update(ref state);
foreach (var (reqSrc, reqEntity) in SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<GoInGameRequest>().WithEntityAccess())
{
commandBuffer.AddComponent<NetworkStreamInGame>(reqSrc.ValueRO.SourceConnection);
// Get the NetworkId for the requesting client
var networkId = networkIdFromEntity[reqSrc.ValueRO.SourceConnection];
// Log information about the connection request that includes the client's assigned NetworkId and the name of the prefab spawned.
UnityEngine.Debug.Log($"'{worldName}' setting connection '{networkId.Value}' to in game, spawning a Ghost '{prefabName}' for them!");
// Instantiate the prefab
var player = commandBuffer.Instantiate(prefab);
// Associate the instantiated prefab with the connected client's assigned NetworkId
commandBuffer.SetComponent(player, new GhostOwner { NetworkId = networkId.Value});
// Add the player to the linked entity group so it is destroyed automatically on disconnect
commandBuffer.AppendToBuffer(reqSrc.ValueRO.SourceConnection, new LinkedEntityGroup{Value = player});
commandBuffer.DestroyEntity(reqEntity);
}
commandBuffer.Playback(state.EntityManager);
}
Your GoInGame.cs file should now look like this:
using UnityEngine;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using Unity.Burst;
/// <summary>
/// This allows sending RPCs between a stand alone build and the editor for testing purposes in the event when you finish this example
/// you want to connect a server-client stand alone build to a client configured editor instance.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
[UpdateInGroup(typeof(InitializationSystemGroup))]
[CreateAfter(typeof(RpcSystem))]
public partial struct SetRpcSystemDynamicAssemblyListSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
SystemAPI.GetSingletonRW<RpcCollection>().ValueRW.DynamicAssemblyList = true;
state.Enabled = false;
}
}
// RPC request from client to server for game to go "in game" and send snapshots / inputs
public struct GoInGameRequest : IRpcCommand
{
}
// When client has a connection with network id, go in game and tell server to also go in game
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct GoInGameClientSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// Run only on entities with a CubeSpawner component data
state.RequireForUpdate<CubeSpawner>();
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<NetworkId>()
.WithNone<NetworkStreamInGame>();
state.RequireForUpdate(state.GetEntityQuery(builder));
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
foreach (var (id, entity) in SystemAPI.Query<RefRO<NetworkId>>().WithEntityAccess().WithNone<NetworkStreamInGame>())
{
commandBuffer.AddComponent<NetworkStreamInGame>(entity);
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<GoInGameRequest>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequest { TargetConnection = entity });
}
commandBuffer.Playback(state.EntityManager);
}
}
// When server receives go in game request, go in game and delete request
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct GoInGameServerSystem : ISystem
{
private ComponentLookup<NetworkId> networkIdFromEntity;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<CubeSpawner>();
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<GoInGameRequest>()
.WithAll<ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
networkIdFromEntity = state.GetComponentLookup<NetworkId>(true);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Get the prefab to instantiate
var prefab = SystemAPI.GetSingleton<CubeSpawner>().Cube;
// Ge the name of the prefab being instantiated
state.EntityManager.GetName(prefab, out var prefabName);
var worldName = new FixedString32Bytes(state.WorldUnmanaged.Name);
var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
networkIdFromEntity.Update(ref state);
foreach (var (reqSrc, reqEntity) in SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<GoInGameRequest>().WithEntityAccess())
{
commandBuffer.AddComponent<NetworkStreamInGame>(reqSrc.ValueRO.SourceConnection);
// Get the NetworkId for the requesting client
var networkId = networkIdFromEntity[reqSrc.ValueRO.SourceConnection];
// Log information about the connection request that includes the client's assigned NetworkId and the name of the prefab spawned.
UnityEngine.Debug.Log($"'{worldName}' setting connection '{networkId.Value}' to in game, spawning a Ghost '{prefabName}' for them!");
// Instantiate the prefab
var player = commandBuffer.Instantiate(prefab);
// Associate the instantiated prefab with the connected client's assigned NetworkId
commandBuffer.SetComponent(player, new GhostOwner { NetworkId = networkId.Value});
// Add the player to the linked entity group so it is destroyed automatically on disconnect
commandBuffer.AppendToBuffer(reqSrc.ValueRO.SourceConnection, new LinkedEntityGroup{Value = player});
commandBuffer.DestroyEntity(reqEntity);
}
commandBuffer.Playback(state.EntityManager);
}
}
If you press Play now, you should see the replicated cube in the Game view and the Entity Hierarchy view.
Replicated cube
Moving the cube
Because you used the Support Auto Command Target feature when you set up the ghost component, you can take advantage of the IInputComponentData
struct for storing input data. This struct dictates what you will be serializing and deserializing as the input data. You also need to create a system that fills in input data.
Create a script called CubeInputAuthoring.cs and add the following code:
using Unity.Burst;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
public struct CubeInput : IInputComponentData
{
public int Horizontal;
public int Vertical;
}
[DisallowMultipleComponent]
public class CubeInputAuthoring : MonoBehaviour
{
class CubeInputBaking : Unity.Entities.Baker<CubeInputAuthoring>
{
public override void Bake(CubeInputAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<CubeInput>(entity);
}
}
}
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial struct SampleCubeInput : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<NetworkStreamInGame>();
state.RequireForUpdate<CubeSpawner>();
}
public void OnUpdate(ref SystemState state)
{
foreach (var playerInput in SystemAPI.Query<RefRW<CubeInput>>().WithAll<GhostOwnerIsLocal>())
{
playerInput.ValueRW = default;
if (Input.GetKey("left"))
playerInput.ValueRW.Horizontal -= 1;
if (Input.GetKey("right"))
playerInput.ValueRW.Horizontal += 1;
if (Input.GetKey("down"))
playerInput.ValueRW.Vertical -= 1;
if (Input.GetKey("up"))
playerInput.ValueRW.Vertical += 1;
}
}
}
Add the CubeInputAuthoring
component to your cube prefab, and then finally, create a system that can read the CubeInput
and move the player.
Create a new file script called CubeMovementSystem.cs
and add the following code:
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
using Unity.Burst;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[BurstCompile]
public partial struct CubeMovementSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var speed = SystemAPI.Time.DeltaTime * 4;
foreach (var (input, trans) in SystemAPI.Query<RefRO<CubeInput>, RefRW<LocalTransform>>().WithAll<Simulate>())
{
var moveInput = new float2(input.ValueRO.Horizontal, input.ValueRO.Vertical);
moveInput = math.normalizesafe(moveInput) * speed;
trans.ValueRW.Position += new float3(moveInput.x, 0, moveInput.y);
}
}
}
Test the code
Now you have set up your code, open Multiplayer > PlayMode Tools and set the PlayMode Type to Client & Server. Enter Play Mode, and the cube spawns. Press the Arrow keys to move the cube around.
Build standalone build and connect an Editor-based client
Now that you have the server-client instance running in the Editor, you might want to see what it would be like to test connecting another client. To do this follow these steps:
- Verify that your Project Settings > Entities > Build > NetCode Client Target is set to ClientAndServer.
- Make a development build and run that standalone build.
- Select the Multiplayer menu bar option and select the Editor PlayMode Tools window.
- Set the PlayMode Type to: Client
- Set the Auto Connect Port to: 7979
- Optionally, you can dock or close this window at this point.
- Enter Play Mode
You should now see on your server-client standalone build the Editor-based client's cube and be able to see both cubes move around!