Mirror multiplayer physics issue

Hello community!

I have a problem transforming a simple game into a working multiplayer game using #Mirror.
Here is my controller that works in a single player context:

using UnityEngine;

namespace QuickStart
{
    public class VelocityMovement : MonoBehaviour
    {
        public float mouseSensitivity = 150.0f;
        public float hitForceMultiplier = 2.0f; // Multiplier for force applied to the ball
        private bool cursorLocked = true;
        private Rigidbody rbPlayer;
        private Vector3 accumulatedMovement = Vector3.zero;

        public void Awake()
        {
            LockCursor();
            rbPlayer = GetComponent<Rigidbody>();
            rbPlayer.constraints = RigidbodyConstraints.FreezeRotation | RigidbodyConstraints.FreezePositionY;
            rbPlayer.interpolation = RigidbodyInterpolation.Interpolate;
        }

        public void Update()
        {
            float mouseX = Input.GetAxis("Mouse X");
            float mouseY = Input.GetAxis("Mouse Y");

            Vector3 movement = new Vector3(mouseX, 0, mouseY) * mouseSensitivity * Time.deltaTime;
            if (movement.magnitude > 0.001f)
            {
                accumulatedMovement += movement;
            }
        }

        void FixedUpdate()
        {
            if (Input.GetKeyDown(KeyCode.Escape))
            {
                UnlockCursor();
            }
            else if (!cursorLocked && Input.GetKeyDown(KeyCode.S))
            {
                LockCursor();
            }

            if (cursorLocked)
            {
                HandleMouseMovement();
            }
        }

        private Vector3 GetMouseMovement()
        {
            float mouseX = Input.GetAxis("Mouse X");
            float mouseY = Input.GetAxis("Mouse Y");

            return new Vector3(mouseX, 0, mouseY) * mouseSensitivity;
        }

        private void HandleMouseMovement()
        {
            if (accumulatedMovement.magnitude > 0.01f)
            {
                Vector3 targetVelocity = accumulatedMovement / Time.fixedDeltaTime;
                rbPlayer.linearVelocity = new Vector3(targetVelocity.x, rbPlayer.linearVelocity.y, targetVelocity.z);
            }
            else
            {
                rbPlayer.linearVelocity = new Vector3(0, rbPlayer.linearVelocity.y, 0);
            }

            accumulatedMovement = Vector3.zero;
        }

        private void LockCursor()
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
            cursorLocked = true;
        }

        private void UnlockCursor()
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
            cursorLocked = false;
        }

        void OnDisable()
        {
            UnlockCursor();
        }
    }
}

This script moves my player in sync with mouse movements.
Once the player collides with a ball the ball gets hit based on rigidbody physics.
Now In the multiplayer context I extended the script as follows:

using UnityEngine;
using Mirror;

namespace QuickStart
{
    public class VelocityMovementNetwork : NetworkBehaviour
    {
        public float mouseSensitivity = 150.0f;
        public float hitForceMultiplier = 2.0f;
        private bool cursorLocked = true;
        private Rigidbody rbPlayer;
        private Vector3 accumulatedMovement = Vector3.zero;

        private GameObject ball;
        private Rigidbody ballRb;

        public void Awake()
        {
            LockCursor();
            ball = GameObject.FindWithTag("Ball");
            ballRb = ball.GetComponent<Rigidbody>();

            rbPlayer = GetComponent<Rigidbody>();
            rbPlayer.constraints = RigidbodyConstraints.FreezeRotation | RigidbodyConstraints.FreezePositionY;
            rbPlayer.interpolation = RigidbodyInterpolation.Interpolate;
        }

        public void Update()
        {
            if (!isLocalPlayer) return;

            float mouseX = Input.GetAxis("Mouse X");
            float mouseY = Input.GetAxis("Mouse Y");

            Vector3 movement = new Vector3(mouseX, 0, mouseY) * mouseSensitivity * Time.deltaTime;
            if (movement.magnitude > 0.001f)
            {
                accumulatedMovement += movement;
            }
        }

        void FixedUpdate()
        {
            if (!isLocalPlayer) return;

            if (Input.GetKeyDown(KeyCode.Escape))
            {
                UnlockCursor();
            }
            else if (!cursorLocked && Input.GetKeyDown(KeyCode.S))
            {
                LockCursor();
            }

            if (cursorLocked)
            {
                HandleMouseMovement();
            }
        }

        private void HandleMouseMovement()
        {
            if (accumulatedMovement.magnitude > 0.01f)
            {
                Vector3 targetVelocity = accumulatedMovement / Time.fixedDeltaTime;
                //rbPlayer.AddForce(targetVelocity, ForceMode.VelocityChange);
                rbPlayer.linearVelocity = new Vector3(targetVelocity.x, rbPlayer.linearVelocity.y, targetVelocity.z);
            }
            else
            {
                Vector3 targetVelocity = new Vector3(0, rbPlayer.linearVelocity.y, 0);
                //rbPlayer.AddForce(targetVelocity, ForceMode.VelocityChange);
                rbPlayer.linearVelocity = targetVelocity;
            }

            accumulatedMovement = Vector3.zero;
        }

        private void LockCursor()
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
            cursorLocked = true;
        }

        private void UnlockCursor()
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
            cursorLocked = false;
        }

        void OnDisable()
        {
            UnlockCursor();
        }

        [Command]
        private void CmdHitBall(Vector3 hitDirection, float hitForce)
        {
            // call the Apply function on the server
            Apply(hitDirection, hitForce);
        }

        private void Apply(Vector3 hitDirection, float hitForce)
        {
            hitDirection.y = 0; // Prevent the ball from flying up
            if (ball)
            {
                Debug.Log("Hit ball with force: " + hitForce);
                ballRb.linearVelocity = hitDirection * hitForce;
            }
        }

        private void TriggerForce(Vector3 hitDirection, float hitForce)
        {
            Debug.Log("Hit ball with force: " + hitForce);

            Apply(hitDirection, hitForce); // Client Prediction
            if (isClient)
            {
                CmdHitBall(hitDirection, hitForce); // Server Authority
            }
        }

        // Detect collision with the ball and apply speed-based force
        private void OnCollisionEnter(Collision collision)
        {
            if (!isLocalPlayer) return;

            if (collision.gameObject.CompareTag("Ball"))
            {
                float playerSpeed = rbPlayer.linearVelocity.magnitude;
                Debug.Log("Player speed: " + playerSpeed);

                if (playerSpeed > 0.01f)
                {
                    Vector3 hitDirection = (collision.transform.position - transform.position).normalized;
                    float hitForce = playerSpeed * hitForceMultiplier;

                    TriggerForce(hitDirection, hitForce);
                }
            }
        }
    }
}

As you can see in the video:

movements don’t feel natural sometimes and also the ball is tunneling the player in some frames.
The player has a Network Transform applied and I added a network latency (Latency Simulation) of 10ms.

The ball has a Rigidbody as well as a Predicted Rigidbody:

Does anybody have an idea how to fix this issue? Is there something I am conceptionally doing wrong? E.g. is it a problem to use Rigidbody and to apply a force OnCollisionEnter? If yes, how would I solve that?
I would be very grateful for any help.

Best regards
Josch

I use NGO which doesn’t support rigidbody prediction out of the box with a component. So you have to implement it yourself with the anticipation system.

The results you see are actually pretty decent.