Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Questions About Client-Side Prediction with Server Reconciliation & Lag Compensation

Discussion in 'Netcode for GameObjects' started by afavar, Jun 24, 2023.

  1. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    57
    I am trying to implement these two concepts in Netcode. I have couple questions in my mind though.

    So lets say I have a Client-Side Prediction with Server Reconciliation implementation and the server stores the player positions in a buffer. I have seen couple videos that are implementing this based on the tick and they are making the buffer size 1024. For a server with 30 tick rate, wouldn't this store approximately 34 seconds (1024/30) of history? Am I missing something here or is it a bit too much? Some games kick players when their ping is higher than 1000ms so wouldnt a 2 seconds buffer time (with a size of 60) be enough?

    If I were to also implement Lag Compensation to detect raycast hits, is it logical to use the already saved positions instead of saving another buffer for the Lag Compensation?

    Another question is about the rewinding part of the Lag Compensation. How would a position rewind work when the server is also processing the input coming from the clients? I mean lets assume a player is trying to shoot another player who is running. When the player shoots, the server has to rewind the running player, do a hit detection and reset the position. But the server is also processing the movement input from the client. Wouldn't this cause a problem when rewinding even for a quick time? Should I instead create a temp player collider at that position and destroy it after the hit detection?
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    3,899
    Only if each entry in the array is a reference to a set of historical data.
    If it's the historical data itself, such as the position + rotation + velocity of an object encoded linearly (12+12+12 = 36 bytes) then the set of data the buffer contains would be 1024/36 or 28 entries.
    Impossible to say without seeing the code.

    I would use the existing data of course.

    Since Unity is single-threaded you will do these steps in a sequential order. For example, first receive network input, then perform rewinds.

    You may want to look at the implementation of this in a similar networking framework: Fish-Net. Scale down on the wheel re-inventing part. ;)
     
  3. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    57
    Thank you for your answer, here is the code I have implemented based on this youtube video. I believe this is a reference to a set of historical data. Btw, for the testing purpose I have only implemeted the position.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Netcode;
    5.  
    6. public struct InputPayload
    7. {
    8.     public int tick;
    9.     public Vector3 inputVector;
    10. }
    11.  
    12. public class StatePayload : INetworkSerializable
    13. {
    14.     public int tick;
    15.     public Vector3 position;
    16.  
    17.     public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    18.     {
    19.         if (serializer.IsReader)
    20.         {
    21.             var reader = serializer.GetFastBufferReader();
    22.             reader.ReadValueSafe(out tick);
    23.             reader.ReadValueSafe(out position);
    24.         }
    25.         else
    26.         {
    27.             var writer = serializer.GetFastBufferWriter();
    28.             writer.WriteValueSafe(tick);
    29.             writer.WriteValueSafe(position);
    30.         }
    31.     }
    32.  
    33.     public bool Equals(StatePayload other)
    34.     {
    35.         if (other == null)
    36.         {
    37.             return false;
    38.         }
    39.         return tick == other.tick && position == other.position;
    40.     }
    41. }
    42.  
    43. public class CustomNetworkTransform : NetworkBehaviour
    44. {
    45.     // Shared
    46.     private float timer;
    47.     private int currentTick;
    48.     private float minTimeBetweenTicks;
    49.     // private const float SERVER_TICK_RATE = 30f;
    50.     private const int BUFFER_SIZE = 1024;
    51.  
    52.     // Client specific
    53.     private StatePayload[] clientStateBuffer;
    54.     private InputPayload[] inputBuffer;
    55.     private NetworkVariable<StatePayload> latestServerState = new NetworkVariable<StatePayload>();
    56.     private StatePayload lastProcessedState;
    57.     private float horizontalInput;
    58.     private float verticalInput;
    59.  
    60.     // Server specific
    61.     private StatePayload[] serverStateBuffer;
    62.     private Queue<InputPayload> inputQueue;
    63.  
    64.     void Start()
    65.     {
    66.         minTimeBetweenTicks = 1f / NetworkManager.Singleton.NetworkConfig.TickRate;
    67.     }
    68.  
    69.     public override void OnNetworkSpawn()
    70.     {
    71.         base.OnNetworkSpawn();
    72.         if (IsServer)
    73.         {
    74.             serverStateBuffer = new StatePayload[BUFFER_SIZE];
    75.             inputQueue = new Queue<InputPayload>();
    76.         }
    77.  
    78.         if (IsClient)
    79.         {
    80.             clientStateBuffer = new StatePayload[BUFFER_SIZE];
    81.             inputBuffer = new InputPayload[BUFFER_SIZE];
    82.         }
    83.     }
    84.  
    85.     void Update()
    86.     {
    87.         if (IsClient && IsOwner)
    88.         {
    89.             horizontalInput = Input.GetAxis("Horizontal");
    90.             verticalInput = Input.GetAxis("Vertical");
    91.         }
    92.  
    93.         timer += Time.deltaTime;
    94.  
    95.         while (timer >= minTimeBetweenTicks)
    96.         {
    97.             timer -= minTimeBetweenTicks;
    98.             HandleTickClient();
    99.             HandleTickServer();
    100.             currentTick++;
    101.         }
    102.     }
    103.  
    104.     [ServerRpc]
    105.     public void SendInputServerRpc(int tick, Vector3 inputVector)
    106.     {
    107.         InputPayload inputPayload = new InputPayload
    108.         {
    109.             tick = tick,
    110.             inputVector = inputVector,
    111.         };
    112.         inputQueue.Enqueue(inputPayload);
    113.     }
    114.  
    115.     void HandleTickClient()
    116.     {
    117.         if (!IsClient)
    118.         {
    119.             return;
    120.         }
    121.  
    122.         if (IsOwner)
    123.         {
    124.             if (latestServerState.Value != null)
    125.             {
    126.                 HandleServerReconciliation();
    127.             }
    128.  
    129.             int bufferIndex = currentTick % BUFFER_SIZE;
    130.  
    131.             // Add payload to inputBuffer
    132.             InputPayload inputPayload = new InputPayload();
    133.             inputPayload.tick = currentTick;
    134.             inputPayload.inputVector = new Vector3(horizontalInput, 0, verticalInput);
    135.             inputBuffer[bufferIndex] = inputPayload;
    136.  
    137.             // Add payload to clientStateBuffer
    138.             clientStateBuffer[bufferIndex] = ProcessMovement(inputPayload);
    139.  
    140.             // Send input to server
    141.             SendInputServerRpc(inputPayload.tick, inputPayload.inputVector);
    142.         }
    143.         else if (latestServerState.Value != null)
    144.         {
    145.             transform.position = latestServerState.Value.position;
    146.         }
    147.     }
    148.  
    149.     void HandleTickServer()
    150.     {
    151.         if (!IsServer)
    152.         {
    153.             return;
    154.         }
    155.         // Process the input queue
    156.         int bufferIndex = -1;
    157.         while (inputQueue.Count > 0)
    158.         {
    159.             InputPayload inputPayload = inputQueue.Dequeue();
    160.  
    161.             bufferIndex = inputPayload.tick % BUFFER_SIZE;
    162.  
    163.             StatePayload statePayload = ProcessMovement(inputPayload);
    164.             serverStateBuffer[bufferIndex] = statePayload;
    165.         }
    166.  
    167.         if (bufferIndex != -1)
    168.         {
    169.             latestServerState.Value = serverStateBuffer[bufferIndex];
    170.         }
    171.     }
    172.  
    173.     StatePayload ProcessMovement(InputPayload input)
    174.     {
    175.         // Should always be in sync with same function on Server
    176.         transform.position += input.inputVector * 5f * minTimeBetweenTicks;
    177.  
    178.         return new StatePayload()
    179.         {
    180.             tick = input.tick,
    181.             position = transform.position,
    182.         };
    183.     }
    184.  
    185.     void HandleServerReconciliation()
    186.     {
    187.         print("HandleServerReconciliation");
    188.         lastProcessedState = latestServerState.Value;
    189.  
    190.         int serverStateBufferIndex = latestServerState.Value.tick % BUFFER_SIZE;
    191.         float positionError = Vector3.Distance(latestServerState.Value.position, clientStateBuffer[serverStateBufferIndex].position);
    192.  
    193.         if (positionError > 0.001f)
    194.         {
    195.             Debug.Log("Reconcile");
    196.             // Rewind & Replay
    197.             transform.position = latestServerState.Value.position;
    198.  
    199.             // Update buffer at index of latest server state
    200.             clientStateBuffer[serverStateBufferIndex] = latestServerState.Value;
    201.  
    202.             // Now re-simulate the rest of the ticks up to the current tick on the client
    203.             int tickToProcess = latestServerState.Value.tick + 1;
    204.  
    205.             while (tickToProcess < currentTick)
    206.             {
    207.                 int bufferIndex = tickToProcess % BUFFER_SIZE;
    208.  
    209.                 // Process new movement with reconciled state
    210.                 StatePayload statePayload = ProcessMovement(inputBuffer[bufferIndex]);
    211.  
    212.                 // Update buffer with recalculated state
    213.                 clientStateBuffer[bufferIndex] = statePayload;
    214.  
    215.                 tickToProcess++;
    216.             }
    217.         }
    218.     }
    219. }
    I will also check out the Fish-Net. Thanks!