Client to execute a method on another client

I am currently delving into netcode and I want to allow a client to execute a method on another client with a vector3 input. How would I be able to do this? I have heard of client rpcs, is that what I’m looking for?

Or can I just do it directly, what I want to do is transfer velocity from one player to another when they collide and in a singleplayer game I can use OnCollisionEnter, but how do I do that with multiplayer? Trying to execute the method it wont let me, presumably because the client wasn’t the owner of the other client’s player I was executing code in.

Help is much appreciated :slight_smile:

Hi @GradientOGames !

Netcode for GameObjects is server authoritative, so your clients don’t talk to each other directly. To achive what you want you need to send a ServerRPC, and then a targeted ClientRPC.

The RPC documentation contains examples about this.

Does this help?

It really does help! Although only partially, the documentation doesn’t describe where I would put the code.
When I write the serverrpc code on the client script, will it just execute on the server for all players, e.g. If I call a server rpc can I just spawn a network prefab somewhere or is there other things I need to add? And when do the clientrpc do I just write the code on the same script or is there a specific place I write the server rpc?

Would it be something like this?

(written in psuedoish-code)

public class PlayerMovement_FPS : NetworkBehaviour
{
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            GetID = //how would I get playerid from collision?
          
            SetVelocityServerRpc(Velocity, GetID)
        }
    }

    [ServerRpc]
    void SetVelocityServerRpc(Vector3 Velocity, PlayerID ID)
    {
        if (!IsServer) return;

        ClientRpcParams clientRpcParams = new ClientRpcParams
        {
            Send = new ClientRpcSendParams
            {
                TargetClientIds = new ulong[] { ID }
            }
        };

        SetVelocityClientRpc(Velocity, clientRpcParams);
    }

    [ClientRpc]
    void SetVelocityClientRpc(Vector3 Velocity, ClientRpcParams clientRpcParams)
    {
        if (IsOwner) return;

        rb.velocity = Velocity;
    }
}

If you couldn’t bother reading my above paragraph here I my follow-up questions:
-Would my code above work?
-How would I get the playerID from a collision?

I gleefully appreciate your help and await your reply :smile:

I think, in your clientRpc this:

if (IsOwner) return;

should be:

if (!IsOwner) return;

or the hole line can be removed…

I got sidetracked making a nametag system and I got it working flawlessly, now back to this. This is my code:

void OnCollisionEnter(Collision collision)
    {
        if (!IsOwner) return;

        Debug.Log("Collided");

        Vector3 V = Fields.Rigidbody.velocity;

        if (collision.gameObject.CompareTag("Player"))
        {
            Debug.Log("Collided with player");

            ulong ID = collision.gameObject.GetComponent<NetworkObject>().NetworkObjectId;

            SetVelocityServerRpc(V, ID);
        }
    }

    [ServerRpc]
    void SetVelocityServerRpc(Vector3 Velocity, ulong ID)
    {
        Debug.Log("send message to server");

        ClientRpcParams clientRpcParams = new ClientRpcParams
        {
            Send = new ClientRpcSendParams
            {
                TargetClientIds = new ulong[] { ID }
            }
        };

        SetVelocityClientRpc(Velocity, clientRpcParams);
    }

    [ClientRpc]
    void SetVelocityClientRpc(Vector3 Velocity, ClientRpcParams clientRpcParams)
    {
        Debug.Log("send message to client with velocity: " + Velocity);

        Fields.Rigidbody.velocity = Velocity;
    }

Doing that made no difference, at the moment the serverrpc is being sent succesfully but executing the clientrpc throws this warning: Trying to send ClientRpcMessage to client 2 which is not in a connected state.

I think it is something to do with the serverrpc not creating correct clientrpcparameters. instead of using clientrpcparams, would it be feasible to just use network object ids instead? (if that is the issue of course)

Thanks! :slight_smile:

Try replacing NetworkObjectId with OwnerClientId.

Thanks for the reply! I figured that one out myself a few hours ago, it now works successfully on both client (supposed to be one of them but ez fix) and successfully puts the velocity value in the console! But… now it throws a NullReferenceException (NRE) and I have never encountered anything like it in the past. It appears to be a multiplayer thing but it is having trouble setting the velocity from what I can tell of the stack trace:

(first three lines)
NullReferenceException
UnityEngine.Rigidbody.set_velocity (UnityEngine.Vector3 value) (at <49ef468a114d4b5bb0d197c2b98f1cc4>:0)
PlayerMovement_FPS.SetVelocityClientRpc (UnityEngine.Vector3 Velocity, Unity.Netcode.ClientRpcParams clientRpcParams) (at Assets/Project/Runtime/Main/Scripts/PlayerMovement_FPS.cs:153)
PlayerMovement_FPS.SetVelocityServerRpc (UnityEngine.Vector3 Velocity, System.UInt64 ID) (at Assets/Project/Runtime/Main/Scripts/PlayerMovement_FPS.cs:145)

it’s probably something incredibly stupid and I’m tryna to figure it out but I don’t have any ideas atm. any help? Thanks! :smile:

edit: is there anyway to embed a stack trace on the forums to make it easier to read?

As you can see from the stacktrace, the error is in your PlayerMovement_FPS script at line 153, so if you look there you’ll figure out what the null reference is.

The code tag

Oops, forgot to add the line of code it was asking for,

the line of code is Fields.Rigidbody.velocity = Velocity;

and it is inside here

[ClientRpc]
    void SetVelocityClientRpc(Vector3 Velocity, ClientRpcParams clientRpcParams)
    {
        Debug.Log("send message to client with velocity: " + Velocity);

        Fields.Rigidbody.velocity = Velocity;
    }

the problem I’m facing is that I have no idea what is wrong with Fields.Rigidbody.velocity = Velocity; it is perfectly fine in all my use cases, perhaps it doesn’t work in a clientrpc?

Thanks :smile:

Either Fields or its rigidbody is null on client, you can check which one is null by printing it with a Debug.Log. Then be sure to assign it somewhere in the rest of your code. It’s not a netcode issue anymore :smile:

Update: I’ve been working on this on my own on and off for a bit and I’m still confounded by a few weird things that I’ve been trying to solve but there’s two parts that just don’t work and I have no idea why.

  1. clients can’t set their own velocity even though it works in every other script (now I write this I realise it might just be because the velocity is being overwritten by fixedupdate so I will have to check later)
  2. using ClientRpcParams prevents all clients from recieving the rpc; I take the target client’s OwnerClientId which is passed to the server rpc which converts that into ClientRpcParams however it doesn’t find any client which is very odd.

(I’m gonna put the entire script here, the whole velocity stuff starts on line 141)

using System;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using TMPro;

/*============================ ======================Script==================================================*/


[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement_FPS : NetworkBehaviour
{
    [Header("Rigidbody Movement \n")]
    [Tooltip("The variables that should be set manually.")] public PMFPSParameters Parameters;
    [Tooltip("All variables that doesn't require manual intervention.")] public PMFPSFields Fields;



    //setup
    public override void OnNetworkSpawn()
    {
        if (!IsOwner) { return; } else { GetComponent<Rigidbody>().collisionDetectionMode = CollisionDetectionMode.Continuous; }

        Camera.main.transform.SetParent(transform);
        Camera.main.transform.localPosition = Parameters.CameraPosition;
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        Fields.CursorLock = true;

        Destroy(transform.Find("Eye").GetComponent<MeshRenderer>());
        transform.Find("Nametag").GetComponent<TextMeshPro>().fontSize = 0;
        Destroy(transform.Find("nametag background").GetComponent<MeshRenderer>());

        Fields.NetworkObject = GetComponent<NetworkObject>();

        Fields.Rigidbody = GetComponent<Rigidbody>();
        Fields.GroundPoint = new GameObject("GroundPoint").transform;
        Fields.GroundPoint.parent = transform;
        Fields.GroundPoint.localPosition = new Vector3(0, Parameters.BottomDistance, 0);

        SetGameObjectNameServerRpc(gameObject, GameObject.Find("UserName").GetComponent<TextMeshProUGUI>().text.Replace("Username: ", ""));
    }



    //inputs and camera
    private void Update()
    {
        if (!IsOwner) return;

        if (Input.GetKeyDown(KeyCode.Escape)) { Fields.CursorLock = false; }
        else if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) { Fields.CursorLock = true; }

        if (Fields.CursorLock)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;

            Fields.MouseXY = new Vector2(Input.GetAxis("Mouse X") * Parameters.MouseSensitivity, Fields.MouseXY.y - Input.GetAxis("Mouse Y") * Parameters.MouseSensitivity);
            Fields.MouseXY.y = Mathf.Clamp(Fields.MouseXY.y, -90f, 90f);

            Camera.main.transform.localRotation = Quaternion.Euler(Camera.main.transform.localRotation.x + Fields.MouseXY.y, 0, 0);
            transform.rotation *= Quaternion.Euler(0, Fields.MouseXY.x, 0);
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }

        Fields.XYInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
        if (Fields.XYInput.magnitude > 1) Fields.XYInput.Normalize();
        Fields.XYOutput = (Fields.XYInput * Parameters.MoveSpeed);

        Fields.JumpInput = Input.GetButton("Jump");
        Fields.JumpOutput = (Convert.ToInt32(Fields.JumpInput) * Parameters.JumpHeight);     
    }



    //physics
    void FixedUpdate()
    {
        if (!IsOwner) return;

        Fields.OnGround_Walk = Physics.CheckSphere(Fields.GroundPoint.position, Parameters.MoveDistance, Parameters.GroundLayer);
        Fields.OnGround_Jump = Physics.CheckSphere(Fields.GroundPoint.position, Parameters.JumpDistance, Parameters.GroundLayer);

        Vector3 move = transform.forward * Fields.XYOutput.y + transform.right * Fields.XYOutput.x;

        if (Fields.OnGround_Walk && Parameters.MovementType == PMFPSMovementType.realistic) Fields.Rigidbody.velocity = new Vector3(move.x, Fields.Rigidbody.velocity.y, move.z);
        else if (Parameters.MovementType == PMFPSMovementType.forgiving)
        {
            if (Fields.OnGround_Walk)
            {
                if (Mathf.Abs(Fields.Rigidbody.velocity.x) < Parameters.MoveSpeed && move.x != 0) Fields.Rigidbody.velocity = new Vector3(move.x, Fields.Rigidbody.velocity.y, Fields.Rigidbody.velocity.z);
                if (Mathf.Abs(Fields.Rigidbody.velocity.z) < Parameters.MoveSpeed && move.z != 0) Fields.Rigidbody.velocity = new Vector3(Fields.Rigidbody.velocity.x, Fields.Rigidbody.velocity.y, move.z);
            }
            else Fields.Rigidbody.AddForce(move.x, 0f, move.z);
        }
        else if (Parameters.MovementType == PMFPSMovementType.normal) Fields.Rigidbody.velocity = new Vector3(move.x, Fields.Rigidbody.velocity.y, move.z);

        if (Fields.JumpInput && Fields.OnGround_Jump) Fields.Rigidbody.velocity = new Vector3(Fields.Rigidbody.velocity.x, Fields.Rigidbody.velocity.y + Fields.JumpOutput, Fields.Rigidbody.velocity.z);
    }
 


    //set a client's username
    [ServerRpc]
    public void SetGameObjectNameServerRpc(NetworkObjectReference Player, string Name)
    {
        ((GameObject)Player).name = Name;
        DebugLog("Setting player name in server hierarchy");

        GameObject[] ServerNameObjects = GameObject.FindGameObjectsWithTag("Player");
        string[] ServerNames = new string[ServerNameObjects.Length];
        for(int i = 0; i < ServerNames.Length; i++)
        {
            ServerNames[i] = ServerNameObjects[i].name;
        }
        SyncNamesClientRpc(new NetworkStringArrayStruct { Array = ServerNames });
    }
    //sync all current usernames with all clients
    [ClientRpc]
    public void SyncNamesClientRpc(NetworkStringArrayStruct ServerNames)
    {
        DebugLog("Syncing Client Names");
        GameObject[] NameTags = GameObject.FindGameObjectsWithTag("Nametag");
        int i = 0;
       
        foreach (GameObject Tag in NameTags)
        {           
            Tag.GetComponent<PlayerNameTag>().Name = ServerNames.Array[i];
            DebugLog("Sync Client Name: " + ServerNames.Array[i]);
            i++;
        }
    }



    //send collision velocity to player that was collided with
    void OnCollisionEnter(Collision collision)
    {
        if (!IsOwner) return;

        Vector3 V = Fields.Rigidbody.velocity;

        if (collision.gameObject.CompareTag("Player"))
        {
            DebugLog("Collided with player with velocity: " + V);

            ulong ID = collision.gameObject.GetComponent<NetworkObject>().OwnerClientId;

            SetVelocityServerRpc(V, ID);
        }
    }
    //send velocity to server to then send to client
    [ServerRpc]
    void SetVelocityServerRpc(Vector3 Velocity, ulong ID)
    {
        DebugLog("send collision message to server");

        ClientRpcParams clientRpcParams = new ClientRpcParams
        {
            Send = new ClientRpcSendParams
            {
                TargetClientIds = new ulong[] { ID }
            }
        };

        SetVelocityClientRpc(Velocity, clientRpcParams);
    }
    //Get velocity to set from server
    [ClientRpc]
    void SetVelocityClientRpc(Vector3 Velocity, ClientRpcParams clientRpcParams = default)
    {
        DebugLog("send message to client with velocity: " + Velocity);

        SetVelocity(Velocity);
    }
    //set velocity based on input
    void SetVelocity(Vector3 Velocity)
    {
        if (!IsOwner) { DebugLog("Not owner and cannot do SetVelocity()"); return; }

        DebugLog("Velocity to send" + Velocity);
        DebugLog(GetComponent<PlayerNameTag>().Name + " " + Fields.Rigidbody);

        Fields.Rigidbody.velocity += Velocity;
    }



    //Debug
    void DebugLog(object message)
    {
        if (!IsOwner) return;

        DebugLogServerRpc(message.ToString());
    }
    [ServerRpc]
    void DebugLogServerRpc(string message)
    {
        DebugLogClientRpc(message);
    }
    [ClientRpc]
    void DebugLogClientRpc(string message)
    {
        Debug.Log(message);
    }
}



//serialize player names array to use in RPC
public struct NetworkStringArrayStruct : INetworkSerializable
{
    public string[] Array;

    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        var length = 0;
        if (!serializer.IsReader)
            length = Array.Length;

        serializer.SerializeValue(ref length);

        if (serializer.IsReader)
            Array = new string[length];

        for (var n = 0; n < length; ++n)
            serializer.SerializeValue(ref Array[n]);
    }
}



/*==================================================CustomClasses===========================================*/



[System.Serializable]
public class PMFPSParameters
{
    [Header("Movement")]
    [Tooltip("The speed of walking without any multipliers or adders.")] public float MoveSpeed;
    [Tooltip("The distance the player has to be from the ground to walk.")] public float MoveDistance;

    [Header("Mouse")]
    [Tooltip("The sensitivity of the FPS mouse movement.")] public float MouseSensitivity;
    [Tooltip("Offset of the camera from the player.")] public Vector3 CameraPosition;

    [Header("Jumping")]
    [Tooltip("The height of jumping without any multipliers or adders.")] public float JumpHeight;
    [Tooltip("The distance the player has to be from the ground to jump.")] public float JumpDistance;   

    [Header("Types")]
    [Tooltip("The type of movement that will be used.")] public PMFPSMovementType MovementType;

    [Header("Miscellaneous")]
    [Tooltip("The lowest point of the player from the centre.")] public float BottomDistance;
    [Tooltip("The layer in which the player can jump on.")] public LayerMask GroundLayer;

    [Header("Debug")]
    [ReadOnly] [Tooltip("Broadcast all debug.log messages")] public bool DebugMode;
}

public enum PMFPSMovementType
{
    realistic, //player has ZERO movement control in midair.
    forgiving, //player has WEAK movement control in midair.
    normal     //player has FULL movement control in midair.
}

[System.Serializable]
public class PMFPSFields
{
    [Header("Networking")]
    [ReadOnly] [Tooltip("The network object of the player")] public NetworkObject NetworkObject;
   
    [Header("Objects")]
    [ReadOnly] [Tooltip("The Rigidbody used for the player to interact with physics.")] public Rigidbody Rigidbody;
    [ReadOnly] [Tooltip("Should be touching the ground when the player is on the ground.")] public Transform GroundPoint;

    [Header("Inputs")]
    [ReadOnly] [Tooltip("The horizontal and vertical input.")] public Vector2 XYInput;
    [ReadOnly] [Tooltip("The jump input.")] public bool JumpInput;

    [Header("Camera")]
    [ReadOnly] [Tooltip("The X and Y movement of the mouse.")] public Vector2 MouseXY;
    [ReadOnly] [Tooltip("If mouse is being used in game.")] public bool CursorLock;

    [Header("Movement")]
    [ReadOnly] [Tooltip("The horizontal and vertical movement final values.")] public Vector2 XYOutput;
    [ReadOnly] [Tooltip("The jump final output.")] public float JumpOutput;
    [ReadOnly] [Tooltip("If the player is in jump mode.")] public bool JumpCooldown;

    [Header("Conditions")]
    [ReadOnly] [Tooltip("Is the player on the ground or not? (to be able to walk)")] public bool OnGround_Walk;
    [ReadOnly] [Tooltip("Is the player on the ground or not? (to be able to jump)")] public bool OnGround_Jump;
}

Thanks for reading, and hopefully helping :wink: