docs.unity3d.com
Search Results for

    Show / Hide Table of Contents

    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:

    1. Right-click within the Hierarchy window in the Unity Editor.
    2. Select New Subscene > Empty Scene...
    3. 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
    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.

    Hierarcy View
    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 RPCs. 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:

    1. Create a cube in the scene by right-clicking on the scene, then selecting 3D Object > Cube.
    2. Select the Cube GameObject under the Scene and drag it into the Project’s Asset folder. This creates a prefab of the cube.
    3. After creating the prefab, you can delete the cube from the scene, but do not delete the prefab.

    Create a cube 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:

    1. Check the Has Owner box. This automatically adds and checks a new property called Support Auto Command Target (more on this later).
    2. 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
    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);
            }
        }
    }
    
    1. Right-click on SharedData and select Create Empty.
    2. Rename it to Spawner, then add a CubeSpawner.
    3. Because both the client and the server need to know about these ghosts, add it to the SharedData Subscene.
    4. In the Inspector, drag the cube prefab to the cube field of the spawner.

    Ghost Spawner settings
    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 the NetworkId of the requesting client.
    • 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
    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!


    Did you find this page useful? Please give it a rating:

    Thanks for rating this page!

    Report a problem on this page

    What kind of problem would you like to report?

    • This page needs code samples
    • Code samples do not work
    • Information is missing
    • Information is incorrect
    • Information is unclear or confusing
    • There is a spelling/grammar error on this page
    • Something else

    Thanks for letting us know! This page has been marked for review based on your feedback.

    If you have time, you can provide more information to help us fix the problem faster.

    Provide more information

    You've told us this page needs code samples. If you'd like to help us further, you could provide a code sample, or tell us about what kind of code sample you'd like to see:

    You've told us there are code samples on this page which don't work. If you know how to fix it, or have something better we could use instead, please let us know:

    You've told us there is information missing from this page. Please tell us more about what's missing:

    You've told us there is incorrect information on this page. If you know what we should change to make it correct, please tell us:

    You've told us this page has unclear or confusing information. Please tell us more about what you found unclear or confusing, or let us know how we could make it clearer:

    You've told us there is a spelling or grammar error on this page. Please tell us what's wrong:

    You've told us this page has a problem. Please tell us more about what's wrong:

    Thank you for helping to make the Unity documentation better!

    Your feedback has been submitted as a ticket for our documentation team to review.

    We are not able to reply to every ticket submitted.

    In This Article
    • Creating an initial scene
    • Establish a connection
    • Communicate with the server
    • Create a ghost prefab
    • Create a spawner
    • Spawning our prefab
      • Update GoInGameClientSystem and GoInGameServerSystem
    • Moving the cube
    • Test the code
    • Build standalone build and connect an Editor-based client
    Back to top
    Copyright © 2025 Unity Technologies — Trademarks and terms of use
    • Legal
    • Privacy Policy
    • Cookie Policy
    • Do Not Sell or Share My Personal Information
    • Your Privacy Choices (Cookie Settings)