Build a session with Netcode for Entities
Build a multiplayer game with sessions using Netcode for Entities for projects using DOTS.
Note: The Multiplayer Services SDK uses sessions to manage groups of players. Sessions relies internally on different combinations of Unity Gaming Services such as Relay, Distributed Authority, Lobby, Matchmaker and Multiplay Hosting, and thus contributes to the billing of those services.
If you haven't built a session-based multiplayer game before, the recommended best practice is to Build a session with Netcode for GameObjects first.
Similar to Build a session with Netcode for GameObjects, use the principles of Multiplayer Services sessions to create one capsule controlled by the session's host and the other capsule controlled by the joining player. However, this time, use Network for Entities, which is part of Unity's Data-Oriented Technology Stack (DOTS).
Important: Before you can create or join a Multiplayer Services session, you must create your Netcode for Entities client and server worlds. When you first add the Netcode for Entities package to your project, the default bootstrap automatically creates the client and server worlds at startup. For more advanced use cases, use a custom network handler to circumvent the default integration with Netcode for Entities.
Prerequisites
In addition to the Multiplayer Services package, ensure that you install the following packages through the Unity Registry in the Editor's Package Manager:
Widgets
(orcom.unity.multiplayer.widgets
): Enables you to test multiplayer services without having to write code.Multiplayer Play Mode
(orcom.unity.multiplayer.playmode
): Enables you to test multiplayer functionality without leaving the Unity Editor.- Packages required by Netcode for Entities, which include entities, entities graphics, and networking capabilities:
Netcode for Entities
(orcom.unity.netcode
), version 1.3.0 or later.Note: This also installs the corresponding
Entities
package.Entities Graphics
(orcom.unity.entities.graphics
), version 1.3.0 or later.
If Netcode for GameObjects is installed, then remove the package to avoid conflicts.
Note: If the error Burst error BC1055: Unable to resolve the definition of the method 'Unity.Collections.DataStreamReader.Flush()'
appears in the console after you uninstall Netcode for GameObjects, then restart the Editor.
Configure Multiplayer Play Mode
To configure Play Mode for your project to test multiple players:
- In the Unity Editor, go to Window > Multiplayer > Multiplayer Play Mode.
- In the Multiplayer Play Mode window, enable Player 2.
After you add Player 2, you can test the project with a second window.
Configure Build Profiles and Project Settings
You must add the current scene to the build to enter Play Mode.
To add the current scene to the build:
- In the Unity Editor, select File > Build Profiles.
- Select Open Scene List.
- Select Add Open Scenes.
- Close Build Profiles.
Your project should also run in the background to prevent dropped connections while testing, for example when switching to a different window.
To update your game to run in the background:
- Select Edit > Project Settings > Player > Resolution and Presentation.
- Enable Run in Background.
- Close Project Settings.
Create an initial scene
Set up a way to share data between the client and the server in Netcode for Entities:
- Right-click within the Hierarchy window in the Unity Editor.
- Select New Sub Scene > Empty Scene.
- Name the new scene
SharedData.unity
.
This new scene sets up a method for data sharing between the client and server by creating a different World for the server and each client, using the Entities package.
Create a ghost prefab
To synchronize data across a client and server setup, create a definition of the networked object, called a ghost.
To create a ghost prefab:
- In the Hierarchy window, right-click on the new scene.
- Select GameObject > 3D Object > Capsule.
- Rename the capsule to
Player Prefab
. - In the Project tab, right-click the
Assets
folder, then Create > Folder. - Name the new folder
Prefabs
. - Drag the
Player Prefab
object from the Hierarchy window into thePrefabs
folder to make it into a prefab. - In the Hierarchy window, right-click the capsule, then select Delete. This deletes the player from the scene.
To identify and synchronize the prefab using Netcode for Entities, you need to create an IComponent
and Author
for it:
In the Project tab, right-click the
Assets
folder, then Create > Folder.Name the new folder
Scripts
.Right-click the new Scripts folder, then Create > Scripting > Empty C# Script.
Rename this script
PlayerAuthoring.cs
.Double-click the script to edit it.
Replace the contents of this script with the following code:
using Unity.Entities; using Unity.NetCode; using UnityEngine; public struct Player : IComponentData { } [DisallowMultipleComponent] public class PlayerAuthoring : MonoBehaviour { class Baker : Baker<PlayerAuthoring> { public override void Bake(PlayerAuthoring authoring) { var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponent<Player>(entity); } } }
In the Project tab, select
Assets
>Prefabs
>Player Prefab
so its Inspector window opens.In the Project tab, select
Assets
>Scripts
.Drag the
Player Authoring
script to thePlayer Prefab
Inspector window to attach it.In the Inspector window, select Add component.
Search for
Ghost Authoring Component
, then select it and add it to the Prefab.When you attach a Ghost Authoring Component to a prefab, Unity automatically serializes the Translation and Rotation components and adds a Linked Entity Group Authoring script.
Before you can move the capsule around, you must change some settings in the newly-added Ghost Authoring Component:
- Enable Has Owner. This setting automatically adds and checks a new property called Support Auto Command Target.
- Change the Default Ghost Mode to Owner Predicted. Later, setting the
NetworkId
member of the Ghost Owner Component through code ensures that you predict your own movement.
You now have a prefab of a ghost capsule object.
Create a spawner
To tell Netcode for Entities which ghosts to use, reference the Prefabs from the sub-scene.
First, create a new component for the spawner:
- In the Project tab, right-click the Scripts folder, then Create > Scripting > Empty C# Script.
- Rename this script
PlayerSpawnerAuthoring.cs
. - Replace the contents of this script with the following code:
using Unity.Entities; using UnityEngine; public struct PlayerSpawner : IComponentData { public Entity Player; } [DisallowMultipleComponent] public class PlayerSpawnerAuthoring : MonoBehaviour { public GameObject PlayerPrefab; class Baker : Baker<PlayerSpawnerAuthoring> { public override void Bake(PlayerSpawnerAuthoring authoring) { PlayerSpawner component = default(PlayerSpawner); component.Player = GetEntity(authoring.PlayerPrefab, TransformUsageFlags.Dynamic); var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponent(entity, component); } } }
- Right-click within the Hierarchy window, then select Create Empty.
- Rename the empty GameObject to
Spawner
. - With the empty object selected in the Hierarchy window, in its Inspector window, select Add Component.
- Attach the
Player Spawner Authoring
script. - With the Inspector window still open, select the Assets > Prefabs folder from the Project tab.
- Drag the Player Prefab object to the Player Prefab field in the Spawner's Inspector window.
- In the Hierarchy window, move the Spawner into the SharedData sub-scene.
You have now created a new component for the Spawner, instructed it which ghosts to use, and ensured that both the client and the server know about these ghosts when they spawn.
Enable communication and spawn the prefab
To enable communication between the client and server, you need to establish a connection. The Multiplayer Services SDK manages connections so you can focus on which messages are communicated. Netcode for Entities uses remote procedure calls (RPC) for its messages.
A critical concept in Netcode for Entities is InGame
. When a connection is marked with InGame
, it tells the simulation that it is ready to start synchronizing. You must send a GoInGame
RPC when you are ready for the server to start synchronizing and to spawn the prefab.
To create an RPC that acts as a Go In Game message:
- In the Project tab, right-click the Scripts folder, then Create > Scripting > Empty C# Script.
- Rename this script
GoInGameSystems.cs
. - Replace the contents of this script with the following code:
Tip: Any class inheriting from
ISystem
is automatically invoked by the runtime, and the sample below combines related systems into one file for brevity.using UnityEngine; using Unity.Collections; using Unity.Entities; using Unity.NetCode; using Unity.Burst; /// <summary> /// This allows sending RPCs between a standalone build and the Editor for testing purposes in the event that, when you finish this example, /// you want to connect a server-client standalone 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 PlayerSpawner component data state.RequireForUpdate<PlayerSpawner>(); 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<PlayerSpawner>(); 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<PlayerSpawner>().Player; // 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); } }
You have now created an RPC that tells the simulation that it's ready to start synchronizing.
Move the prefab
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 are serializing and deserializing as the input data. You also need to create a System
to fill in the input data.
To create the IInputComponentData
struct:
- In the Project tab, right-click the Scripts folder, then Create > Scripting > Empty C# Script.
- Rename this script
PlayerInputAuthoring.cs
. - Replace the contents of this script with the following code:
using Unity.Burst; using Unity.Entities; using Unity.NetCode; using UnityEngine; public struct PlayerInput : IInputComponentData { public float Horizontal; public float Vertical; } [DisallowMultipleComponent] public class PlayerInputAuthoring : MonoBehaviour { class PlayerInputBaking : Unity.Entities.Baker<PlayerInputAuthoring> { public override void Bake(PlayerInputAuthoring authoring) { var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponent<PlayerInput>(entity); } } } [UpdateInGroup(typeof(GhostInputSystemGroup))] public partial struct SamplePlayerInput : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<NetworkStreamInGame>(); state.RequireForUpdate<PlayerSpawner>(); } public void OnUpdate(ref SystemState state) { foreach (var playerInput in SystemAPI.Query<RefRW<PlayerInput>>().WithAll<GhostOwnerIsLocal>()) { playerInput.ValueRW = default; playerInput.ValueRW.Horizontal = Input.GetAxis("Horizontal"); playerInput.ValueRW.Vertical = Input.GetAxis("Vertical"); } } }
- In the Project tab, select
Assets
>Prefabs
>Player Prefab
. - Attach the
Player Input Authoring
script to thePlayer Prefab
Inspector window.
Finally, create a system that can read this input and move the player:
- In the Project tab, right-click the Scripts folder, then Create > Scripting > Empty C# Script.
- Rename this script
PlayerMovementSystem.cs
. - Replace the contents of this script with 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 PlayerMovementSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { var speed = SystemAPI.Time.DeltaTime * 4; foreach (var (input, trans) in SystemAPI.Query<RefRO<PlayerInput>, 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); } } }
You have now set up a way to accept user input, read that input, and then move the player's capsule object.
Create and join your session
The following steps outline how to create and join a session using Netcode for Entities with Widgets and Multiplayer Services in the Unity Editor.
- Right-click in the Hierarchy window, then select Multiplayer Widgets > Create > Create Session.
Notes:
- If you receive a TMP Importer prompt, select Import TMP Essentials.
- If you do not see this prompt and the widget has no label, select Window > TextMeshPro > Import TMP Essential Resources.
- In the Hierarchy window, select Canvas > Create Session.
- In its Inspector window, set Rect Transform > Pos Y to
50
. - Right-click in the Hierarchy window, then select Multiplayer Widgets > Info > Show Session Code.
- Right-click in the Hierarchy window, then select Multiplayer Widgets > Join and Leave > Join Session By Code.
- In the Hierarchy window, select Canvas > Join Session By Code.
- In its Inspector window, set Rect Transform > Pos Y to
-100
. - In the Unity Editor, select File > Save to refresh the scene for Player 2.
- In the Toolbar, select Play to enter Play Mode.
- In the Game window's Session Name field, enter a name and then select Create.
- Copy the session join code.
- In the Player 2 window's Session Code field, enter the code and then select Join.
You have now created a session in one of the Editor windows and joined it using the displayed join code from the other Editor window. After joining the session, two superimposed capsules appear in the scene.
Move the capsules
To move the capsules in your multiplayer session:
- Click within the Game window of the Editor.
- Use the arrow keys on your keyboard to move the capsule around the scene. It moves in real time in both the Game window and the Player 2 window.
- Click within the Player 2 window.
- Use the arrow keys on your keyboard to move the capsule around the scene. Notice that the other capsule (Player 2's capsule) moves this time.
By entering Play Mode and joining the session, you now have a scene of two capsules that can only each be controlled by the owning player.
Additional resources
- Networked cube (Unity Netcode for Entities)
- Get acquainted with DOTS (Unity Learn)
- Multiplayer Services SDK