Search Unity

Question Client-Side Prediction Rotation Issue In Unity Netcode

Discussion in 'Multiplayer' started by Hris54, Feb 1, 2023.

  1. Hris54

    Hris54

    Joined:
    Dec 6, 2021
    Posts:
    65
    Hey Everyone,
    First let me tell you about my setup. My player object have two parts ,
    1. Animated part which plays animations
    2 I have active ragdoll , each bone has configurable joint and I am copying the rotation of each configurable joint from animated part of the player.

    I am trying to remove lag when rotating a client player in server authoritative model so I watched this video on client-side prediction


    But I am having this weird issue ( Left Side is Host and right side is client)


    Here is my scripts

    Client Script -

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Netcode;
    5. public struct InputPayload : INetworkSerializable
    6. {
    7.     public int tick;
    8.     public Vector3 inputVector;
    9.     public Quaternion rotationVector;
    10.     public ulong clientId;
    11.  
    12.     public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    13.     {
    14.         serializer.SerializeValue(ref tick);
    15.         serializer.SerializeValue(ref inputVector);
    16.         serializer.SerializeValue(ref rotationVector);
    17.         serializer.SerializeValue(ref clientId);
    18.        
    19.     }
    20. }
    21.  
    22. public struct StatePayload : INetworkSerializable
    23. {
    24.     public int tick;
    25.     public Vector3 position;
    26.     public Quaternion rotation;
    27.     public ulong clientId;
    28.     public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    29.     {
    30.         serializer.SerializeValue(ref tick);
    31.         serializer.SerializeValue(ref position);
    32.         serializer.SerializeValue(ref rotation);
    33.         serializer.SerializeValue(ref clientId);
    34.      
    35.     }
    36. }
    37.  
    38. public class Client : NetworkBehaviour
    39. {
    40.  
    41.  
    42.     // Shared
    43.     private float timer;
    44.     private int currentTick;
    45.     private float minTimeBetweenTicks;
    46.     private const float SERVER_TICK_RATE = 30f;
    47.     private const int BUFFER_SIZE = 1024;
    48.  
    49.     // Client specific
    50.     private StatePayload[] stateBuffer;
    51.     private InputPayload[] inputBuffer;
    52.     private StatePayload latestServerState;
    53.     private StatePayload lastProcessedState;
    54.     public Vector3 moveAmount;
    55.     float horizontal;
    56.     float vertical;
    57.     public Rigidbody hips;
    58.     public Quaternion rotationValue;
    59.     public float rotationSpeed = 70;
    60.     public Server server;
    61.  
    62.  
    63.  
    64.     void Start()
    65.     {
    66.         if(!IsOwner)
    67.         return;
    68.        
    69.         minTimeBetweenTicks = 1f / SERVER_TICK_RATE;
    70.  
    71.         stateBuffer = new StatePayload[BUFFER_SIZE];
    72.         inputBuffer = new InputPayload[BUFFER_SIZE];
    73.     }
    74.  
    75.     void Update()
    76.     {
    77.         if(!IsOwner)
    78.         return;
    79.  
    80.         timer += Time.deltaTime;
    81.    
    82.         while (timer >= minTimeBetweenTicks)
    83.         {
    84.             timer -= minTimeBetweenTicks;
    85.             HandleTick();
    86.             currentTick++;
    87.         }
    88.     }
    89.     [ClientRpc]
    90.     public void OnServerMovementStateClientRpc(StatePayload serverState)
    91.     {
    92.        
    93.             latestServerState = serverState;
    94.     }
    95.  
    96.     IEnumerator SendToServer(InputPayload inputPayload)
    97.     {
    98.         yield return new WaitForSeconds(0.02f);
    99.  
    100.         server.OnClientInputServerRpc(inputPayload);
    101.     }
    102.  
    103.     void HandleTick()
    104.     {
    105.         if (!latestServerState.Equals(default(StatePayload)) &&
    106.             (lastProcessedState.Equals(default(StatePayload)) ||
    107.             !latestServerState.Equals(lastProcessedState)))
    108.         {
    109.            if(!IsHost)
    110.                 HandleServerReconciliation();
    111.         }
    112.  
    113.         int bufferIndex = currentTick % BUFFER_SIZE;
    114.  
    115.         // Add payload to inputBuffer
    116.         InputPayload inputPayload = new InputPayload();
    117.         inputPayload.tick = currentTick;
    118.         inputPayload.inputVector = moveAmount;
    119.         inputPayload.rotationVector = rotationValue;
    120.         inputPayload.clientId = GetComponent<NetworkObject>().OwnerClientId;
    121.         inputBuffer[bufferIndex] = inputPayload;
    122.  
    123.         // Add payload to stateBuffer
    124.         stateBuffer[bufferIndex] = ProcessMovement(inputPayload);
    125.  
    126.             if(!IsHost)
    127.                 StartCoroutine(SendToServer(inputPayload));
    128.     }
    129.  
    130.     StatePayload ProcessMovement(InputPayload input)
    131.     {
    132.      
    133.         // Should always be in sync with same function on Server
    134.         hips.AddForce(input.inputVector * minTimeBetweenTicks);
    135.  
    136.        
    137.         hips.GetComponent<ConfigurableJoint>().targetRotation = input.rotationVector;
    138.        
    139.         return new StatePayload()
    140.         {
    141.             tick = input.tick,
    142.             position = hips.transform.position,
    143.             rotation = input.rotationVector,
    144.             clientId = input.clientId,
    145.        
    146.  
    147.         };
    148.     }
    149.  
    150.     void HandleServerReconciliation()
    151.     {
    152.      
    153.         lastProcessedState = latestServerState;
    154.  
    155.         int serverStateBufferIndex = latestServerState.tick % BUFFER_SIZE;
    156.         float positionError = Vector3.Distance(latestServerState.position, stateBuffer[serverStateBufferIndex].position);
    157.         float angle = Quaternion.Angle(latestServerState.rotation,stateBuffer[serverStateBufferIndex].rotation);
    158.         if (positionError > 0.001f || angle > .01f )
    159.         {
    160.             Debug.Log("We have to reconcile bro");
    161.             // Rewind & Replay
    162.             hips.transform.position = latestServerState.position;
    163.             hips.GetComponent<ConfigurableJoint>().targetRotation = latestServerState.rotation;
    164.  
    165.             // Update buffer at index of latest server state
    166.             stateBuffer[serverStateBufferIndex] = latestServerState;
    167.  
    168.             // Now re-simulate the rest of the ticks up to the current tick on the client
    169.             int tickToProcess = latestServerState.tick + 1;
    170.  
    171.             while (tickToProcess < currentTick)
    172.             {
    173.                 int bufferIndex = tickToProcess % BUFFER_SIZE;
    174.  
    175.                 // Process new movement with reconciled state
    176.                 StatePayload statePayload = ProcessMovement(inputBuffer[bufferIndex]);
    177.  
    178.                 // Update buffer with recalculated state
    179.                 stateBuffer[bufferIndex] = statePayload;
    180.  
    181.                 tickToProcess++;
    182.             }
    183.         }
    184.  
    185.     }
    186. }

    Server Script


    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using Unity.Netcode;
    5. using UnityEngine;
    6.  
    7. public class Server : NetworkBehaviour
    8. {
    9.  
    10.  
    11.     private float timer;
    12.     private int currentTick;
    13.     private float minTimeBetweenTicks;
    14.     private const float SERVER_TICK_RATE = 30f;
    15.     private const int BUFFER_SIZE = 1024;
    16.  
    17.     private StatePayload[] stateBuffer;
    18.     private Queue<InputPayload> inputQueue;
    19.     public Client client;
    20.     void Awake()
    21.     {
    22.         if(!NetworkManager.Singleton.IsServer)
    23.             return;
    24.    
    25.     }
    26.  
    27.     void Start()
    28.     {
    29.         if(!NetworkManager.Singleton.IsServer )
    30.             return;
    31.         minTimeBetweenTicks = 1f / SERVER_TICK_RATE;
    32.  
    33.         stateBuffer = new StatePayload[BUFFER_SIZE];
    34.         inputQueue = new Queue<InputPayload>();
    35.     }
    36.  
    37.     void Update()
    38.     {  
    39.         if(!NetworkManager.Singleton.IsServer )
    40.             return;
    41.  
    42.        
    43.         timer += Time.deltaTime;
    44.  
    45.         while (timer >= minTimeBetweenTicks)
    46.         {
    47.             timer -= minTimeBetweenTicks;
    48.             HandleTick();
    49.             currentTick++;
    50.         }
    51.     }
    52.     [ServerRpc]
    53.     public void OnClientInputServerRpc(InputPayload inputPayload)
    54.     {
    55.         inputQueue.Enqueue(inputPayload);
    56.     }
    57.    
    58.     IEnumerator SendToClient(StatePayload statePayload)
    59.     {
    60.         yield return new WaitForSeconds(0.02f);
    61.  
    62.        client.OnServerMovementStateClientRpc(statePayload);
    63.     }
    64.  
    65.     void HandleTick()
    66.     {
    67.         // Process the input queue
    68.         int bufferIndex = -1;
    69.      
    70.  
    71.            
    72.             while(inputQueue.Count > 0)
    73.             {
    74.                 InputPayload inputPayload = inputQueue.Dequeue();
    75.                 Debug.Log("Working");
    76.                 bufferIndex = inputPayload.tick % BUFFER_SIZE;
    77.  
    78.                 StatePayload statePayload = ProcessMovement(inputPayload);
    79.                 stateBuffer[bufferIndex] = statePayload;
    80.             }
    81.        
    82.             if (bufferIndex != -1)
    83.             {
    84.                
    85.                 StartCoroutine(SendToClient(stateBuffer[bufferIndex]));
    86.             }
    87.        
    88.     }
    89.  
    90.     StatePayload ProcessMovement(InputPayload input)
    91.     {
    92.      {
    93.      
    94.         // Should always be in sync with same function on Server
    95.         client.hips.AddForce(input.inputVector * minTimeBetweenTicks);
    96.  
    97.        
    98.         client.hips.GetComponent<ConfigurableJoint>().targetRotation = input.rotationVector;
    99.        
    100.         return new StatePayload()
    101.         {
    102.             tick = input.tick,
    103.             position = client.hips.transform.position,
    104.             rotation = input.rotationVector,
    105.             clientId = input.clientId,
    106.        
    107.  
    108.         };
    109.      }
    110.     }
    111. }

    Thank you!
     
  2. Hris54

    Hris54

    Joined:
    Dec 6, 2021
    Posts:
    65
    Okay so after removing this line from HandleServerReconciliation() fixed the glitching. But character is still rotating late( or is laggy) as you can see in my video
     
  3. Claytonious

    Claytonious

    Joined:
    Feb 16, 2009
    Posts:
    904
    No idea what your video is attempting to show.
     
  4. Hris54

    Hris54

    Joined:
    Dec 6, 2021
    Posts:
    65
    When I rotate my character in host( left side), it immediately rotates the character but when rotating character in client (right side) it rotates after lag
     
  5. mattseaton22

    mattseaton22

    Joined:
    Sep 12, 2019
    Posts:
    43
    I've been wondering about this also. I think responsive rotation/aiming is not compatible with client side prediction. I'm going to try just doing rotation live on the client and syncing to the server and only using CSP for movement.