Search Unity

Question How to implement RPC's with interface parameters.

Discussion in 'Netcode for GameObjects' started by cerestorm, Nov 13, 2021.

  1. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    I'm trying to send messages between server and client where the RPC parameter is an interface, allowing for the sending of different messages that implement the same interface.

    Here's a simplified example of the messages:

    Code (CSharp):
    1. public interface IMessage
    2. {
    3.  
    4. }
    5.  
    6. public struct FirstMessage : IMessage
    7. {
    8.     int myInt;
    9.     string myString;
    10. }
    11.  
    12. public struct SecondMessage : IMessage
    13. {
    14.     int myInt;
    15.     SomeClass myObject;
    16. }
    And the RPC call:

    Code (CSharp):
    1. [ServerRpc]
    2. public void SendMessageServerRpc(IMessage message)
    3. {
    4.     // process message
    5. }
    I had this working in Mirror with a bit of leg work, but I'm just encountering errors with Netcode using INetworkSerializable or creating extension methods for FastBufferReader.ReadValueSafe() and FastBufferWriter.WriteValueSafe(). Any hints or tips for getting this working would be most welcome.

    After further poking with the extension methods they always fall over with an error like this:

    InvalidProgramException: Invalid IL code in PlayerMessageService:SendInterfaceServerRpc (Message): IL_00dd: call 0x0600004d
    Any ideas as I'm stumped at this point.

    (Edit: typo's)
     
    Last edited: Nov 13, 2021
  2. herrmutig

    herrmutig

    Joined:
    Nov 30, 2020
    Posts:
    22
    I don't get why you don't use INetworkSerializable.
    As long as you don't send your message as Array, the following works just fine.

    1. Code (CSharp):
      1. public struct FirstMessage : INetworkSerializable
      2. {
      3.     int myInt;
      4.     int myString;
      5.     void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
      6.     {
      7.        serializer.SerializeValue(ref myInt);
      8.        serializer.SerializeValue(ref myString);
      9.     }
      10. }
    Also look here for examples of how to serialize:
    https://docs-multiplayer.unity3d.com/docs/advanced-topics/serialization/inetworkserializable
     
    Talkyn likes this.
  3. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    I'm away from my PC at the moment. I discounted INetworkSerializable as it expects a constructor on IMessage.

    Your example doesn't solve the problem of using an interface in the RPC call, unless I'm missing something?
     
  4. Talkyn

    Talkyn

    Joined:
    Apr 13, 2019
    Posts:
    8
    RPCs require serialization so one way or another what you pass in there has to be made serializable. The example above shows you exactly what you are trying to do, but implements an interface that specifically enforces serialization so it plays nice over a network. I suppose you could implement multiple interfaces, but that sounds like extra steps.

    You can do as @herrmutig suggested and just implement INetworkSerializable or you can read the docs he linked along with this: https://docs-multiplayer.unity3d.com/docs/advanced-topics/custom-serialization and roll your own.

    I don't see the constructor you refer to. IMessage is your example interface, so that must be a typo. In fact, I'm pretty sure interfaces can't define constructors.
     
  5. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    I've gone through the docs and I've already had what herrmutig suggested working fine but that's not the issue here. The key difference is that the RPC above expects a struct or class implementing the IMessage interface. The problem is I haven't found a way to tell how those structs/classes should be serialized.

    From my tinkering it looks like neither INetworkSerializable nor the extension methods are designed to work with interfaces. This is crucial for me in the way I pass messages between client and server. Mirror caters for this so I'm assuming Netcode will as well as it's a viable use case, but I've had no luck finding a solution so far.
     
  6. Talkyn

    Talkyn

    Joined:
    Apr 13, 2019
    Posts:
    8
    I'm not sure I understand what the issue is. Something is missing for me to help, I'm sure.

    INetworkSerializable is an interface. I think this is where I'm hung up, what is wrong with this interface for your purposes? If you just want to be able to group your messages or define a generic message, why not simply change your code to something like this below? Alternatively, I guess you could try implementing multiple interfaces and include your own along with INetworkSerializable, or just go ahead and copy a working NetworkSerialize method from it into your IMessage interface.

    The big caveat here being I haven't tested any of this.

    Code (CSharp):
    1. public struct FirstMessage : INetworkSerializable
    2. {
    3.     int myInt;
    4.     string myString;
    5.     void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    6.     {    
    7.     serializer.SerializeValue(ref myInt);     serializer.SerializeValue(ref myString);  
    8.     }
    9. }
    10.  
    11. public struct SecondMessage : INetworkSerializable
    12. {
    13.     int myInt;
    14.     SomeClass myObject;
    15.     void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    16.     {    
    17.     serializer.SerializeValue(ref myInt);     serializer.SerializeValue(ref myObject);  
    18.     }
    19.  
    20. [ServerRpc]
    21. public void SendMessageServerRpc(INetworkSerializable  message)
    22. {
    23.     // process message
    24. }
    25.  
    26. }
     
  7. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    It's a nice idea, I hadn't tried it, but now it wants to serialize INetworkSerializable:
    Assets/Scripts/Messaging/Services/PlayerMessageService.cs(11,9): error  - Don't know how to serialize INetworkSerializable - implement INetworkSerializable or add an extension method for FastBufferWriter.WriteValueSafe to define serialization.
    So back to square one. Maybe someone from Unity can set us straight, I wish they were more visible on the forums for their experimental stuff, maybe after the weekend. :)
     
    herrmutig likes this.
  8. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    I did some more digging today and it looks like there's an issue with FastBufferWriter/Reader as described here https://issueexplorer.com/issue/Unity-Technologies/com.unity.netcode.gameobjects/1355, 'ref' needs to be added to the extension method calls:
    Code (CSharp):
    1. public static void ReadValueSafe(this FastBufferReader reader, out IMessage iMessage) // broken
    2. public static void ReadValueSafe(this ref FastBufferReader reader, out IMessage iMessage) // working
    With that fixed I was able to get an interface working in the RPC call, although it's a bit of a hack. Example solution below.

    Code (CSharp):
    1. public class RequestMessage : IMessage
    2. {
    3.     private MessageType messageType;
    4.     private int number;
    5.  
    6.  
    7.     public RequestMessage(MessageType messageType, int number)
    8.     {
    9.         this.messageType = messageType;
    10.         this.number = number;
    11.     }
    12.  
    13.     public MessageType MessageType { get => messageType; set => messageType = value; }
    14.     public int Number { get => number; set => number = value; }
    15. }
    16.  
    17.  
    18. public enum MessageType
    19. {
    20.     Request,
    21.     Response
    22. }
    23.  
    24.  
    25. public interface IMessage
    26. {
    27.  
    28. }
    29.  
    30.  
    31. public static class IMessageSerializer
    32. {
    33.     public static void ReadValueSafe(this ref FastBufferReader reader, out IMessage iMessage)
    34.     {
    35.         reader.ReadValueSafe(out MessageType messageType);
    36.  
    37.         switch (messageType)
    38.         {
    39.             case MessageType.Request:
    40.                 reader.ReadValueSafe(out int number);
    41.                 iMessage = new RequestMessage(messageType, number);
    42.                 break;
    43.  
    44.             default:
    45.                 Debug.LogError("No match for message type");
    46.                 iMessage = null;
    47.                 break;
    48.         }
    49.     }
    50.  
    51.     public static void WriteValueSafe(this ref FastBufferWriter writer, in IMessage iMessage)
    52.     {
    53.         Debug.Log("Writer type: " + iMessage.GetType());
    54.         if (iMessage is RequestMessage message)
    55.         {
    56.             writer.WriteValueSafe(message.MessageType);
    57.             writer.WriteValueSafe(message.Number);
    58.         }
    59.     }
    60. }
    61.  
    62.  
    63. public void SendServerInterfaceMessage()
    64. {
    65.     IMessage message = new RequestMessage(MessageType.Request, 123);
    66.  
    67.     messageService.SendInterfaceServerRpc(message);
    68. }
    69.  
    70.  
    71. [ServerRpc]
    72. public void SendInterfaceServerRpc(IMessage iMessage)
    73. {
    74.     if (iMessage is RequestMessage message)
    75.     {
    76.         Debug.Log("Message Type: " + message.MessageType);
    77.         Debug.Log("message Number: " + message.Number);
    78.     }
    79. }
    80.  
    81.  
    For the serializer WriteValueSafe is fine as it can get the type of IMessage when it comes in. The problem is ReadValueSafe doesn't know the object type it's meant to create, so I've tacked on the field MessageType as a way of identifying it.

    Here's a more complex solution with an interface within a class, using INetworkSerializable and custom serialization methods.

    Code (CSharp):
    1. public class PlayerMessage : INetworkSerializable
    2. {
    3.     PlayerMessageHeader header = new PlayerMessageHeader();
    4.     MessageBodyType bodyType;
    5.     IMessageBody messageBody;
    6.  
    7.     public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    8.     {
    9.         header.NetworkSerialize(serializer);
    10.         serializer.SerializeValue(ref bodyType);
    11.  
    12.         if (serializer.IsWriter)
    13.         {
    14.             messageBody.Serialize(serializer.GetFastBufferWriter());
    15.         }
    16.         else if (serializer.IsReader)
    17.         {
    18.             FastBufferReader fastBufferReader = serializer.GetFastBufferReader();
    19.             Debug.Log("Reader len: " + fastBufferReader.Length + " Position: " + fastBufferReader.Position);
    20.  
    21.             switch (bodyType)
    22.             {
    23.                 case MessageBodyType.First:
    24.                     messageBody = new FirstMessageBody();
    25.                     messageBody.DeSerialize(fastBufferReader);
    26.                     break;
    27.             }
    28.         }
    29.     }
    30.  
    31.     public PlayerMessageHeader Header { get => header; set => header = value; }
    32.     public MessageBodyType BodyType { get => bodyType; set => bodyType = value; }
    33.     public IMessageBody MessageBody { get => messageBody; set => messageBody = value; }
    34. }
    35.  
    36.  
    37. public class PlayerMessageHeader : INetworkSerializable
    38. {
    39.     PlayerMessageType messageType;
    40.  
    41.     public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    42.     {
    43.         serializer.SerializeValue(ref messageType);
    44.     }
    45.  
    46.     public PlayerMessageType MessageType { get => messageType; set => messageType = value; }
    47. }
    48.  
    49.  
    50. public enum PlayerMessageType
    51. {
    52.     Request,
    53.     Response
    54. }
    55.  
    56.  
    57. public enum MessageBodyType
    58. {
    59.     First,
    60.     Second
    61. }
    62.  
    63.  
    64. public class FirstMessageBody : IMessageBody
    65. {
    66.     int id;
    67.     FixedString64Bytes message;
    68.  
    69.     public void Serialize(FastBufferWriter writer)
    70.     {
    71.         if (!writer.TryBeginWrite(sizeof(int)))
    72.         {
    73.             throw new OverflowException("Not enough space in the buffer");
    74.         }
    75.  
    76.         writer.WriteValue(id);
    77.  
    78.         if (!writer.TryBeginWriteValue<FixedString64Bytes>(message))
    79.         {
    80.             throw new OverflowException("Not enough space in the buffer");
    81.         }
    82.  
    83.         writer.WriteValue(message);
    84.     }
    85.  
    86.     public void DeSerialize(FastBufferReader reader)
    87.     {
    88.         reader.ReadValueSafe(out id);
    89.         reader.ReadValueSafe(out message);
    90.     }
    91.  
    92.     public int Id { get => id; set => id = value; }
    93.     public FixedString64Bytes Message { get => message; set => message = value; }
    94. }
    95.  
    96.  
    97. public interface IMessageBody
    98. {
    99.     public void Serialize(FastBufferWriter writer);
    100.     public void DeSerialize(FastBufferReader reader);
    101. }
    102.  
    103.  
    104. public void SendServerPlayerMessage()
    105. {
    106.     PlayerMessage playerMessage = new PlayerMessage();
    107.     playerMessage.Header.MessageType = PlayerMessageType.Request;
    108.  
    109.     FirstMessageBody firstMessageBody = new FirstMessageBody();
    110.     firstMessageBody.Id = 1;
    111.     firstMessageBody.Message = "message";
    112.  
    113.     playerMessage.MessageBody = firstMessageBody;
    114.  
    115.     messageService.SendPlayerMessageServerRpc(playerMessage);
    116. }
    117.  
    118. [ServerRpc]
    119. public void SendPlayerMessageServerRpc(PlayerMessage playerMessage)
    120. {
    121.     Debug.Log("MessageType: " + playerMessage.Header.MessageType);
    122.     Debug.Log("Body: " + playerMessage.MessageBody);
    123.  
    124.     if (playerMessage.MessageBody is FirstMessageBody messageBody)
    125.     {
    126.         Debug.Log("Body id: " + messageBody.Id);
    127.         Debug.Log("Body message: " + messageBody.Message);
    128.     }
    129. }
    Again this has the same problem, the FastBufferReader doesn't know the object type so an identifier MessageBodyType has to be provided.

    Any advice on how to get around this issue or create a cleaner solution let me know.
     
  9. TheCaveOfWonders

    TheCaveOfWonders

    Joined:
    Mar 2, 2014
    Posts:
    27
    Last edited: Nov 14, 2021
    cerestorm likes this.
  10. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    666
    Ah thanks for the heads up. I wonder if this will change in future.