RPCs | Unity NetCode | 0.6.0-preview.7
docs.unity3d.com
    Show / Hide Table of Contents

    RPCs

    NetCode uses a limited form of RPCs to handle events. A job on the sending side can issue RPCs, and they then execute on a job on the receiving side. This limits what you can do in an RPC; such as what data you can read and modify, and what calls you are allowed to make from the engine. For more information on the Job System see the Unity User Manual documentation on the C# Job System.

    To make the system a bit more flexible, you can use the flow of creating an entity that contains specific netcode components such as SendRpcCommandRequestComponent and ReceiveRpcCommandRequestComponent, which this page outlines.

    Extend IRpcCommand

    To start, create a command by extending the IRpcCommand:

    public struct OurRpcCommand : IRpcCommand
    {
    }
    

    Or if you need some data in your RPC:

    public struct OurRpcCommand : IRpcCommand
    {
        public int intData;
        public short shortData;
    }
    

    This will generate all the code you need for serialization and deserialization as well as registration of the RPC.

    Sending and recieving commands

    To complete the example, you must create some entities to send and recieve the commands you created. To send the command you need to create an entity and add the command and the special component SendRpcCommandRequestComponent to it. This component has a member called TargetConnection that refers to the remote connection you want to send this command to.

    Note

    If TargetConnection is set to Entity.Null you will broadcast the message. On a client you don't have to set this value because you will only send to the server.

    The following is an example of a simple send system:

    [UpdateInGroup(typeof(ClientSimulationSystemGroup))]
    public class ClientRcpSendSystem : ComponentSystem
    {
        protected override void OnCreate()
        {
            RequireSingletonForUpdate<NetworkIdComponent>();
        }
    
        protected override void OnUpdate()
        {
            if (Input.GetKey("space"))
            {
                var req = PostUpdateCommands.CreateEntity();
                PostUpdateCommands.AddComponent(req, new OurRpcCommand());
                PostUpdateCommands.AddComponent(req, new SendRpcCommandRequestComponent());
            }
        }
    }
    

    This system sends a command if the user presses the space bar on their keyboard.

    In the previous example, the RpcExecutor.ExecuteCreateRequestComponent<OurRpcCommand>(ref parameters); function call to the IRpCommand creates an entity that you can filter on. To test if this works, the following example creates a system that receives the OurRpcCommand:

    [UpdateInGroup(typeof(ServerSimulationSystemGroup))]
    public class ServerRpcReceiveSystem : ComponentSystem
    {
        protected override void OnUpdate()
        {
            Entities.ForEach((Entity entity, ref OurRpcCommand cmd, ref ReceiveRpcCommandRequestComponent req) =>
            {
                PostUpdateCommands.DestroyEntity(entity);
                Debug.Log("We received a command!");
            });
        }
    }
    

    The RpcSystem automatically finds all of the requests, sends them, and then deletes the send request. On the remote side they show up as entities with the same IRpcCommand and a ReceiveRpcCommandRequestComponent which you can use to identify which connection the request was received from.

    Creating an RPC without generating code

    The code generation for RPCs is optional, if you do not wish to use it you need to create a component and a serializer. These can be the same struct or two different ones. To create a single struct which is both the component and the serializer you would need to add:

    [BurstCompile]
    public struct OurRpcCommand : IComponentData, IRpcCommandSerializer<OurRpcCommand>
    {
        public void Serialize(ref DataStreamWriter writer, in OurRpcCommand data)
        {
        }
    
        public void Deserialize(ref DataStreamReader reader, ref OurRpcCommand data)
        {
        }
    
        public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
        {
        }
    
        [BurstCompile]
        private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
        {
        }
    
        static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer = new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
    }
    

    The IRpcCommandSerializer interface has three methods: Serialize, Deserialize, and CompileExecute. Serialize and Deserialize store the data in a packet, while CompileExecute uses Burst to create a FunctionPointer. The function it compiles takes a RpcExecutor.Parameters by ref that contains:

    • DataStreamReader reader
    • Entity connection
    • EntityCommandBuffer.Concurrent commandBuffer
    • int jobIndex

    Because the function is static, it needs to use Deserialize to read the struct data before it executes the RPC. The RPC then either uses the command buffer to modify the connection entity, or uses it to create a new request entity for more complex tasks. It then applies the command in a separate system at a later time. This means that you don’t need to perform any additional operations to receive an RPC; its Execute method is called on the receiving end automatically.

    To create an entity that holds an RPC, use the function ExecuteCreateRequestComponent<T>. To do this, extend the previous InvokeExecute function example with:

    [BurstCompile]
    private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
    {
        RpcExecutor.ExecuteCreateRequestComponent<OurRpcCommand, OurRpcCommand>(ref parameters);
    }
    

    This creates an entity with a ReceiveRpcCommandRequestComponent and OurRpcCommand components.

    Once you create an IRpcCommandSerializer, you need to make sure that the RpcCommandRequest system picks it up. To do this, you can create a system that extends the RpcCommandRequest system, as follows:

    public class OurRpcCommandRequestSystem : RpcCommandRequestSystem<OurRpcCommand, OurRpcCommand>
    {
        [BurstCompile]
        protected struct SendRpc : IJobEntityBatch
        {
            public SendRpcData data;
            public void Execute(ArchetypeChunk chunk, int orderIndex)
            {
                data.Execute(chunk, orderIndex);
            }
        }
        protected override void OnUpdate()
        {
            var sendJob = new SendRpc{data = InitJobData()};
            ScheduleJobData(sendJob);
        }
    }
    

    The RpcCommandRequest system uses an RpcQueue internally to schedule outgoing RPCs.

    A note about serialization

    You might have data that you want to attach to the RpcCommand. To do this, you need to add the data as a member of your command and then use the Serialize and Deserialize functions to decide on what data should be serialized. See the following code for an example of this:

    [BurstCompile]
    public struct OurDataRpcCommand : IComponentData, IRpcCommandSerializer<OurDataRpcCommand>
    {
        public int intData;
        public short shortData;
    
        public void Serialize(ref DataStreamWriter writer, in OurDataRpcCommand data)
        {
            writer.WriteInt(data.intData);
            writer.WriteShort(data.shortData);
        }
    
        public void Deserialize(ref DataStreamReader reader, ref OurDataRpcCommand data)
        {
            data.intData = reader.ReadInt();
            data.shortData = reader.ReadShort();
        }
    
        public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
        {
        }
    
        [BurstCompile]
        private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
        {
            RpcExecutor.ExecuteCreateRequestComponent<OurDataRpcCommand, OurDataRpcCommand>(ref parameters);
        }
    
        static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer = new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
    }
    
    Note

    To avoid problems, make sure the serialize and deserialize calls are symmetric. The example above writes an int then a short, so your code needs to read an int then a short in that order. If you omit reading a value, forget to write a value, or change the order of the way the code reads and writes, you might have unforeseen consequences.

    RpcQueue

    The RpcQueue is used internally to schedule outgoing RPCs. However, you can use OnGetOrCreateManager to manually create your own schedule. To do this, call m_RpcQueue = World.GetOrCreateManager<RpcSystem>().GetRpcQueue<OurRpcCommand>(); and cache it through the lifetime of your application. When you have the queue, get the OutgoingRpcDataStreamBufferComponent from an entity to schedule events in the queue and then call rpcQueue.Schedule(rpcBuffer, new OurRpcCommand);, as follows:

    [UpdateInGroup(typeof(ClientSimulationSystemGroup))]
    public class ClientQueueRcpSendSystem : ComponentSystem
    {
        private RpcQueue<OurRpcCommand, OurRpcCommand> rpcQueue;
        protected override void OnCreate()
        {
            RequireSingletonForUpdate<NetworkIdComponent>();
            rpcQueue = World.GetOrCreateSystem<RpcSystem>().GetRpcQueue<OurRpcCommand, OurRpcCommand>();
        }
    
        protected override void OnUpdate()
        {
            if (Input.GetKey("space"))
            {
                Entities.ForEach((Entity entity, ref NetworkStreamConnection connection) =>
                {
                    var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
                    if (rpcFromEntity.Exists(entity))
                    {
                        var buffer = rpcFromEntity[entity];
                        rpcQueue.Schedule(buffer, new OurRpcCommand());
                    }
                });
            }
        }
    }
    

    This example sends an RPC using the RpcQueue when the user presses the space bar on their keyboard.

    In This Article
    • Extend IRpcCommand
    • Sending and recieving commands
    • Creating an RPC without generating code
    • A note about serialization
    • RpcQueue
    Back to top
    Copyright © 2023 Unity Technologies — Terms of use
    • Legal
    • Privacy Policy
    • Cookies
    • Do Not Sell or Share My Personal Information
    • Your Privacy Choices (Cookie Settings)
    "Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
    Generated by DocFX on 18 October 2023