Search Unity

Question Can someone clear up this misconception about ClientRpc and ClientRpcParams?

Discussion in 'Netcode for GameObjects' started by mattseaton22, Nov 11, 2022.

  1. mattseaton22

    mattseaton22

    Joined:
    Sep 12, 2019
    Posts:
    43
    I am testing my game in host mode as the lone client.

    I call a server RPC to spawn an object. I have verified that this function is only called once.

    Inside the server RPC I call a client RPC to inject dependencies for the player.

    I want to send this call only to the client whose player is in control of the object, so I use ClientRpcParams.

    Inside the client RPC function I get the Id of this client with clientRpcParams.Send.TargetClientIds[0]

    There are 2 behaviors I don't understand.

    1. The TargetClientIds object is null and a NRE is raised.
    2. The function is called again and TargetClientIds is no longer null.
    Can someone please explain? Does the NRE cause the RPC to trigger a 2nd time? Is it related to RpcDeliveryMode.Reliable? Why is the value null the first time? Why isn't it null the 2nd time?

    Here is my code
    Code (CSharp):
    1.         [ServerRpc]
    2.         internal void SpawnPieceServerRpc(NetworkGuid guid, Vector3 position, ulong ownerId, VisibilityMode visibility)
    3.         {
    4.             Debug.Log("Spawning");
    5.             if (m_PieceRegistry.TryGetPiece(guid.ToGuid(), out var pieceData))
    6.             {
    7.                 var prefab = pieceData.isStatic ? m_StaticPiecePrefab : m_MovablePiecePrefab;
    8.                 PieceDataWrapper newPiece = Instantiate(prefab);
    9.                 newPiece.name = pieceData.GetType().Name;
    10.                 newPiece.Data = pieceData;
    11.                 newPiece.transform.position = position;
    12.                 if (visibility == VisibilityMode.LocalPlayerOnly)
    13.                     newPiece.NetworkObject.CheckObjectVisibility += (clientId) => clientId == ownerId;
    14.                 newPiece.NetworkObject.Spawn();
    15.                 newPiece.NetworkObject.ChangeOwnership(ownerId);
    16.                 if (!pieceData.isStatic)
    17.                 {
    18.                     m_ClientCallbacks.InjectPieceDependencyClientRpc(newPiece.NetworkObjectId, new ClientRpcParams
    19.                     {
    20.                         Send = new ClientRpcSendParams
    21.                         {
    22.                             TargetClientIds = new ulong[] { ownerId },
    23.                         }
    24.                     });
    25.                 }
    26.                 m_ClientCallbacks.SpawnAvatarClientRpc(newPiece.NetworkObject.NetworkObjectId, guid);
    27.             }
    28.             else
    29.             {
    30.                 Debug.LogError("Unable to match GUID for piece");
    31.             }
    32.         }
    Code (CSharp):
    1.         [ClientRpc(Delivery = RpcDelivery.Reliable)]
    2.         internal void InjectPieceDependencyClientRpc(ulong pieceId, ClientRpcParams clientRpcParams = default)
    3.         {
    4.             Debug.Log(clientRpcParams.Send.TargetClientIds);
    5.             if (IsLocalPlayer && clientRpcParams.Send.TargetClientIds[0] == OwnerClientId)
    6.                 m_Pieces.AddPiece(
    7.                     NetworkManager.SpawnManager.SpawnedObjects[pieceId].GetComponent<PieceDataWrapper>()
    8.                 );
    9.         }
    10.  
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,953
    Reliable is the default, you can omit that. It has nothing to do with the observed issue in any case.

    Are both RPC methods in a class that inherits from NetworkBehaviour, and these scripts are on a game object that is spawned ans has a NetworkObject?

    When do you call the ServerRpc? It should be called in or after OnNetworkSpawn.

    Maybe check the RPC doc pages again because there are hints at the order of events and timing that are crucial to internalize.
     
  3. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    664
    There's Send and Receive parts of ClientRpcParams, the Send part is only set on the server and it would be the Receive part you check on the client, although it's currently not used. You shouldn't need any additional checks inside the ClientRpc as if it was called it was meant for that client.

    It does look like the rpc being called twice is a side effect of the exception on the host as it's only called once otherwise.
     
  4. mattseaton22

    mattseaton22

    Joined:
    Sep 12, 2019
    Posts:
    43
    Yes. There are 2 scripts, both on the player prefab, one for the server rpcs and one for client rpcs.

    Here is the relevant part of the server script:
    Code (CSharp):
    1.  
    2.     [DisallowMultipleComponent]
    3.     [RequireComponent(typeof(ClientCallbacks))]
    4.     public class ServerCallbacks : NetworkBehaviour
    5.  
    6.         public override void OnNetworkSpawn()
    7.         {
    8.             enabled = IsOwner;
    9.             NetworkManager.SceneManager.OnLoadEventCompleted += OnGameLoaded;
    10.             NetworkManager.SceneManager.OnSynchronizeComplete += OnSyncComplete;
    11.         }
    12.  
    13.         private void OnSyncComplete(ulong clientId)
    14.         {
    15.             Debug.Log("Synced");
    16.             SpawnInitialSetupServerRpc();
    17.         }
    18.  
    19.         private void OnGameLoaded(string sceneName, LoadSceneMode loadSceneMode, List<ulong> clientsCompleted, List<ulong> clientsTimedOut)
    20.         {
    21.             Debug.Log("Started");
    22.             SpawnInitialSetupServerRpc();
    23.         }
    24.  
    25.         public override void OnNetworkDespawn()
    26.         {
    27.             NetworkManager.SceneManager.OnLoadEventCompleted -= OnGameLoaded;
    28.         }
    29.  
    30.         [ServerRpc]
    31.         internal void SpawnInitialSetupServerRpc()
    32.         {
    33.             foreach (PieceData piece in GetComponent<PlayerCharacterWrapper>().GetInitialSetup())
    34.             {
    35.                 var position = transform.position;
    36.                 SpawnPieceServerRpc(piece.Guid.ToNetworkGuid(), position, OwnerClientId, VisibilityMode.All);
    37.             }
    38.         }
    39.  
    40.         [ServerRpc]
    41.         internal void SpawnPieceServerRpc(NetworkGuid guid, Vector3 position, ulong ownerId, VisibilityMode visibility)
    42.         {
    43.             if (m_PieceRegistry.TryGetPiece(guid.ToGuid(), out var pieceData))
    44.             {
    45.                 PieceDataWrapper newPiece = Instantiate(prefab);
    46.                 newPiece.NetworkObject.Spawn();
    47.                 newPiece.NetworkObject.ChangeOwnership(ownerId);
    48.                 m_ClientCallbacks.InjectPieceDependencyClientRpc(newPiece.NetworkObjectId, new ClientRpcParams
    49.                  {
    50.                     Send = new ClientRpcSendParams
    51.                     {
    52.                         TargetClientIds = new ulong[] { ownerId },
    53.                     }
    54.                 });
    55.             }
    56.         }
    57.     }
    58.  
    59.  
    The logic is very simple and straight forward.

    Spawn -> set scene loaded callback -> Scene loaded -> spawn some initial pieces -> send message to target client to inject dependencies

    I just don't see how it's possible the Send value could be null. The client rpc is only called from this code here
     
    Last edited: Nov 13, 2022
  5. mattseaton22

    mattseaton22

    Joined:
    Sep 12, 2019
    Posts:
    43
    Last edited: Nov 13, 2022
  6. mattseaton22

    mattseaton22

    Joined:
    Sep 12, 2019
    Posts:
    43
    In theory yes. I haven't tested to see that it is not being called on other clients. But it shouldn't cause any issues either I would think. I'm just trying to understand this unexpected behavior.
     
    Last edited: Nov 13, 2022
  7. Karabin

    Karabin

    Joined:
    Apr 26, 2015
    Posts:
    4
    I have the same trouble. Is there any solution?
     
  8. mattseaton22

    mattseaton22

    Joined:
    Sep 12, 2019
    Posts:
    43
    I just got rid of my if statement checking the clientId and trusted the clientrpsparams to do its job. It worked fine. Never figured out my I was getting null.

    I've since moved on to Fishnet which is a much more feature complete solution, and they have a discord which is sometimes helpful. Although there are some aspects of NGO I miss, the addition of client side prediction, rollback, lag compensation, network lod, etc. all built in make it a no brainer. It's also much more scalable than NGO out of the box.
     
    Last edited: Mar 22, 2023