Creating an [RPC] [Command] System

In UNET there are [RPC]'s and [Commands] which allow you to invoke methods on the server or client without having to manage serialization, serialization, or finding the script and method on the target. They are really nice, but I’ve since moved away from UNET to Steamworks.NET.

I’d like to create a system that can do [RPC]'s or [Command]'s in a similar fashion as UNET.

ExampleMethod1( )
ExampleMethod2(float someFloat)
ExampleMethod3(int someInt, string someString)

where I could invoke these remotely by calling for example
-InvokeOnServer(ExampleMethod1) ;
-InvokeOnClient(ClientID, ExampleMethod2, 5.7f);
-InvokeOnAll(ExampleMethod3, 234, “This is a string”);

I’ve have a system set up to send network messages containing a byte array. I can serialize/deserialize fields like float, int, byte, string, etc. into and from the byte array. So the process looks something like this.
-During initialization all RPC/Command methods are registered and assigned methodID’s stored in a dictionary (int UNET they are registered with [RPC] and [Command] attributes)
-Call InvokeOnX(MethodName, args)
-A network message is created containing the objectID, methodID, and serialized arg data
-The message is sent to 1 or more targets across the network
-When received the message is deserialized extracting the objectID, methodID
-The object is found from the objectID
-The method is found on that object from the methodID
-Once the method is found it’s arg types are extracted (For example Method3 would have types int and string)
-Once the arg types are known they are deserialized from the network message (i.e. for ExampleMethod3 it would read intValue and stringValue)
-The method is inoveked locally on the target…i.e. ExampleMethod3(intValue, stringValue)

I have it all working, but I find myself duplicating code for each permutation of method arguments. I would like to generically handle any number or type of serializeable input arguments without having to duplicate code. But I am unsure how to do this. Using generics would be nice but not 100% necessary as these RPC/Command’s are sent very infrequently and boxing/unboxing overhead is not an issue (so reflection and casting wouldn’t be bad if it worked).

Any ideas how to do this?

Thanks in advance.

1 Like

I made it with reflection. Works fine for the moment.

I store Method type and parameter. When i receive id i find the Method in my dictionnary and parameters.

I loop throught parameters and use reglection to check type and drsereliazing. The fun fact is that patameter find with reflection are in the same order than definition…so it s cool.
Finally i invoke method with the byte array deserialized.

You can find my repo by typing hlapi ext on this forum

1 Like

Thanks. Look like that will work!

@Driiade
I found this

But there’s a lot of files and I can’t find the one you are referring to. Which file name are you referring to?

Do you store the handles to your methods in your dictionary as Delegates, MethodInfo, or something else. The trouble I’ve had with Delegates is they are type specific can’t handle variable # of parameters while being typesafe. MethodInfo same problem… not sure how to make it typesafe (MethodInfo found with non-typesafe string, and also when the end user Invoke’s the paramaters aren’t typesafe?).

I understand internal to this RPC/Command script things will not be strongly typed or anything and that’s OK. But trying to make it so that the end user has a typesafe interface to invoke.

Thanks.

The NetworkingBehaviour.

        MethodInfo[] GetNetworkedMethods()
        {
            MethodInfo[] allMethods = this.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            List<MethodInfo> networkedMethods = new List<MethodInfo>();
            for (int i = 0; i < allMethods.Length; i++)
            {
                object[] attributes = allMethods[i].GetCustomAttributes(typeof(Networked),true);
                for (int j = 0; j < attributes.Length; j++)
                {
                    if(attributes[j] is Networked)
                    {
                        networkedMethods.Add(allMethods[i]);
                    }
                }
            }

            return networkedMethods.ToArray();
        }
     public void SendToServer(string methodName, int channelId,params object[] parameters)
        {
            writer.SeekZero(true);
            writer.StartMessage();
            writer.Write(NetworkingMessageType.Command);
            SerializeCall(writer, methodName, parameters);
            writer.FinishMessage();

            connection.Send(writer, channelId);
        }
        void SerializeCall(NetworkingWriter writer, string methodName, object[] parameters)
        {
            writer.Write(this.networkingIdentity.netId);
            writer.Write(this.m_netId);
            writer.Write(GetNetworkMethodIndex(methodName, networkedMethods));
            //Debug.Log(methodName + " : " + GetNetworkMethodIndex(methodName, networkedMethods));
            for (int i = 0; i < parameters.Length; i++)
            {
                object param = parameters[i];

                if (param is int)
                {
                    writer.Write((int)param);
                }
                else if (param is string)
                {
                    writer.Write((string)param);
                }
                else if (param is ushort)
                {
                    writer.Write((ushort)param);
                }
                else if (param is byte[])
                {
                    writer.WriteBytesAndSize(((byte[])param), ((byte[])param).Length);
                }
                else
                    throw new System.Exception("Serialization is impossible : " + param.GetType());
            }
        }
        internal void HandleMethodCall(NetworkingReader reader)
        {
            byte methodIndex = reader.ReadByte();

            MethodInfo method = networkedMethods[methodIndex];
            ParameterInfo[] parameterInfos = method.GetParameters();
            object[] parameters = new object[parameterInfos.Length];

            for (int i = 0; i < parameterInfos.Length; i++)
            {
                ParameterInfo info = parameterInfos[i];
                if (info.ParameterType == typeof(int))
                {
                    parameters[i] = reader.ReadInt32();
                }
                  else if(info.ParameterType == typeof(string))
                {
                    parameters[i] = reader.ReadString();
                }
                else if (info.ParameterType == typeof(ushort))
                {
                    parameters[i] = reader.ReadUInt16();
                }
                else if (info.ParameterType == typeof(byte[]))
                {
                    parameters[i] = reader.ReadBytesAndSize();
                }
                else
                    throw new System.Exception("UnSerialization is impossible : " + info.ParameterType.GetType());
            }

            method.Invoke(this, parameters);
        }

I don’t think there is a way to strongly type the method call. You can do what UNET did with code generation. But you have to describe a new function for each use case (like a different channel/target), however with this method i can easily change my target/channel.

And the not typed things is not so dramatic, the method call just fail if so, and you have error on your game. It’s easy to repair.

It’s based on photon Rpc/Command call

1 Like

Thanks for taking the time to post that. You helped me and I got it working.

In your system
The good: Can handle any number of parameters.
The bad: The method string name and the parameters are not typesafe (i.e. you could accidentally send a float to a method that requires an integer). Errors won’t be thrown until runtime.

I tried as best I could to use strongly typed and couldn’t figure it out completely. But managed to…
The good: The method name and parameters are typesafe.
The bad: is a little redundant during register (it’s not implicit) and you need a delegate for each number of parameters (but you only need to do this once). Methods must be registered manually (but you could make this automatic as you did).

In both cases the internal data is boxed/unboxed to and from object causing a little overhead (not a big deal assuming these methods aren’t called frequently).

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Reflection;

public class CommandControllerGeneric
{
    public delegate void DelegateComand0();
    public delegate void DelegateComand1<T>(T tIn);
    public delegate void DelegateComand2<T1, T2>(T1 t1In, T2 t2In);
    public delegate void DelegateComand3<T1, T2, T3>(T1 t1In, T2 t2In, T3 t3In);
    public delegate void DelegateComand4<T1, T2, T3, T4>(T1 t1In, T2 t2In, T3 t3In, T4 t4In);
    public delegate void DelegateComand5<T1, T2, T3, T4, T5>(T1 t1In, T2 t2In, T3 t3In, T4 t4In, T5 t5In);

    private BiDictionary<byte, Delegate> biDirectionaryCommandIndexToDelegate = new BiDictionary<byte, Delegate>();

    private SteamGameObject steamGameObject;

    public CommandControllerGeneric(SteamGameObject steamGameObjectIn)
    {
        steamGameObject = steamGameObjectIn;
    }

    public void InitializeRegisterCommand(DelegateComand0 delegateComandIn)
    {
        //Debug.Log("RegisterCommand0: " + delegateComandIn.Method.Name);
        biDirectionaryCommandIndexToDelegate.Add((byte)(biDirectionaryCommandIndexToDelegate.Count+1), delegateComandIn);
    }

    public void InitializeRegisterCommand<T>(DelegateComand1<T> delegateComandIn)
    {
        //Debug.Log("RegisterCommand1: " + delegateComandIn.Method.Name + " " + typeof(T).Name);
        biDirectionaryCommandIndexToDelegate.Add((byte)(biDirectionaryCommandIndexToDelegate.Count + 1), delegateComandIn);
    }

    public void InitializeRegisterCommand<T1, T2>(DelegateComand2<T1, T2> delegateComandIn)
    {
        //Debug.Log("RegisterCommand2: " + delegateComandIn.Method.Name + " " + typeof(T1).Name + " " + typeof(T2).Name);
        biDirectionaryCommandIndexToDelegate.Add((byte)(biDirectionaryCommandIndexToDelegate.Count + 1), delegateComandIn);
    }

    public void Invoke(SendTo sendToIn, DelegateComand0 delegateCommandIn)
    {
        InvokeCommandInternal(sendToIn, delegateCommandIn, new object[] { });
    }

    public void Invoke<T>(SendTo sendToIn, DelegateComand1<T> delegateCommandIn, T tIn)
    {
        InvokeCommandInternal(sendToIn, delegateCommandIn, new object[] { tIn });
    }

    public void Invoke<T1, T2>(SendTo sendToIn, DelegateComand2<T1, T2> delegateCommandIn, T1 t1In, T2 t2In)
    {
        InvokeCommandInternal(sendToIn, delegateCommandIn, new object[] { t1In, t2In });
    }

    private void InvokeCommandInternal(SendTo sendToIn, Delegate delegateIn, object[] objectArgArrayIn)
    {
        try
        {
            byte commandIndexOut;
            if (biDirectionaryCommandIndexToDelegate.TryGetAFromB(delegateIn, out commandIndexOut))
            {
                BatBitStreamWriter batBitStreamWriter = new BatBitStreamWriter();
                for (int i = 0; i < objectArgArrayIn.Length; i++)
                {
                    batBitStreamWriter.WriteCommandObject(objectArgArrayIn[i]);
                }
                bool[] bitArray = batBitStreamWriter.ToBitArray();
                SteamMessageCommand steamMessageCommand = new SteamMessageCommand(steamGameObject, commandIndexOut, bitArray);
                SendMessageInternal(steamMessageCommand, sendToIn);
            }
            else
            {
                BatDebug.Error(this, "46325hjntbg214y53t634", "Cannot Find Command");
            }
        }
        catch(Exception exceptionIn)
        {
            BatDebug.Exception(this, "534uhtgr156564i75kjy", exceptionIn);
        }
   
    }

    public void OnReceiveSteamMessageCommand(SteamMessageCommand steamMessageCommandIn) //Both Server AND Client can receive
    {
        try
        {
            Delegate delegateOut;
            if (biDirectionaryCommandIndexToDelegate.TryGetBFromA(steamMessageCommandIn.actionIndex, out delegateOut))
            {
                ParameterInfo[] parameterInfoArray = delegateOut.Method.GetParameters();
                object[] objectArray = new object[parameterInfoArray.Length];

                BatBitStreamReader batBitStreamReader = new BatBitStreamReader(steamMessageCommandIn.bitArray);
                for (int i = 0; i < parameterInfoArray.Length; i++)
                {
                    objectArray[i] = batBitStreamReader.ReadCommandObject(parameterInfoArray[i].ParameterType);
                }
                delegateOut.DynamicInvoke(objectArray);
            }
            else
            {
                BatDebug.Error(this, "35786uijht156536ujikyunrb", "Cannot Find Command");
            }
        }
        catch(Exception exceptionIn)
        {
            BatDebug.Exception(this, "473hjnbg31t2458u6i7uy551", exceptionIn);
        }
   
    }

    public enum SendTo { ST0_HeroOwnerToServer, ST1_ServerToOwner, ST2_ServerToAll } //this is the input, and then based on whether you are the Server or Clientit choses a SendFromTo

    private void SendMessageInternal(SteamMessageCommand steamMessageCommandIn, SendTo sendToIn)
    {
        if (sendToIn == SendTo.ST0_HeroOwnerToServer)
        {
            steamMessageCommandIn.SendToServer();
        }
        else if (sendToIn == SendTo.ST1_ServerToOwner)
        {
            steamMessageCommandIn.SendToClientOwner(steamGameObject.cSteamIdObjectOwner);
        }
        else if (sendToIn == SendTo.ST2_ServerToAll)
        {
            steamMessageCommandIn.SendFromServerToAllIncludingSelfFullyConnected();
        }
        else
        {
            BatDebug.Error(this, "3573753265262", "Invalid Enum");
        }
    }

}