Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How would you implement a custom attribute similar to [Command] or [ClientRPC]?

Discussion in 'Scripting' started by PhilSA, Aug 25, 2016.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    C# attributes are an area of programming that I've really never explored much. I don't yet fully grasp the higher-level stuff you can accomplish with them. So, for learning purposes, I was wondering how you would implement an attribute like UNET's [Command]?

    Basically, something that checks if we are the server or not, and sends a network message to call that method on the server instead of executing the method locally if we are just a client. And also without using reflection (except maybe at initialization), because that would be terrible for performance. Not looking for the specifics of it. Just an overview of how it would work.
     
    Last edited: Aug 25, 2016
    Artaani likes this.
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,181
    Reflection!

    What you're essentially looking for is "if this method has the attribute on it, call that method at some point". There's no way to answer the question "What methods has the attribute [Foo] on them" than through reflection. See the official docs.

    As you're aware, reflection is slow. The correct way to do something like [Command] is to figure out which methods have the attribute either on initialization, or the first time you need that information, and cache that information then.

    For [Command], I'm guessing that the object is examined through reflection, and if a [Command]-attribute is found on a method, the object and the MethodInfo for that method is added to a data structure somewhere. When the command is received, the MethodInfo will be invoked with the object as the target and the input arguments as it's arguments.

    The important thing to know about is the class Type (all objects have a GetType method), which again has methods like GetFields, GetMethods, and so on, which returns the Info classes (FieldInfo, MethodInfo). Those, in turn, have a GetCustomAttributes method, which outputs the attributes on that member.

    A MethodInfo that you get from type T can be used together with an object of the type T to invoke the method on that object. That is slower than actually invoking the method, but not much - the slow part is finding the method in the first place.

    Ask if you have any other questions!

    Whenever you find yourself wanting to use an attribute to solve something like [Command], ask yourself if you could do the same thing through an interface. Interfaces are faster, easier to write and read, and allows for type checking.
     
    Artaani and PhilSA like this.
  3. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    super helpful, thanks!
     
  4. Artaani

    Artaani

    Joined:
    Aug 5, 2012
    Posts:
    423
    Hello.
    I am also interesting in that.

    Did you managed to achieve some kind of working solution?
    Could you please share your knowledge about this question? It would be great.
     
  5. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    Artaani likes this.
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I've actually abandoned the [RPC] / [SyncVar] workflow entirely to favor a much more manual (and more powerful) approach instead. I've found that there are way too many interesting bandwidth and CPU optimizations that you can do with manual synchronization that are just impossible to achieve when you have a system that tries to simplify everything too much, like UNET's HLAPI or even UE4's built-in networking system

    Here's a quick glance at how my networking system is generally going to work:
    • There is a WorldState struct somewhere in the code, which contains the network-relevant state of the entire game presently (character positions, rotations, velocities, projectiles, items, times, etc, etc....)
    • Every system in the game is in charge of keeping that WorldState up to date
    • On every tick of the server, we serialize that WorldState struct manually to an array of bytes, and send it to clients
    • On clients, when receiving that WorldState, we deserialize it manually and apply the results
    As for RPCs, I just write them manually for now, but I'm investigating some code generation approaches. RPCs in HLAPI use Cecil to generate all the requiremed code directly into compiled code, but I don't think I'll need something that fancy

    Manual serialization/deserialization is crucial, because that's where you can make a lot of optimizations that are very specific to your game and that an automatic system could never do (or not do elegantly/efficiently, at least). Having one big message also saves a lot of bandwidth because you only have one message header to send instead of potentially 1000+. And download/upload limits will still remain the real limiting factor in the foreseeable future, so you wouldn't want your total world synchronization size to bust the limit of one fragmented message anyway.

    Besides, I am really enjoying how clear everything is when all synchronization code is explicit and in the same place, as opposed to being hidden under layers of systems and spread out across the whole project
     
  7. Artaani

    Artaani

    Joined:
    Aug 5, 2012
    Posts:
    423
    Thanks you for reply!

    Wow, interesting approach. Sounds very unusual, but seems like have a sense.

    However seems like RPC still very important. because we need to send single actions too.

    Also seems like such optimization may decrease flexibility of architecture.

    Yes, sounds complex.

    So you are using just a simple:

    Code (CSharp):
    1. if (msg == "DoSomething") {
    2.     DoSomething ()
    3. }
    4.  
    5. if (msg == "DoSomethingElse") {
    6.     DoSomethingElse ()
    7. }
    Right? Seems like it is the most simple approach. But the amount of lines of code may became huge.
     
  8. Artaani

    Artaani

    Joined:
    Aug 5, 2012
    Posts:
    423
  9. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Little late, but AFAIK, the attributes were used to generate extra-code so the reflection overhead does not actually apply at runtime.
     
    Artaani likes this.
  10. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Here's an example of a RPC/message in my system:

    Code (CSharp):
    1. public struct FireWeaponMsg
    2.     {
    3.         public int AtTick;
    4.         public int FiringPlayerId;
    5.         public Quaternion FiringRotation;
    6.         public int HitCharacterId;
    7.  
    8.         public void Serialize(SerializationBuffer buf)
    9.         {
    10.             buf.SeekZero();
    11.             buf.WriteInt((int)NetworkMessageID.LazerHit);
    12.  
    13.             buf.WriteInt(AtTick);
    14.             buf.WriteInt(FiringPlayerId);
    15.             buf.WriteQuaternion(FiringRotation);
    16.             buf.WriteInt(HitCharacterId);
    17.         }
    18.  
    19.         public void Deserialize(SerializationBuffer buf)
    20.         {
    21.             AtTick = buf.ReadInt();
    22.             FiringPlayerId = buf.ReadInt();
    23.             FiringRotation = buf.ReadQuaternion();
    24.             HitCharacterId = buf.ReadInt();
    25.         }
    26.     }
    when I want to send it, I set all the parameters of the FireWeaponMsg, call Serialize(), and then do a NetworkTransport.Send() of the array of bytes inside SerializationBuffer
     
    Artaani likes this.
  11. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    That's correct. HLAPI doesn't do any runtime reflection; it's only done at the Cecil code gen step (during compilation, basically)

    But the HLAPI has plenty of other problems. It doesn't give you enough control, and does GC allocs all over the place. Thankfully, Unity will be replacing it with something new entirely (according to their GDC talk and to what devs said on the forums)
     
    Artaani likes this.
  12. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That's exactly the reason why I started to implement my own custom networking some years ago. It's not completely finished but used to run really well in test environments. It also requires to do some manual and programmatic setup (only registration of the network serializables tho), no fancy attributes.
    It also works in pure .NET, i.e. it's not specific to Unity, that was another main reason for me to experiment with it. I hope I could do a major revision some day, as I've came up with new ideas and more optimizations.

    I haven't heard about another new system from Unity, it's the first time now. I'll check that out, thanks for the information.
     
    Artaani likes this.
  13. Artaani

    Artaani

    Joined:
    Aug 5, 2012
    Posts:
    423
    Wow! Great news! It would be great. Maybe they will realize and fix its mistakes.

    Thanks you for example of code. Looks complex. Seems like LLAPI requires years of work in order to make it usable. Definitely not an optimal solution. Will wait for the better solution from Unity.
    Also I am looking at TNet, maybe it will suit a needs.