Can someone clear up this misconception about ClientRpc and ClientRpcParams?

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.

  • The TargetClientIds object is null and a NRE is raised.
  • 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

        [ServerRpc]
        internal void SpawnPieceServerRpc(NetworkGuid guid, Vector3 position, ulong ownerId, VisibilityMode visibility)
        {
            Debug.Log("Spawning");
            if (m_PieceRegistry.TryGetPiece(guid.ToGuid(), out var pieceData))
            {
                var prefab = pieceData.isStatic ? m_StaticPiecePrefab : m_MovablePiecePrefab;
                PieceDataWrapper newPiece = Instantiate(prefab);
                newPiece.name = pieceData.GetType().Name;
                newPiece.Data = pieceData;
                newPiece.transform.position = position;
                if (visibility == VisibilityMode.LocalPlayerOnly)
                    newPiece.NetworkObject.CheckObjectVisibility += (clientId) => clientId == ownerId;
                newPiece.NetworkObject.Spawn();
                newPiece.NetworkObject.ChangeOwnership(ownerId);
                if (!pieceData.isStatic)
                {
                    m_ClientCallbacks.InjectPieceDependencyClientRpc(newPiece.NetworkObjectId, new ClientRpcParams
                    {
                        Send = new ClientRpcSendParams
                        {
                            TargetClientIds = new ulong[] { ownerId },
                        }
                    });
                }
                m_ClientCallbacks.SpawnAvatarClientRpc(newPiece.NetworkObject.NetworkObjectId, guid);
            }
            else
            {
                Debug.LogError("Unable to match GUID for piece");
            }
        }
        [ClientRpc(Delivery = RpcDelivery.Reliable)]
        internal void InjectPieceDependencyClientRpc(ulong pieceId, ClientRpcParams clientRpcParams = default)
        {
            Debug.Log(clientRpcParams.Send.TargetClientIds);
            if (IsLocalPlayer && clientRpcParams.Send.TargetClientIds[0] == OwnerClientId)
                m_Pieces.AddPiece(
                    NetworkManager.SpawnManager.SpawnedObjects[pieceId].GetComponent<PieceDataWrapper>()
                );
        }

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.

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.

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:

    [DisallowMultipleComponent]
    [RequireComponent(typeof(ClientCallbacks))]
    public class ServerCallbacks : NetworkBehaviour

        public override void OnNetworkSpawn()
        {
            enabled = IsOwner;
            NetworkManager.SceneManager.OnLoadEventCompleted += OnGameLoaded;
            NetworkManager.SceneManager.OnSynchronizeComplete += OnSyncComplete;
        }

        private void OnSyncComplete(ulong clientId)
        {
            Debug.Log("Synced");
            SpawnInitialSetupServerRpc();
        }

        private void OnGameLoaded(string sceneName, LoadSceneMode loadSceneMode, List<ulong> clientsCompleted, List<ulong> clientsTimedOut)
        {
            Debug.Log("Started");
            SpawnInitialSetupServerRpc();
        }

        public override void OnNetworkDespawn()
        {
            NetworkManager.SceneManager.OnLoadEventCompleted -= OnGameLoaded;
        }

        [ServerRpc]
        internal void SpawnInitialSetupServerRpc()
        {
            foreach (PieceData piece in GetComponent<PlayerCharacterWrapper>().GetInitialSetup())
            {
                var position = transform.position;
                SpawnPieceServerRpc(piece.Guid.ToNetworkGuid(), position, OwnerClientId, VisibilityMode.All);
            }
        }

        [ServerRpc]
        internal void SpawnPieceServerRpc(NetworkGuid guid, Vector3 position, ulong ownerId, VisibilityMode visibility)
        {
            if (m_PieceRegistry.TryGetPiece(guid.ToGuid(), out var pieceData))
            {
                PieceDataWrapper newPiece = Instantiate(prefab);
                newPiece.NetworkObject.Spawn();
                newPiece.NetworkObject.ChangeOwnership(ownerId);
                m_ClientCallbacks.InjectPieceDependencyClientRpc(newPiece.NetworkObjectId, new ClientRpcParams
                 {
                    Send = new ClientRpcSendParams
                    {
                        TargetClientIds = new ulong[] { ownerId },
                    }
                });
            }
        }
    }

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

This is wrong.

See https://docs-multiplayer.unity3d.com/netcode/current/api/Unity.Netcode.ClientRpcReceiveParams
and https://docs-multiplayer.unity3d.com/netcode/current/api/Unity.Netcode.ClientRpcSendParams

Also I’m already getting the data out of the Send params, just not on the first try for some reason

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.

I have the same trouble. Is there any solution?

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.