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. 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:

    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 , 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
    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 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 RPCs. 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 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)
        {
            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 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)
            {
                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 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, 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 Sub Scene.
    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. 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.

    Update GoInGameClientSystem and GoInGameServerSystem

    We want the GoInGameClientSystem and GoInGameServerSystem to only run on the entities that have CubeSpawner component data associated with them. In order to do this we will 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, we will instantiate an instance of the prefab.
      • For each prefab instance we will set the GhostOwner.NetworkId value to the NetworkId of the requesting client.
    • Finally we will add the newly instantiated instance to the LinkedEntityGroup so when the client disconnects the entity will be 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
    {
    }
    
    [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);
        }
    }
    
    [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)
        {
            // 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 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;
    
    public struct CubeInput : IInputComponentData
    {
        public int Horizontal;
        public int Vertical;
    }
    
    [DisallowMultipleComponent]
    public class CubeInputAuthoring : MonoBehaviour
    {
        class Baking : 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 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.

    Build Stand Alone Build & 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. In order 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 stand alone build.
    • Select the Multiplayer menu bar option and select the editor play mode 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 into PlayMode

    You should now see on your server-client stand alone build the editor-based client's cube and be able to see both cubes move around!

    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 Stand Alone Build & Connect an Editor-Based Client
    Back to top
    Copyright © 2024 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)