Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Resolved Client is moving really slowly on the Server, why?

Discussion in 'Multiplayer' started by purjosopan, Jul 31, 2023.

  1. purjosopan

    purjosopan

    Joined:
    Jul 18, 2023
    Posts:
    3
    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!

    Code (CSharp):
    1. using System;
    2. using Unity.Netcode;
    3. using UnityEngine;
    4. using JetBrains.Annotations;
    5. using UnityEngine.UI;
    6. using System.Collections;
    7. using TMPro;
    8. using System.Collections.Generic;
    9.  
    10. public class PlayerController : NetworkBehaviour
    11. {
    12.     public float normalSpeed = 2f;
    13.     public float fastSpeed = 4f; // Adjust the faster speed as desired
    14.     public float turnSpeed = 250f; // Adjust the turn speed as desired
    15.     public GameObject staminaSliderPrefab; // Reference to the stamina slider prefab
    16.     public float minTurnSpeed = 90f; // The minimum allowed value for turnSpeed
    17.  
    18.     private Camera _mainCamera;
    19.     private bool isUpKeyPressed = false;
    20.     private PlayerLength _playerLength;
    21.     private readonly ulong[] _targetClientArray = new ulong[1];
    22.     private bool _canCollide = true;
    23.     private bool isUsingFastSpeed = false;
    24.     private float currentStamina;
    25.     private float staminaUsageRate = 100f; // Stamina drained per second when using fastSpeed
    26.     private float staminaRechargeRate = 15f; // Stamina recharged per second when not using fastSpeed
    27.     private float maxStamina = 100f;
    28.     private float fastSpeedDuration = 1f; // Duration of fastSpeed in seconds
    29.     private float fastSpeedTimer = 0f;
    30.     private float rechargeDelay = 0f; // Delay before stamina recharge starts after hitting 0%
    31.     private bool isRechargeDelayed = false;
    32.     private float rechargeDelayTimer = 0f;
    33.     private Slider staminaSlider; // Reference to the stamina slider in the UI
    34.     private bool isEliminated = false;
    35.     private bool hasCollidedWithCircle = false;
    36.     private BoxCollider2D bgBoxCollider; // Reference to the box collider of the background
    37.     private string playerName;
    38.    
    39.     [CanBeNull] public static event System.Action GameOverEvent;
    40.     [SerializeField] private NetworkObject foodPrefab;
    41.  
    42.     private void Initialize()
    43.     {
    44.         _mainCamera = Camera.main;
    45.         _playerLength = GetComponent<PlayerLength>();
    46.         currentStamina = maxStamina; // Initialize stamina to full on spawn
    47.  
    48.         // Find the background box collider using its tag
    49.         GameObject bgGameObject = GameObject.FindGameObjectWithTag("Background");
    50.         bgBoxCollider = bgGameObject?.GetComponent<BoxCollider2D>();
    51.     }
    52.  
    53.  
    54.     public override void OnNetworkSpawn()
    55.     {
    56.         base.OnNetworkSpawn();
    57.         Initialize();
    58.  
    59.  
    60.         // Find the PlayerCanvas GameObject and spawn the Slider prefab into it
    61.         GameObject playerCanvasGO = GameObject.Find("PlayerCanvas");
    62.         if (playerCanvasGO != null)
    63.         {
    64.             // Instantiate the Slider prefab and set it as a child of playerCanvasGO
    65.             GameObject sliderGO = Instantiate(staminaSliderPrefab, playerCanvasGO.transform);
    66.             staminaSlider = sliderGO.GetComponent<Slider>();
    67.             staminaSlider.minValue = 0f;
    68.             staminaSlider.maxValue = maxStamina;
    69.             staminaSlider.value = currentStamina;
    70.  
    71.             // Find the PlayerNameFollow text object and update its text with the player's name
    72.             TMP_Text playerNameFollowText = GameObject.Find("PlayerNameFollow")?.GetComponent<TMP_Text>();
    73.             TMP_Text playerNameRefText = GameObject.Find("PlayerNameRef")?.GetComponent<TMP_Text>();
    74.             if (playerNameFollowText != null && playerNameRefText != null)
    75.             {
    76.                 playerNameFollowText.text = playerNameRefText.text; // Copy text from PlayerName to PlayerNameFollow
    77.                 UpdatePlayerNameServerRpc(playerNameFollowText.text); // Synchronize the initial name to other clients
    78.             }
    79.             else
    80.             {
    81.                 Debug.LogError("PlayerNameFollow or PlayerName TextMeshPro text object not found! Make sure they exist in the scene.");
    82.             }
    83.         }
    84.         else
    85.         {
    86.             Debug.LogError("PlayerCanvas GameObject not found! Make sure it exists in the scene.");
    87.         }
    88.     }
    89.  
    90.     void LateUpdate()
    91.     {
    92.         if (!IsOwner) return;
    93.  
    94.         // Calculate the camera offset based on the object's rotation
    95.         Vector3 cameraOffset = new Vector3(0f, 0f, -10f);
    96.  
    97.         // Update the camera position to follow the object
    98.         _mainCamera.transform.position = transform.position + cameraOffset;
    99.         _mainCamera.transform.LookAt(transform.position);
    100.     }
    101.  
    102.     void Update()
    103.     {
    104.         if (!IsOwner || isEliminated) return;
    105.  
    106.         UpdateStaminaUI();
    107.         HandleStamina();
    108.         MovePlayerServer();
    109.         UpdatePlayerNameServerRpc(playerName);
    110.     }
    111.  
    112.     [ServerRpc]
    113.     private void UpdatePlayerNameServerRpc(string name)
    114.     {
    115.         playerName = name;
    116.         UpdatePlayerNameClientRpc(name);
    117.     }
    118.  
    119.     [ClientRpc]
    120.     private void UpdatePlayerNameClientRpc(string name)
    121.     {
    122.         // Update the PlayerNameFollow text with the name received from the server
    123.         TMP_Text playerNameFollowText = GameObject.Find("PlayerNameFollow")?.GetComponent<TMP_Text>();
    124.         if (playerNameFollowText != null)
    125.         {
    126.             playerNameFollowText.text = name;
    127.         }
    128.         else
    129.         {
    130.             Debug.LogError("PlayerNameFollow TextMeshPro text object not found! Make sure it exists in the scene.");
    131.         }
    132.     }
    133.  
    134.     private IEnumerator CollisionCheckCoroutine()
    135.     {
    136.         _canCollide = false;
    137.         yield return new WaitForSeconds(0.5f);
    138.         _canCollide = true;
    139.     }
    140.  
    141.     private void UpdateStaminaUI()
    142.     {
    143.         if (staminaSlider != null)
    144.         {
    145.             // Update the slider value based on current stamina percentage
    146.             staminaSlider.value = currentStamina;
    147.         }
    148.     }
    149.  
    150.     private void HandleStamina()
    151.     {
    152.         // Check if the up arrow key is pressed
    153.         isUpKeyPressed = Input.GetKey(KeyCode.UpArrow);
    154.  
    155.         // Check if using fastSpeed
    156.         if (isUpKeyPressed && currentStamina > 0f && !isRechargeDelayed)
    157.         {
    158.             if (!isUsingFastSpeed)
    159.             {
    160.                 // Start using fastSpeed
    161.                 isUsingFastSpeed = true;
    162.                 fastSpeedTimer = fastSpeedDuration;
    163.             }
    164.  
    165.             // Drain stamina while using fastSpeed
    166.             currentStamina -= staminaUsageRate * Time.deltaTime;
    167.             fastSpeedTimer -= Time.deltaTime;
    168.             if (fastSpeedTimer <= 0f)
    169.             {
    170.                 // Stop using fastSpeed when the duration is over
    171.                 isUsingFastSpeed = false;
    172.                 isRechargeDelayed = true;
    173.                 rechargeDelayTimer = rechargeDelay;
    174.             }
    175.         }
    176.         else
    177.         {
    178.             // Recharge delay after reaching 0% stamina
    179.             if (isRechargeDelayed)
    180.             {
    181.                 rechargeDelayTimer -= Time.deltaTime;
    182.                 if (rechargeDelayTimer <= 0f)
    183.                 {
    184.                     isRechargeDelayed = false;
    185.                 }
    186.             }
    187.             else
    188.             {
    189.                 // Recharge stamina when not using fastSpeed and not in recharge delay
    190.                 if (!isUpKeyPressed && currentStamina < maxStamina)
    191.                 {
    192.                     currentStamina += staminaRechargeRate * Time.deltaTime;
    193.                 }
    194.             }
    195.         }
    196.  
    197.         // Ensure the stamina stays within bounds
    198.         currentStamina = Mathf.Clamp(currentStamina, 0f, maxStamina);
    199.  
    200.         // Disable forward key (KeyCode.UpArrow) if stamina is empty
    201.         if (currentStamina <= 0f)
    202.         {
    203.             isUpKeyPressed = false;
    204.         }
    205.  
    206.         // Update the stamina bar UI here with 'currentStamina' value
    207.     }
    208.  
    209.     public void IncreaseTurnSpeed()
    210.     {
    211.         turnSpeed = Mathf.Max(turnSpeed - 0.8f, minTurnSpeed);
    212.     }
    213.  
    214.     private void MovePlayerServer()
    215.     {
    216.         if (isEliminated || hasCollidedWithCircle) return;
    217.  
    218.         float rotationInput = Input.GetAxisRaw("Horizontal");
    219.         float rotationAmount = rotationInput * turnSpeed * Time.deltaTime;
    220.         // If the rotation input is positive (arrow key to the right),
    221.         // rotate the player in the negative direction (opposite way).
    222.         if (rotationInput > 0)
    223.         {
    224.             transform.Rotate(Vector3.forward, -rotationAmount);
    225.         }
    226.         else if (rotationInput < 0)
    227.         {
    228.             // If the rotation input is negative (arrow key to the left),
    229.             // rotate the player in the positive direction (opposite way).
    230.             transform.Rotate(Vector3.forward, Mathf.Abs(rotationAmount));
    231.         }
    232.  
    233.         float currentSpeed = isUpKeyPressed && currentStamina > 0f ? fastSpeed : normalSpeed;
    234.         transform.position += transform.up * currentSpeed * Time.deltaTime;
    235.  
    236.         // Inform the server of the intended movement
    237.         MovePlayerServerRpc(transform.position, transform.rotation);
    238.  
    239.         if (bgBoxCollider == null)
    240.         {
    241.             Debug.LogError("Background box collider object not found. Make sure it has the 'Background' tag and a BoxCollider2D component.");
    242.             return;
    243.         }
    244.  
    245.         // Check if the player is beyond the box's bounds
    246.         Vector2 boxCenter = bgBoxCollider.bounds.center;
    247.         Vector2 boxExtents = bgBoxCollider.bounds.extents;
    248.         Vector2 playerPosition = transform.position;
    249.  
    250.         if (playerPosition.x - boxCenter.x > boxExtents.x ||
    251.             playerPosition.x - boxCenter.x < -boxExtents.x ||
    252.             playerPosition.y - boxCenter.y > boxExtents.y ||
    253.             playerPosition.y - boxCenter.y < -boxExtents.y)
    254.         {
    255.             Debug.Log("Player Beyond the Box's Bounds");
    256.             StartCoroutine(CollisionCheckCoroutine());
    257.             GameOverClientRpc(); // Activate Game Over on the Client
    258.             EliminatePlayerServerRpc(); // Eliminate the player on the Server
    259.  
    260.             // If the player is beyond the box's bounds, prevent further movement
    261.             hasCollidedWithCircle = true;
    262.             return;
    263.         }
    264.     }  
    265.  
    266.     [ServerRpc]
    267.     private void MovePlayerServerRpc(Vector3 position, Quaternion rotation)
    268.     {
    269.         // Server handles movement and collision resolution
    270.         transform.position = position;
    271.         transform.rotation = rotation;
    272.  
    273.     }
    274.  
    275.  
    276.  
    277.     private void OnCollisionEnter2D(Collision2D col)
    278.     {
    279.         if (col.gameObject == this.gameObject) return; // Skip collision with self
    280.         if (!IsOwner || !_canCollide) return;
    281.  
    282.         StartCoroutine(CollisionCheckCoroutine());
    283.  
    284.         // Handle collision with the background box collider
    285.         if (bgBoxCollider != null && col.collider == bgBoxCollider)
    286.         {
    287.             // Check if the player is beyond the box's bounds
    288.             Vector2 boxCenter = bgBoxCollider.bounds.center;
    289.             Vector2 boxExtents = bgBoxCollider.bounds.extents;
    290.             Vector2 playerPosition = transform.position;
    291.  
    292.             if (playerPosition.x - boxCenter.x > boxExtents.x ||
    293.                 playerPosition.x - boxCenter.x < -boxExtents.x ||
    294.                 playerPosition.y - boxCenter.y > boxExtents.y ||
    295.                 playerPosition.y - boxCenter.y < -boxExtents.y)
    296.             {
    297.                 Debug.Log("Player Beyond the Box's Bounds");
    298.                 StartCoroutine(CollisionCheckCoroutine());
    299.                 GameOverClientRpc(); // Activate Game Over on the Client
    300.                 EliminatePlayerServerRpc(); // Eliminate the player on the Server
    301.             }
    302.         }
    303.         else // Head-on Collision or Tail Collision
    304.         {
    305.             if (col.gameObject.TryGetComponent(out PlayerLength playerLength))
    306.             {
    307.                 Debug.Log("Tail Collision");
    308.                 var player1 = new PlayerData()
    309.                 {
    310.                     id = OwnerClientId,
    311.                     length = _playerLength.length.Value
    312.                 };
    313.                 var player2 = new PlayerData()
    314.                 {
    315.                     id = playerLength.OwnerClientId,
    316.                     length = playerLength.length.Value
    317.                 };
    318.                 DetermineCollisionWinnerServer(player1, player2);
    319.             }
    320.             else if (col.gameObject.TryGetComponent(out Tail tail))
    321.             {
    322.                 Debug.Log("Tail Collision");
    323.                 WinInformationServerRpc(winner: tail.networkedOwner.GetComponent<PlayerController>().OwnerClientId, loser: OwnerClientId);
    324.             }
    325.         }  
    326.     }
    327.  
    328.     private void OnTriggerEnter2D(Collider2D other)
    329.     {
    330.         if (!IsOwner || isEliminated) return;
    331.  
    332.         // Handle trigger collision with the background box collider
    333.         if (bgBoxCollider != null && other == bgBoxCollider)
    334.         {
    335.             // Check if the player is beyond the box's bounds
    336.             Vector2 boxCenter = bgBoxCollider.bounds.center;
    337.             Vector2 boxExtents = bgBoxCollider.bounds.extents;
    338.             Vector2 playerPosition = transform.position;
    339.  
    340.             if (playerPosition.x - boxCenter.x > boxExtents.x ||
    341.                 playerPosition.x - boxCenter.x < -boxExtents.x ||
    342.                 playerPosition.y - boxCenter.y > boxExtents.y ||
    343.                 playerPosition.y - boxCenter.y < -boxExtents.y)
    344.             {
    345.                 Debug.Log("Player Beyond the Box's Bounds");
    346.                 isEliminated = true;
    347.                 GameOverClientRpc(); // Activate Game Over on the Client
    348.                 EliminatePlayerServerRpc(); // Eliminate the player on the Server
    349.             }
    350.         }
    351.     }
    352.  
    353.     private void DetermineCollisionWinnerServer(PlayerData player1, PlayerData player2)
    354.     {
    355.         if (player1.length > player2.length)
    356.         {
    357.             WinInformationServerRpc(winner: player1.id, loser: player2.id);
    358.         }
    359.         else
    360.         {
    361.             WinInformationServerRpc(winner: player2.id, loser: player1.id);
    362.         }
    363.     }
    364.  
    365.     [ServerRpc]
    366.     private void WinInformationServerRpc(ulong winner, ulong loser)
    367.     {
    368.         _targetClientArray[0] = winner;
    369.         ClientRpcParams clientRpcParams = new ClientRpcParams
    370.         {
    371.             Send = new ClientRpcSendParams
    372.             {
    373.                 TargetClientIds = _targetClientArray
    374.             }
    375.         };
    376.         AtePlayerClientRpc(clientRpcParams);
    377.  
    378.         _targetClientArray[0] = loser;
    379.         clientRpcParams.Send.TargetClientIds = _targetClientArray;
    380.         GameOverClientRpc(clientRpcParams);
    381.     }
    382.  
    383.     [ClientRpc]
    384.     private void AtePlayerClientRpc(ClientRpcParams ClientRpcParams = default)
    385.     {
    386.         if (!IsOwner) return;
    387.         Debug.Log(message: "You Ate a Player");
    388.     }
    389.  
    390.     [ClientRpc]
    391.     private void GameOverClientRpc(ClientRpcParams clientRpcParams = default)
    392.     {
    393.         if (!IsOwner) return;
    394.         Debug.Log(message: "You Lose");
    395.  
    396.         // Call the method to spawn food at the location of each tail
    397.         if (isEliminated && _playerLength != null)
    398.         {
    399.             SpawnFoodAtTails(_playerLength.Tails);
    400.         }
    401.  
    402.         GameOverEvent?.Invoke();
    403.         NetworkManager.Singleton.Shutdown();
    404.     }
    405.  
    406.     [ServerRpc]
    407.     private void EliminatePlayerServerRpc()
    408.     {
    409.         if (IsOwner && !isEliminated)
    410.         {
    411.             isEliminated = true;
    412.             Debug.Log("You are eliminated.");
    413.             // Additional elimination logic can be added here
    414.         }
    415.     }
    416.  
    417.     private void SpawnFoodAtTails(List<GameObject> tails)
    418.     {
    419.         // Loop through all the tails and spawn food at their positions
    420.         foreach (var tailObj in tails)
    421.         {
    422.             Vector3 tailPosition = tailObj.transform.position;
    423.  
    424.             // Spawn the food prefab using NetworkManager
    425.             NetworkObject foodObj = NetworkObject.Instantiate(foodPrefab, tailPosition, Quaternion.identity);
    426.             if (!foodObj.IsSpawned) foodObj.Spawn(destroyWithScene: true);
    427.         }
    428.     }
    429.  
    430.     struct PlayerData : INetworkSerializable
    431.     {
    432.         public ulong id;
    433.         public ushort length;
    434.  
    435.         public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    436.         {
    437.             serializer.SerializeValue(ref id);
    438.             serializer.SerializeValue(ref length);
    439.         }
    440.     }
    441. }
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,979
    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.
     
  3. purjosopan

    purjosopan

    Joined:
    Jul 18, 2023
    Posts:
    3
    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?
    Code (CSharp):
    1.     void Update()
    2.     {
    3.         if (!IsOwner || isEliminated) return;
    4.  
    5.         UpdateStaminaUI();
    6.         HandleStamina();
    7.         UpdatePlayerNameServerRpc(playerName);
    8.  
    9.         float rotationInput = Input.GetAxisRaw("Horizontal");
    10.         float rotationAmount = rotationInput * turnSpeed * Time.deltaTime;
    11.         // If the rotation input is positive (arrow key to the right),
    12.         // rotate the player in the negative direction (opposite way).
    13.         if (rotationInput > 0)
    14.         {
    15.             transform.Rotate(Vector3.forward, -rotationAmount);
    16.         }
    17.         else if (rotationInput < 0)
    18.         {
    19.             // If the rotation input is negative (arrow key to the left),
    20.             // rotate the player in the positive direction (opposite way).
    21.             transform.Rotate(Vector3.forward, Mathf.Abs(rotationAmount));
    22.         }
    23.  
    24.         float currentSpeed = isUpKeyPressed && currentStamina > 0f ? fastSpeed : normalSpeed;
    25.         transform.position += transform.up * currentSpeed * Time.deltaTime;
    26.     }
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,979
    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.
     
  5. purjosopan

    purjosopan

    Joined:
    Jul 18, 2023
    Posts:
    3
    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.