Search Unity

Having RPC's handle passed objects in a generic fashion

Discussion in 'Netcode for GameObjects' started by cerestorm, Oct 14, 2022.

  1. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    In this game of mine there's a lot of communication between client and server and rather than have each class have their own rpc I pass the messages via a message service. There are two message services as there are two sections to the game (Portal and Game) but having two makes things a little awkward and it's causing a real problem at end game so I'm looking to combine them into just one.

    What I'm trying to do is have a single clientRpc and serverRpc that can handle message objects of different types, rather than calls that only handle specific objects. This is what I've come up with in testing:

    A base message class
    Code (CSharp):
    1. public class BaseMessage : INetworkSerializable
    2. {
    3.     protected DerivedType derivedType;
    4.     BaseMessage derivedMessage;
    5.  
    6.     public virtual void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    7.     {
    8.         Debug.Log("MessageBase writer: " + serializer.IsWriter + " reader: " + serializer.IsReader);
    9.  
    10.         if(serializer.IsWriter)
    11.         {
    12.             Debug.LogError("BaseMessage serializer IsWriter but base class does not serialize.");
    13.             return;
    14.         }
    15.  
    16.         serializer.SerializeValue(ref derivedType);
    17.  
    18.         switch (derivedType)
    19.         {
    20.             case DerivedType.Portal:
    21.                 derivedMessage = new PortalMessage();
    22.                 derivedMessage.NetworkSerialize(serializer);
    23.                 break;
    24.  
    25.             case DerivedType.Game:
    26.                 derivedMessage = new GameMessage();
    27.                 derivedMessage.NetworkSerialize(serializer);
    28.                 break;
    29.         }
    30.     }
    31.  
    32.     public DerivedType DerivedType { get => derivedType; set => derivedType = value; }
    33.     public BaseMessage DerivedMessage { get => derivedMessage; set => derivedMessage = value; }
    34. }
    A derived message class
    Code (CSharp):
    1. public class PortalMessage : BaseMessage
    2. {
    3.     PortalMessageType messageType;
    4.     int eventIndex;
    5.     IEventDetail eventDetail;
    6.  
    7.     public PortalMessage()
    8.     {
    9.         base.DerivedType = DerivedType.Portal;
    10.     }
    11.  
    12.     public override void NetworkSerialize<T>(BufferSerializer<T> serializer)
    13.     {
    14.         Debug.Log("PortalMessage writer: " + serializer.IsWriter + " reader: " + serializer.IsReader);
    15.  
    16.         if (serializer.IsWriter)
    17.         {
    18.             serializer.SerializeValue(ref base.derivedType);
    19.  
    20.             serializer.SerializeValue(ref messageType);
    21.             serializer.SerializeValue(ref eventIndex);
    22.  
    23.             eventDetail.Serialize(serializer.GetFastBufferWriter());
    24.         }
    25.         else if(serializer.IsReader)
    26.         {
    27.             serializer.SerializeValue(ref messageType);
    28.             serializer.SerializeValue(ref eventIndex);
    29.  
    30.             FastBufferReader fastBufferReader = serializer.GetFastBufferReader();
    31.  
    32.             switch (messageType)
    33.             {
    34.                 case PortalMessageType.Portal:
    35.                     break;
    36.                 case PortalMessageType.Lobby:
    37.                     break;
    38.                 case PortalMessageType.Construct:
    39.                     eventDetail = DeserializeConstructEvent(eventIndex, fastBufferReader);
    40.                     break;
    41.                 case PortalMessageType.Room:
    42.                     break;
    43.                 default:
    44.                     break;
    45.             }
    46.         }
    47.     }
    48.  
    49.     // simplified
    50.     private IEventDetail DeserializeConstructEvent(int index, FastBufferReader fastBufferReader)
    51.     {
    52.         IEventDetail eventDetail;
    53.  
    54.         eventDetail = new SceneEventDetail();
    55.         eventDetail.DeSerialize(fastBufferReader);
    56.  
    57.         return eventDetail;
    58.     }
    59.  
    60.     public override string ToString()
    61.     {
    62.         StringBuilder stringBuilder = new StringBuilder("PortalMessage");
    63.         stringBuilder.Append(" messageType: ").Append(messageType);
    64.         stringBuilder.Append(" eventIndex: ").Append(eventIndex);
    65.         stringBuilder.Append(" eventDetail: ").Append(eventDetail);
    66.  
    67.         return stringBuilder.ToString();
    68.     }
    69.  
    70.     public PortalMessageType MessageType { get => messageType; set => messageType = value; }
    71.     public int EventIndex { get => eventIndex; set => eventIndex = value; }
    72.     public IEventDetail EventDetail { get => eventDetail; set => eventDetail = value; }
    73. }
    Message service calls
    Code (CSharp):
    1.     [ServerRpc(Delivery = RpcDelivery.Reliable, RequireOwnership = false)]
    2.     public void SendMessageServerRpc(BaseMessage baseMessage)
    3.     {
    4.         Debug.Log("MessageService SendMessageServerRpc:" + baseMessage.GetType());
    5.  
    6.         switch (baseMessage.DerivedType)
    7.         {
    8.             case DerivedType.Portal:
    9.                 Debug.Log((PortalMessage)baseMessage.DerivedMessage);
    10.                 break;
    11.             case DerivedType.Game:
    12.                 Debug.Log((GameMessage)baseMessage.DerivedMessage);
    13.                 break;
    14.             default:
    15.                 break;
    16.         }
    17.     }
    18.  
    19.     [ClientRpc(Delivery = RpcDelivery.Reliable)]
    20.     public void SendMessageClientRpc(BaseMessage baseMessage)
    21.     {
    22.         Debug.Log("MessageService SendMessageClientRpc:" + baseMessage.GetType());
    23.  
    24.         switch (baseMessage.DerivedType)
    25.         {
    26.             case DerivedType.Portal:
    27.                 Debug.Log((PortalMessage)baseMessage.DerivedMessage);
    28.                 break;
    29.             case DerivedType.Game:
    30.                 Debug.Log((GameMessage)baseMessage.DerivedMessage);
    31.                 break;
    32.             default:
    33.                 break;
    34.         }
    35.     }
    Calls to message service:
    Code (CSharp):
    1.        if (NetworkManager.Singleton.IsServer)
    2.         {
    3.             PortalMessage portalMessage = new PortalMessage();
    4.             portalMessage.MessageType = PortalMessageType.Construct;
    5.             portalMessage.EventIndex = 0;
    6.             portalMessage.EventDetail = new SceneEventDetail(101, 55);
    7.  
    8.             messageService.SendMessageClientRpc(portalMessage);
    9.         }
    10.         else
    11.         {
    12.             GameMessage gameMessage = new GameMessage();
    13.             gameMessage.MessageType = GameMessageType.Notification;
    14.             gameMessage.EventIndex = 1;
    15.             gameMessage.EventDetail = new NotificationEventDetail(7, "Prefix", "Some game notification");
    16.             messageService.SendMessageServerRpc(gameMessage);
    17.         }
    It's borderline too convoluted for my liking, I could stick with specifc object rpc calls or use a container class to hold different message types, but as I've not touched the rpc code in ages I wanted to check if there's a better solution for creating generic rpc calls?
     
    CodeSmile likes this.
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,001
    That looks fine by me. I'm used to seeing large switch statements that act on a message ID of some sort to decide what method to call. In your case the main refactoring I would try is to not have any netcode in the RPC methods besides the switch that calls other methods, so it will seem to be less convoluted when you only need to look at or change what a specific RPC message does.

    Code (CSharp):
    1. else if(serializer.IsReader)
    A simple else suffices. If it's not a writer, it's not going to be a comfabulator or something. It'll be a reader. ;)
     
    cerestorm likes this.
  3. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    Thanks CodeSmile, I wanted to be sure there's no better alternative, I do have longer switch statements in the messages themselves. I agree regarding the rpc code, it'll delegate to a controller class with the correct message type.

    I did add that at the end for a bit of unnecessary clarity, although if they're going to use two flags I'm quite happy to check both. :)