Client is moving really slowly on the Server, why?

Hello,

Got an issue with my Networking it seems, Im new and still trying to learn so go easy on me.

Problem:

  • When I create host, my player character moves at a perfect and intended speed, say 2f. No issues.
  • However, when I create a Server and connects a client. My player character is moving at really really slow, dont know the exact speed but probably around 0.1f. If I temporarily change my speed in the inspector to 20 while testing, the player character moves at normal again. But its very choppy and laggy.

Appreciate any kind of help or insight on this!

using System;
using Unity.Netcode;
using UnityEngine;
using JetBrains.Annotations;
using UnityEngine.UI;
using System.Collections;
using TMPro;
using System.Collections.Generic;

public class PlayerController : NetworkBehaviour
{
    public float normalSpeed = 2f;
    public float fastSpeed = 4f; // Adjust the faster speed as desired
    public float turnSpeed = 250f; // Adjust the turn speed as desired
    public GameObject staminaSliderPrefab; // Reference to the stamina slider prefab
    public float minTurnSpeed = 90f; // The minimum allowed value for turnSpeed

    private Camera _mainCamera;
    private bool isUpKeyPressed = false;
    private PlayerLength _playerLength;
    private readonly ulong[] _targetClientArray = new ulong[1];
    private bool _canCollide = true;
    private bool isUsingFastSpeed = false;
    private float currentStamina;
    private float staminaUsageRate = 100f; // Stamina drained per second when using fastSpeed
    private float staminaRechargeRate = 15f; // Stamina recharged per second when not using fastSpeed
    private float maxStamina = 100f;
    private float fastSpeedDuration = 1f; // Duration of fastSpeed in seconds
    private float fastSpeedTimer = 0f;
    private float rechargeDelay = 0f; // Delay before stamina recharge starts after hitting 0%
    private bool isRechargeDelayed = false;
    private float rechargeDelayTimer = 0f;
    private Slider staminaSlider; // Reference to the stamina slider in the UI
    private bool isEliminated = false;
    private bool hasCollidedWithCircle = false;
    private BoxCollider2D bgBoxCollider; // Reference to the box collider of the background
    private string playerName;
   
    [CanBeNull] public static event System.Action GameOverEvent;
    [SerializeField] private NetworkObject foodPrefab;

    private void Initialize()
    {
        _mainCamera = Camera.main;
        _playerLength = GetComponent<PlayerLength>();
        currentStamina = maxStamina; // Initialize stamina to full on spawn

        // Find the background box collider using its tag
        GameObject bgGameObject = GameObject.FindGameObjectWithTag("Background");
        bgBoxCollider = bgGameObject?.GetComponent<BoxCollider2D>();
    }


    public override void OnNetworkSpawn()
    {
        base.OnNetworkSpawn();
        Initialize();


        // Find the PlayerCanvas GameObject and spawn the Slider prefab into it
        GameObject playerCanvasGO = GameObject.Find("PlayerCanvas");
        if (playerCanvasGO != null)
        {
            // Instantiate the Slider prefab and set it as a child of playerCanvasGO
            GameObject sliderGO = Instantiate(staminaSliderPrefab, playerCanvasGO.transform);
            staminaSlider = sliderGO.GetComponent<Slider>();
            staminaSlider.minValue = 0f;
            staminaSlider.maxValue = maxStamina;
            staminaSlider.value = currentStamina;

            // Find the PlayerNameFollow text object and update its text with the player's name
            TMP_Text playerNameFollowText = GameObject.Find("PlayerNameFollow")?.GetComponent<TMP_Text>();
            TMP_Text playerNameRefText = GameObject.Find("PlayerNameRef")?.GetComponent<TMP_Text>();
            if (playerNameFollowText != null && playerNameRefText != null)
            {
                playerNameFollowText.text = playerNameRefText.text; // Copy text from PlayerName to PlayerNameFollow
                UpdatePlayerNameServerRpc(playerNameFollowText.text); // Synchronize the initial name to other clients
            }
            else
            {
                Debug.LogError("PlayerNameFollow or PlayerName TextMeshPro text object not found! Make sure they exist in the scene.");
            }
        }
        else
        {
            Debug.LogError("PlayerCanvas GameObject not found! Make sure it exists in the scene.");
        }
    }

    void LateUpdate()
    {
        if (!IsOwner) return;

        // Calculate the camera offset based on the object's rotation
        Vector3 cameraOffset = new Vector3(0f, 0f, -10f);

        // Update the camera position to follow the object
        _mainCamera.transform.position = transform.position + cameraOffset;
        _mainCamera.transform.LookAt(transform.position);
    }

    void Update()
    {
        if (!IsOwner || isEliminated) return;

        UpdateStaminaUI();
        HandleStamina();
        MovePlayerServer();
        UpdatePlayerNameServerRpc(playerName);
    }

    [ServerRpc]
    private void UpdatePlayerNameServerRpc(string name)
    {
        playerName = name;
        UpdatePlayerNameClientRpc(name);
    }

    [ClientRpc]
    private void UpdatePlayerNameClientRpc(string name)
    {
        // Update the PlayerNameFollow text with the name received from the server
        TMP_Text playerNameFollowText = GameObject.Find("PlayerNameFollow")?.GetComponent<TMP_Text>();
        if (playerNameFollowText != null)
        {
            playerNameFollowText.text = name;
        }
        else
        {
            Debug.LogError("PlayerNameFollow TextMeshPro text object not found! Make sure it exists in the scene.");
        }
    }

    private IEnumerator CollisionCheckCoroutine()
    {
        _canCollide = false;
        yield return new WaitForSeconds(0.5f);
        _canCollide = true;
    }

    private void UpdateStaminaUI()
    {
        if (staminaSlider != null)
        {
            // Update the slider value based on current stamina percentage
            staminaSlider.value = currentStamina;
        }
    }

    private void HandleStamina()
    {
        // Check if the up arrow key is pressed
        isUpKeyPressed = Input.GetKey(KeyCode.UpArrow);

        // Check if using fastSpeed
        if (isUpKeyPressed && currentStamina > 0f && !isRechargeDelayed)
        {
            if (!isUsingFastSpeed)
            {
                // Start using fastSpeed
                isUsingFastSpeed = true;
                fastSpeedTimer = fastSpeedDuration;
            }

            // Drain stamina while using fastSpeed
            currentStamina -= staminaUsageRate * Time.deltaTime;
            fastSpeedTimer -= Time.deltaTime;
            if (fastSpeedTimer <= 0f)
            {
                // Stop using fastSpeed when the duration is over
                isUsingFastSpeed = false;
                isRechargeDelayed = true;
                rechargeDelayTimer = rechargeDelay;
            }
        }
        else
        {
            // Recharge delay after reaching 0% stamina
            if (isRechargeDelayed)
            {
                rechargeDelayTimer -= Time.deltaTime;
                if (rechargeDelayTimer <= 0f)
                {
                    isRechargeDelayed = false;
                }
            }
            else
            {
                // Recharge stamina when not using fastSpeed and not in recharge delay
                if (!isUpKeyPressed && currentStamina < maxStamina)
                {
                    currentStamina += staminaRechargeRate * Time.deltaTime;
                }
            }
        }

        // Ensure the stamina stays within bounds
        currentStamina = Mathf.Clamp(currentStamina, 0f, maxStamina);

        // Disable forward key (KeyCode.UpArrow) if stamina is empty
        if (currentStamina <= 0f)
        {
            isUpKeyPressed = false;
        }

        // Update the stamina bar UI here with 'currentStamina' value
    }

    public void IncreaseTurnSpeed()
    {
        turnSpeed = Mathf.Max(turnSpeed - 0.8f, minTurnSpeed);
    }

    private void MovePlayerServer()
    {
        if (isEliminated || hasCollidedWithCircle) return;

        float rotationInput = Input.GetAxisRaw("Horizontal");
        float rotationAmount = rotationInput * turnSpeed * Time.deltaTime;
        // If the rotation input is positive (arrow key to the right),
        // rotate the player in the negative direction (opposite way).
        if (rotationInput > 0)
        {
            transform.Rotate(Vector3.forward, -rotationAmount);
        }
        else if (rotationInput < 0)
        {
            // If the rotation input is negative (arrow key to the left),
            // rotate the player in the positive direction (opposite way).
            transform.Rotate(Vector3.forward, Mathf.Abs(rotationAmount));
        }

        float currentSpeed = isUpKeyPressed && currentStamina > 0f ? fastSpeed : normalSpeed;
        transform.position += transform.up * currentSpeed * Time.deltaTime;

        // Inform the server of the intended movement
        MovePlayerServerRpc(transform.position, transform.rotation);

        if (bgBoxCollider == null)
        {
            Debug.LogError("Background box collider object not found. Make sure it has the 'Background' tag and a BoxCollider2D component.");
            return;
        }

        // Check if the player is beyond the box's bounds
        Vector2 boxCenter = bgBoxCollider.bounds.center;
        Vector2 boxExtents = bgBoxCollider.bounds.extents;
        Vector2 playerPosition = transform.position;

        if (playerPosition.x - boxCenter.x > boxExtents.x ||
            playerPosition.x - boxCenter.x < -boxExtents.x ||
            playerPosition.y - boxCenter.y > boxExtents.y ||
            playerPosition.y - boxCenter.y < -boxExtents.y)
        {
            Debug.Log("Player Beyond the Box's Bounds");
            StartCoroutine(CollisionCheckCoroutine());
            GameOverClientRpc(); // Activate Game Over on the Client
            EliminatePlayerServerRpc(); // Eliminate the player on the Server

            // If the player is beyond the box's bounds, prevent further movement
            hasCollidedWithCircle = true;
            return;
        }
    }   

    [ServerRpc]
    private void MovePlayerServerRpc(Vector3 position, Quaternion rotation)
    {
        // Server handles movement and collision resolution
        transform.position = position;
        transform.rotation = rotation;

    }



    private void OnCollisionEnter2D(Collision2D col)
    {
        if (col.gameObject == this.gameObject) return; // Skip collision with self
        if (!IsOwner || !_canCollide) return;

        StartCoroutine(CollisionCheckCoroutine());

        // Handle collision with the background box collider
        if (bgBoxCollider != null && col.collider == bgBoxCollider)
        {
            // Check if the player is beyond the box's bounds
            Vector2 boxCenter = bgBoxCollider.bounds.center;
            Vector2 boxExtents = bgBoxCollider.bounds.extents;
            Vector2 playerPosition = transform.position;

            if (playerPosition.x - boxCenter.x > boxExtents.x ||
                playerPosition.x - boxCenter.x < -boxExtents.x ||
                playerPosition.y - boxCenter.y > boxExtents.y ||
                playerPosition.y - boxCenter.y < -boxExtents.y)
            {
                Debug.Log("Player Beyond the Box's Bounds");
                StartCoroutine(CollisionCheckCoroutine());
                GameOverClientRpc(); // Activate Game Over on the Client
                EliminatePlayerServerRpc(); // Eliminate the player on the Server
            }
        }
        else // Head-on Collision or Tail Collision
        {
            if (col.gameObject.TryGetComponent(out PlayerLength playerLength))
            {
                Debug.Log("Tail Collision");
                var player1 = new PlayerData()
                {
                    id = OwnerClientId,
                    length = _playerLength.length.Value
                };
                var player2 = new PlayerData()
                {
                    id = playerLength.OwnerClientId,
                    length = playerLength.length.Value
                };
                DetermineCollisionWinnerServer(player1, player2);
            }
            else if (col.gameObject.TryGetComponent(out Tail tail))
            {
                Debug.Log("Tail Collision");
                WinInformationServerRpc(winner: tail.networkedOwner.GetComponent<PlayerController>().OwnerClientId, loser: OwnerClientId);
            }
        }  
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (!IsOwner || isEliminated) return;

        // Handle trigger collision with the background box collider
        if (bgBoxCollider != null && other == bgBoxCollider)
        {
            // Check if the player is beyond the box's bounds
            Vector2 boxCenter = bgBoxCollider.bounds.center;
            Vector2 boxExtents = bgBoxCollider.bounds.extents;
            Vector2 playerPosition = transform.position;

            if (playerPosition.x - boxCenter.x > boxExtents.x ||
                playerPosition.x - boxCenter.x < -boxExtents.x ||
                playerPosition.y - boxCenter.y > boxExtents.y ||
                playerPosition.y - boxCenter.y < -boxExtents.y)
            {
                Debug.Log("Player Beyond the Box's Bounds");
                isEliminated = true;
                GameOverClientRpc(); // Activate Game Over on the Client
                EliminatePlayerServerRpc(); // Eliminate the player on the Server
            }
        }
    }

    private void DetermineCollisionWinnerServer(PlayerData player1, PlayerData player2)
    {
        if (player1.length > player2.length)
        {
            WinInformationServerRpc(winner: player1.id, loser: player2.id);
        }
        else
        {
            WinInformationServerRpc(winner: player2.id, loser: player1.id);
        }
    }

    [ServerRpc]
    private void WinInformationServerRpc(ulong winner, ulong loser)
    {
        _targetClientArray[0] = winner;
        ClientRpcParams clientRpcParams = new ClientRpcParams
        {
            Send = new ClientRpcSendParams
            {
                TargetClientIds = _targetClientArray
            }
        };
        AtePlayerClientRpc(clientRpcParams);

        _targetClientArray[0] = loser;
        clientRpcParams.Send.TargetClientIds = _targetClientArray;
        GameOverClientRpc(clientRpcParams);
    }

    [ClientRpc]
    private void AtePlayerClientRpc(ClientRpcParams ClientRpcParams = default)
    {
        if (!IsOwner) return;
        Debug.Log(message: "You Ate a Player");
    }

    [ClientRpc]
    private void GameOverClientRpc(ClientRpcParams clientRpcParams = default)
    {
        if (!IsOwner) return;
        Debug.Log(message: "You Lose");

        // Call the method to spawn food at the location of each tail
        if (isEliminated && _playerLength != null)
        {
            SpawnFoodAtTails(_playerLength.Tails);
        }

        GameOverEvent?.Invoke();
        NetworkManager.Singleton.Shutdown();
    }

    [ServerRpc]
    private void EliminatePlayerServerRpc()
    {
        if (IsOwner && !isEliminated)
        {
            isEliminated = true;
            Debug.Log("You are eliminated.");
            // Additional elimination logic can be added here
        }
    }

    private void SpawnFoodAtTails(List<GameObject> tails)
    {
        // Loop through all the tails and spawn food at their positions
        foreach (var tailObj in tails)
        {
            Vector3 tailPosition = tailObj.transform.position;

            // Spawn the food prefab using NetworkManager
            NetworkObject foodObj = NetworkObject.Instantiate(foodPrefab, tailPosition, Quaternion.identity);
            if (!foodObj.IsSpawned) foodObj.Spawn(destroyWithScene: true);
        }
    }

    struct PlayerData : INetworkSerializable
    {
        public ulong id;
        public ushort length;

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref id);
            serializer.SerializeValue(ref length);
        }
    }
}

Okay that‘s just too much code.
I assume this is Netcode for GameObjects? If not and it‘s UNet: don‘t use that, it‘s deprecated.
If you do youse NGO why aren‘t you using NetworkTransform? You don‘t have to call a ServerRPC with position and such, the transform is automatically and efficiently synchronized.

Thanks for the answer!
Exactly, Netcode for GameObjects.

Please enlightent me, I thought I were using the NetworkTransform already?
Should I in your opinion just keep the movements in the Update() and skip out on the ServerRPC?

    void Update()
    {
        if (!IsOwner || isEliminated) return;

        UpdateStaminaUI();
        HandleStamina();
        UpdatePlayerNameServerRpc(playerName);

        float rotationInput = Input.GetAxisRaw("Horizontal");
        float rotationAmount = rotationInput * turnSpeed * Time.deltaTime;
        // If the rotation input is positive (arrow key to the right),
        // rotate the player in the negative direction (opposite way).
        if (rotationInput > 0)
        {
            transform.Rotate(Vector3.forward, -rotationAmount);
        }
        else if (rotationInput < 0)
        {
            // If the rotation input is negative (arrow key to the left),
            // rotate the player in the positive direction (opposite way).
            transform.Rotate(Vector3.forward, Mathf.Abs(rotationAmount));
        }

        float currentSpeed = isUpKeyPressed && currentStamina > 0f ? fastSpeed : normalSpeed;
        transform.position += transform.up * currentSpeed * Time.deltaTime;
    }

If you already use NetworkTransform but also manually pass transform position via RPC and have the server apply that you are effectively undoing the NetworkTransform behaviour. I guess that‘s likely going to lead to the slow movement issue you see because you are possibly only adjusting the difference between NetworkTransform (local) position and the server side position of the client plus any position interpolation that happened between the sync of both changes (eg likely a frame or two, if at all).

Meaning: let the NetworkTransform do its job. Do not manually send or modify transform values from the client to the server.

That made the trick, now its moving really smoothly again. Thanks alot for the help!

For some reason my client player wont rotate on the Server as it does on the local host but I guess ill have to figure that out.

im having a similar problem that i cant seem to fix in my game. The host moves just fine but the client moves exeptionaly slower than the host. Here is my script:

using UnityEngine;
using Unity.Netcode;

public class Move : NetworkBehaviour
{
//GoodScript
[SerializeField]
public float moveSpeed = 5f;
public float jumpForce = 10f;
private bool isGrounded;
public Animator anim1;
public ParticleSystem ps1;
public AudioSource au1;
public AudioSource au2;

private Rigidbody2D rb;

private NetworkVariable networkPosition = new NetworkVariable();

private void Start()
{
rb = GetComponent();
}

void Update()
{
if (IsOwner)
{
HandleMovement();
HandleJump();

}
else
{
// Smoothly interpolate to the new position
transform.position = Vector2.Lerp(transform.position, networkPosition.Value, Time.deltaTime * moveSpeed);
}
// Update the position on the server
UpdatePositionServerRpc(transform.position);
}

private void HandleMovement()
{
float horizontalInput = Input.GetAxis(“Horizontal”);
Vector2 move = new Vector2(horizontalInput * moveSpeed * Time.deltaTime, 0f);
transform.Translate(move);

// Update animation and particle effects
if (horizontalInput == 0)
{
anim1.SetBool(“running”, false);
if (!au1.isPlaying) au1.Play();
}
else
{
if (!ps1.isPlaying) ps1.Play();
anim1.SetBool(“running”, true);
}
}

private void HandleJump()
{
if (Input.GetButtonDown(“Jump”) && isGrounded)
{
au2.Play();
rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
isGrounded = false;
}

anim1.SetBool(“jump”, !isGrounded);
}

void OnCollisionEnter2D(Collision2D collision)
{
if (IsOwner)
{
if (collision.gameObject.CompareTag(“Ground”))
{
isGrounded = true;
}
}

}

[ServerRpc(RequireOwnership = false)]
void UpdatePositionServerRpc(Vector2 position)
{
networkPosition.Value = position;
}
}

Don’t move network players with a dynamic rigidbody, it just won’t work well because only the server runs physics simulation.