Grenade Spawning Issues on Client-side

Hi,

I’m setting up a game using Netcode-for-GameObjects for the first time and I’m now running into an issue with throwing a grenade on the client-side.

Here is the code (I’ve included comments to explain how it works and where my issue arises)

// This is called by the animation event
        public void LoadGrenade()
        {
            if (IsOwner)
            {
                LoadGrenadeServerRpc();
            }
        }

        [ServerRpc(RequireOwnership = false)]
        public void LoadGrenadeServerRpc()
        {
            LoadGrenadeClientRpc();
        }


// I've set this up so the grenade is spawned and held locally while the player prepares to throw the Grenade
        [ClientRpc]
        public void LoadGrenadeClientRpc()
        {
            activeGrenade = Instantiate(currentGrenade, currentGrenadeTransform.position, currentGrenadeTransform.rotation);
// Transform follow is a basic script to align the transform to the hand without having to parent the Network Object
            activeGrenade.GetComponent<transformFollow>().AcquireTarget(currentGrenadeTransform);
            activeGrenade.GetComponent<transformFollow>().Follow();
            activeGrenade.GetComponent<Rigidbody>().isKinematic = true;
            activeGrenade.GetComponent<Rigidbody>().useGravity = false;

        }

// This is called by another animation event where the grenade should leave the players hand
        public void ThrowGrenade ()
        {
            if (IsOwner)
            {
                ThrowGrenadeServerRpc();
                
            }
        }

        [ServerRpc(RequireOwnership = false)]
        public void ThrowGrenadeServerRpc(ServerRpcParams serverRpcParams = default)
        {
            ulong senderID = serverRpcParams.Receive.SenderClientId;
            activeGrenade.SpawnWithOwnership(senderID, true);
            ThrowGrenadeClientRpc(senderID, activeGrenade.NetworkObjectId);
        }

        [ClientRpc]
        public void ThrowGrenadeClientRpc(ulong sendingClientID, ulong ID)
        {
// Removing the clients grenade
            if (!IsServer)
            {
                Destroy(activeGrenade);
            }
            else
            { 
            activeGrenade.GetComponent<transformFollow>().StopFollow();
            }

//  The client will use the grenade spawned by the host as the new prefab
            if ( NetworkManager.Singleton.LocalClientId == sendingClientID)
            {
                activeGrenade = NetworkManager.SpawnManager.SpawnedObjects[ID];
                StartCoroutine("ThrowGrenadeAction");
            }
        }

//This is where I'm running into issues. On the Host, the grenade is throwing with zero issues, 
but, the client will throw and either have an ideal throw to where the character is facing or 
it will go in an unintended direction from the hand. It appears to happen randomly. 
            public IEnumerator ThrowGrenadeAction ()
        {
            
            yield return new WaitForEndOfFrame();
            activeGrenade.transform.position = currentGrenadeTransform.position;
            activeGrenade.transform.rotation = currentGrenadeTransform.rotation;

            Rigidbody rigidBody = activeGrenade.GetComponent<Rigidbody>();
            rigidBody.useGravity = true;
            rigidBody.isKinematic = false;
            rigidBody.linearVelocity = new Vector3(0,0,0);
            Vector3 forceToAdd = cam.transform.forward * grenadeForce + transform.up * grenadeUpForce;
            rigidBody.AddForce(forceToAdd, ForceMode.Impulse);
            if (grenadeSpin)
            {
                rigidBody.AddTorque(Vector3.up * grenadeSpinRPM);
            }
            activeGrenade.GetComponent<SmokeGrenade>().Thrown = true;
            activeGrenade = null;
        }

I’ve tried troubleshooting with various different ways and this is the latest where I’m still running into the same issue. There is a character controller as a potential collider but the grenade prefab has a layer that ignores the Players.

(Background information, the game only features 2 players in a 1v1 manner)

And what would that issue be? You didn’t say.

Ah, I see, it’s embedded in the code.

I suppose the problem may be related to latency. Because the direction of the grenade depends on each client’s camera viewpoint, and thus the grenade will not be synchronized between clients. Unless it has a NetworkTransform et al which would make that code superfluous because only the grenade owner would actually be able to move it.

Please don’t write copypasta code like this:

It’s inefficient, hard to maintain, hard to read, prone to hiding any issues inside.

Use variables to avoid repeating code all over again:

            activeGrenade = Instantiate(currentGrenade, currentGrenadeTransform.position, currentGrenadeTransform.rotation);

            var follow = activeGrenade.GetComponent<transformFollow>()
            follow.AcquireTarget(currentGrenadeTransform);
            follow.Follow();

            var rb = activeGrenade.GetComponent<Rigidbody>();
            rb.isKinematic = true;
            rb.useGravity = false;

This will make both your brain and your CPU happier. :wink:

And yes, even for one or two lines it’s good practice to add the var because our human brains can scan vertically much better than horizontally (ie prefer more over fewer but longer lines).

1 Like

The grenade has a ClientNetworkTransform and a NetworkRigidbody.

Only the client who initiated throwing the grenade ends up running “public IEnumerator ThrowGrenadeAction” and launching the grenade RigidBody.

Thanks for the tip, I’m normally better at that but I also didn’t know it can improve performance.

Also, what’s the difference between using

var follow = activeGrenade.GetComponent<transformFollow>()

and

transformFollow follow = activeGrenade.GetComponent<transformFollow>()

just in case that’s another learning moment. I’m not familiar with ‘var’

The reason CodeSmile’s suggestion can improve performance is because calling GetComponent isn’t free. It takes a bit of time to search through the list of components and return the correct one. Caching the result in a local varible saves you from paying that cost more than once within that block of code.

As for the question about the difference between those two lines of code: there is no difference. Using the keyword ‘var’ is just a shorthand when the datatype can be inferred from the assignment. For example:

var x = 1.0f;

In the above case it can be inferred that x is a float because that is the value being assigned by default at it’s creation. Some argue against it, others love it. It’s basically just a shortcut to avoid havinh to type out really long classnames more than once like this:

MyClassTypeThatIsPrettyObvious x = new MyClasTypeThatIsPrettyObvious();

In that above case it’s pretty obvious what type the variable x will be so why write it twice?

2 Likes

That’s great to know! Thanks for the bonus learning opportunity in an easy explanation

I think I’ve found my fix.

I believe the line

            activeGrenade.transform.position = currentGrenadeTransform.position;
            activeGrenade.transform.rotation = currentGrenadeTransform.rotation;

in IEnumerator ThrowGrenadeAction()

public IEnumerator ThrowGrenadeAction ()
        {
            
            yield return new WaitForEndOfFrame();
            activeGrenade.transform.position = currentGrenadeTransform.position;
            activeGrenade.transform.rotation = currentGrenadeTransform.rotation;

            Rigidbody rigidBody = activeGrenade.GetComponent<Rigidbody>();
            rigidBody.useGravity = true;
            rigidBody.isKinematic = false;
            rigidBody.linearVelocity = new Vector3(0,0,0);
            Vector3 forceToAdd = cam.transform.forward * grenadeForce + transform.up * grenadeUpForce;
            rigidBody.AddForce(forceToAdd, ForceMode.Impulse);
            if (grenadeSpin)
            {
                rigidBody.AddTorque(Vector3.up * grenadeSpinRPM);
            }
            activeGrenade.GetComponent<SmokeGrenade>().Thrown = true;
            activeGrenade = null;
        }

was indirectly causing this altered trajectory.

Instead of directly teleporting the grenade to the desired hand position of the client (which the line would otherwise do with no networking involvement), it would quickly glide the NetworkObject to the desired position and perhaps the Rigidbody component would inherit this as movement velocity, so when you add the grenade throwing force, the grenade wouldn’t get launched with a Rigidbody velocity at a net zero, resulting in a difference to the desired trajectory.

The line was initially added because the clients’ grenade was being thrown from the server side location of the players hand instead of the client-side hand which looked strange and was multiplied by any latency.

To solve this:

        public void ThrowGrenade ()
        {
            if (IsOwner)
            {
                ThrowGrenadeServerRpc(currentGrenadeTransform.position, currentGrenadeTransform.rotation);
            }
        }
        [ServerRpc(RequireOwnership = false)]
        public void ThrowGrenadeServerRpc(Vector3 pos, Quaternion rot, ServerRpcParams serverRpcParams = default)
        {
            ClearGrenadeClientRpc();
            ulong senderID = serverRpcParams.Receive.SenderClientId;
            activeGrenade = NetworkManager.Singleton.SpawnManager.InstantiateAndSpawn(currentGrenade, senderID, true, true, true, pos, rot);
            ThrowGrenadeClientRpc(senderID, activeGrenade.NetworkObjectId);
        }

I am now sending the hand coordinates with the initial ServerRpc so when the client spawns the grenade it will appear at the clients hand with a net zero velocity.