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. We achieve this separation 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 , we want to spawn a plane in both the client and the server world. To do this, right click the SharedData Sub Scene and select 3D Object > Plane which then creates a planes 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 are connected, you can start communication. A critical concept in Netcode for Entities is the concept of InGame
. When a connection is marked with InGame
it tells the simulation its ready to start synchronizing.
You communicate with Netcode for Entities by using RPC
s. So to continue create a 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 Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using Unity.Burst;
// 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 a IComponent
and Author it. To do so create a new file called CubeAuthoring.cs and we enter the following:
using Unity.Entities;
using UnityEngine;
public struct Cube : IComponentData
{
}
[DisallowMultipleComponent]
public class CubeAuthoring : MonoBehaviour
{
class Baker : Baker<CubeAuthoring>
{
public override void Bake(CubeAuthoring authoring)
{
Cube component = default(Cube);
AddComponent(component);
}
}
}
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 sub-scene. 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);
AddComponent(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 Sub Scene.
- 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. If you recall from earlier, you must send a GoInGame RPC
when you are ready to tell the server to start synchronizing. You can update that code to actually spawn our cube as well.
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using Unity.Burst;
public struct GoInGameRequest : IRpcCommand
{
}
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct GoInGameClientSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
+ 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);
}
}
[BurstCompile]
// When server receives go in game request, go in game and delete request
[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)
{
+ var prefab = SystemAPI.GetSingleton<CubeSpawner>().Cube;
+ 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);
var networkId = networkIdFromEntity[reqSrc.ValueRO.SourceConnection];
- UnityEngine.Debug.Log($"'{worldName}' setting connection '{networkId.Value}' to in game");
+ UnityEngine.Debug.Log($"'{worldName}' setting connection '{networkId.Value}' to in game, spawning a Ghost '{prefabName}' for them!");
+ var player = commandBuffer.Instantiate(prefab);
+ 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 will fill in our input data.
Create a script called CubeInputAuthoring.cs and add the following code:
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
[GhostComponent(PrefabType=GhostPrefabType.AllPredicted)]
public struct CubeInput : IInputComponentData
{
public int Horizontal;
public int Vertical;
}
[DisallowMultipleComponent]
public class CubeInputAuthoring : MonoBehaviour
{
class Baking : Unity.Entities.Baker<CubeInputAuthoring>
{
public override void Bake(CubeInputAuthoring authoring)
{
AddComponent<CubeInput>();
}
}
}
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial struct SampleCubeInput : ISystem
{
public void OnUpdate(ref SystemState state)
{
bool left = UnityEngine.Input.GetKey("left");
bool right = UnityEngine.Input.GetKey("right");
bool down = UnityEngine.Input.GetKey("down");
bool up = UnityEngine.Input.GetKey("up");
foreach (var playerInput in SystemAPI.Query<RefRW<CubeInput>>().WithAll<GhostOwnerIsLocal>())
{
playerInput.ValueRW = default;
if (left)
playerInput.ValueRW.Horizontal -= 1;
if (right)
playerInput.ValueRW.Horizontal += 1;
if (down)
playerInput.ValueRW.Vertical -= 1;
if (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.