Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question NetworkHide for host-mode

Discussion in 'Netcode for GameObjects' started by AgaDeoN, Nov 24, 2023.

  1. AgaDeoN


    May 19, 2020
    Hello everyone!

    Let's imagine the situation:
    - We have a NetworkObject on the scene which periodically calls some ClientRpc with any kind of data.
    - This object is configured with "Spawn With Observers" unchecked.
    - Clients can ask the server to NetworkShow or NetworkHide this object for them, i.e. "subscribe" and "unsubscribe" from receiving the data passed via abovementioned ClientRpc.

    All works as expected except the one thing: if we run the game as a host, it's possible to NetworkShow this object - The ClientRpc starts to be called periodically - but it's impossible to NetworkHide this object due to VisibilityChangeException("Cannot hide an object from the server").

    I know that the desired result is totally achievable using checks like "IsHost/IsServer etc" or passing list of clientId_s to the RPC, but this requires writing some additional logic which is kind of inconvenient...

    Am I missing something?
    Last edited: Nov 24, 2023
  2. NoelStephens_Unity


    Unity Technologies

    Feb 12, 2022
    I could see where NetworkShow/NetworkHide could seem like the approach to take for this type of functionality.
    However, what I think you might be looking for is a custom named message.
    The nice thing about custom named messages is that you can place that logic in either a NetworkBehaviour or just a normal MonoBehaviour (i.e. does not require a NetworkObject to send/receive the messages). Of course, if you kept your current design to have an in-scene placed NetworkObject for the convenience of being able to add NetworkBehaviour components which provide the convenience of knowing when it is spawned and having access to NetworkManager without having to use the Singleton then it would look something like this:

    • Keep original NetworkObject, but re-enable "Spawn With Observers"
    • In an existing or new NetworkBehaviour component add the below pseudo code:
    Code (CSharp):
    1. public class NetworkEvent : NetworkBehaviour
    2. {
    3.     public class SomeKindOfData : INetworkSerializable
    4.     {
    5.         public int AnInt;
    6.         public long ALong;
    7.         // Etc..
    9.         public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    10.         {
    11.             serializer.SerializeValue(ref AnInt);
    12.             serializer.SerializeValue(ref ALong);
    13.         }
    14.     }
    16.     public delegate void NetworkEventDelegateHandler(SomeKindOfData someKindOfData);
    18.     private event NetworkEventDelegateHandler m_OnNetworkEvent;
    19.     public event NetworkEventDelegateHandler OnNetworkEvent
    20.     {
    21.         add
    22.         {
    23.             if (IsSpawned)
    24.             {
    25.                 if (m_OnNetworkEvent?.GetInvocationList().Length == 0)
    26.                 {
    27.                     UpdateRegistration(true);
    28.                 }
    29.                 m_OnNetworkEvent += value;
    30.             }
    31.         }
    32.         remove
    33.         {
    34.             m_OnNetworkEvent -= value;
    35.             if (m_OnNetworkEvent?.GetInvocationList().Length == 0)
    36.             {
    37.                 UpdateRegistration();
    38.             }
    39.         }
    40.     }
    42.     private void UpdateRegistration(bool register = false)
    43.     {
    44.         if (register)
    45.         {
    46.             NetworkManager.CustomMessagingManager.RegisterNamedMessageHandler($"NetworkEvent-{NetworkObject.NetworkObjectId}", NetworkEventMessageHandler);
    47.         }
    48.         else
    49.         {
    50.             NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler($"NetworkEvent-{NetworkObject.NetworkObjectId}");
    51.         }
    53.         if (!NetworkManager.ShutdownInProgress)
    54.         {
    55.             if (IsServer)
    56.             {
    57.                 NetworkEventRegistration(register, NetworkManager.LocalClientId);
    58.             }
    59.             else
    60.             {
    61.                 // Client's identifier is sent with the ServerRpc
    62.                 NetworkEventRegistrationServerRpc(register);
    63.             }              
    64.         }
    65.     }
    67.     private List<ulong> m_RegisteredClients = new List<ulong>();
    69.     private void NetworkEventRegistration(bool register, ulong clientId)
    70.     {
    71.         if (register && !m_RegisteredClients.Contains(clientId))
    72.         {
    73.             m_RegisteredClients.Add(clientId);
    74.         }
    75.         else
    76.         if (!register && m_RegisteredClients.Contains(clientId))
    77.         {
    78.             m_RegisteredClients.Remove(clientId);
    79.         }
    80.     }
    82.     /// <summary>
    83.     /// Used to notify the server/host that a remote client has subscribed to the NetworkEvent
    84.     /// </summary>
    85.     /// <param name="register">true = subscribe and false = unsubscribe</param>
    86.     [ServerRpc(RequireOwnership = false)]
    87.     private void NetworkEventRegistrationServerRpc(bool register, ServerRpcParams serverRpcParams = default)
    88.     {
    89.         NetworkEventRegistration(register, serverRpcParams.Receive.SenderClientId);
    90.     }
    92.     /// <summary>
    93.     /// If the client instance has subscribed at least once to the OnNetworkEvent, then this will be invoked
    94.     /// when the server sends out an event.
    95.     /// If a host has not subscribed to OnNetworkEvent
    96.     /// </summary>
    97.     /// <param name="senderClientId"></param>
    98.     /// <param name="messagePayload"></param>
    99.     private void NetworkEventMessageHandler(ulong senderClientId, FastBufferReader messagePayload)
    100.     {
    101.         var someKindOfData = new SomeKindOfData();
    102.         messagePayload.ReadNetworkSerializable(out someKindOfData);
    103.         m_OnNetworkEvent?.Invoke(someKindOfData);
    104.     }
    106.     /// <summary>
    107.     /// Sends out a NetworkEvent to any client subscribed to the event for the NetworkObject instance and NetworkBehaviour in question
    108.     /// </summary>
    109.     /// <param name="someKindOfData">data to send</param>
    110.     /// <returns>number of clients the event was sent to</returns>
    111.     public int SendNetworkEvent(SomeKindOfData someKindOfData)
    112.     {
    113.         if (!IsSpawned)
    114.         {
    115.             return 0;
    116.         }
    118.         var writer = new FastBufferWriter(512, Unity.Collections.Allocator.TempJob);
    119.         writer.WriteNetworkSerializable(someKindOfData);
    121.         foreach (var client in m_RegisteredClients)
    122.         {
    123.             // Handle invoking any subscriptions on the host side, but skip sending (not needed)
    124.             if (client == NetworkManager.ServerClientId)
    125.             {
    126.                 m_OnNetworkEvent?.Invoke(someKindOfData);
    127.                 continue;
    128.             }
    129.             // Note: Delivery method is up to you, I picked reliable but you might want to use NetworkDelivery.ReliableFragmentedSequenced if you are sending large chunks of data
    130.             // or if you are sending multiple events for the same instance and the order in which they are received matters you could also use
    131.             NetworkManager.CustomMessagingManager.SendNamedMessage($"NetworkObject-{NetworkObject.NetworkObjectId}", client, writer, NetworkDelivery.ReliableSequenced);
    132.         }
    134.         writer.Dispose();
    135.         return m_RegisteredClients.Count;
    136.     }
    138.     public override void OnNetworkDespawn()
    139.     {
    140.         // Clear events and unregister
    141.         m_OnNetworkEvent = null;
    142.         UpdateRegistration();
    143.         base.OnNetworkDespawn();
    144.     }
    145. }
    I didn't do a full testing of everything (it might just work) but it gives you the general idea of how you could have a relatively generalized "NetworkEvent" where clients (including the Host) can subscribe or unsubscribe...this also allows you to have multiple registrations for various local objects (i.e. you might want more than 1 thing to be notified depending upon the game state). Each named message is unique to the NetworkObjectId, so it works relative to the NetworkObject instance (i.e. you could use this with dynamically spawned NetworkObjects).

    Let me know if this helps you achieve the functionality you were looking for?
  3. AgaDeoN


    May 19, 2020
    Thank you for your reply!

    I understand that there are many ways to achieve the desired result. Taking into account the option you proposed, there are already at least four of them in my head. The question is more about the usability of API.

    It’s not entirely clear to me why it is technically possible not to add the host-client to the object’s observers initially (that is uncheck the "Spawn With Observers" option), but there is no way remove it form object's observers after it's once added. It would be great to have uniform behavior in the described scenario for both the dedicated server and the host.

    Anyway, thanks for your answer!
  4. NoelStephens_Unity


    Unity Technologies

    Feb 12, 2022
    Yeah... :(
    The concept of a host (server + client) has some areas that could definitely be improved upon and you hit the nail on the head with the observers issue. To answer your inquiry about why you can't remove all observers, since a Host really acts like both a server and a client (well... almost like a client...with some server specific caveats) the original design/implementation required the server to "always be an observer".

    Of course, there have been adjustments, since the original implementation, that do allow you to spawn a NetworkObject dynamically with no observers (including the Host) but if you have a scene loaded and then start the NetworkManager as a Host or Server any in-scene placed NetworkObjects will be added to the server's observed list.

    Really, the intended purpose for NetworkShow and NetworkHide was to provide a way to "cull out" NetworkObjects that might not be of interest (i.e. some form of game with teams and you want only certain NetworkObject instances to be visible to team members) and/or a way to implement a form of distance driven interest management (a "Network LOD") in order to help reduce bandwidth consumption for things that are so far away (or in several rooms away) that it was nothing of "interest" to one or more clients at a given point in time.

    Since the server is the authority of everything, there are "certain things" that really need to be always "observable" to a server (whether a host or just server).
    As an example:
    I mentioned any in-scene placed NetworkObjects already loaded in a scene prior to starting a NetworkManager as a Host (or server) will automatically add the server to the observer list. The reason behind this is that in-scene placed NetworkObject instances get tied to their (local) respective instance's scene handle. Without diving into all of the reasons behind that, the bottom line is you could have a scene with nothing but in-scene placed NetworkObjects and use it as a way to load several "groups" of AI or other "netcode" related objects...and you can load the same scene additively multiple times...which means when a late joining client joins there has to be a way to differentiate between both sets of the exact same in-scene placed NetworkObjects with the exact same GlobalObjectIdHash values... thus we associate them with the scene handle... but of course a server's scene handle might not be the same as any given client's scene handle... so there is a process (would take a bunch more explaining o_O) that NGO goes through to assure when a client is doing its initial synchronization that each local scene instance is linked to the appropriate server scene instance.
    Long story short... in order to be able to preserve the NetworkShow/NetworkHide status while also being able to do (all things just described above) the server (as NGO is today) needs to be an observer of, at a minimum, in-scene placed NetworkObjects as you can see here in the SceneEventData where it serializes in-scene placed objects for a late joining client.

    There are other places/scenarios where the server just "needs to be an observer"... and since the Observers list is a single list of identifiers...well let's just say we are slowly migrating towards being less bound by this but currently are bound to requiring the "server" to always be an observer of all spawned NetworkObjects. Since a Host shares the same identifier (i.e. NetworkManager.LocalClientId == NetworkManager.ServerClientId)...the "host-client" has to follow the same "observer requirements" as the server.

    This is why anyone using NetworkShow/NetworkHide for a purpose beyond the scope of its intended use I try to divert/point them to an alternate implementation using other features of NGO to accomplish their desired functionality.
  5. AgaDeoN


    May 19, 2020
    Thank you for such a detailed answer!

    Hopefully there will be more sources of information or discussions in the nearest future of NGO that go beyond the basics a little further =) The official documentation is great though, but there are always some pitfalls or little nuances encountered only in the working process )
    NoelStephens_Unity likes this.