Search Unity

Help Wanted Client-side prediction and server reconciliation.

Discussion in 'Multiplayer' started by Biell2015, Jan 2, 2021.

  1. Biell2015

    Biell2015

    Joined:
    Aug 8, 2017
    Posts:
    5
    For God's sake, can someone help me?
    I've been trying to implement client-side prediction and server reconciliation (from this site https://alankydd.wordpress.com) for about a week or more now, and I'm sure that client-side prediction is working, but for for some reason, when the server returns the player's position, and the client call Reconciliation function, the player starts to teleport to another position(if i dont move the player, the player falls and stay in 0.04999995, and teleports to 0.05 sometimes. And if i move, sometimes he teleport back to 0.05).

    Here's the code.
    Code (CSharp):
    1.  
    2. private void FixedUpdate()
    3.     {
    4.         _inputDirection = Vector2.zero;
    5.         realDirection = Vector3.zero;
    6.  
    7.         //Basically gets what the player is pressing, and by that the _inputDirection.x and _inputDirection.y
    8.         //would be 1 or -1
    9.         CalcDirectionalVector();
    10.  
    11.         realDirection = transform.right * _inputDirection.x + transform.forward * _inputDirection.y;
    12.         realDirection *= moveSpeed;
    13.  
    14.         //I think that this is for not moving in y direction,I don't know if it's a good practice to do this, but if it's
    15.         //working, then I didn't change.
    16.         directionToMove = new Vector3(realDirection.x, 0, realDirection.z);
    17.  
    18.         //Just send a packet to the server.
    19.         ClientSend.PlayerMovement(directionToMove, inputs[4]);
    20.  
    21.         //controller.Move(directionToMove);
    22.         Move();      
    23.  
    24.         savedMoves.Add(new saveMoves(directionToMove, DateTime.Now));
    25.  
    26.         while (savedMoves.Count > 30)
    27.         {
    28.             savedMoves.RemoveAt(0);
    29.         }
    30.     }
    31.  
    Code (CSharp):
    1.  
    2. public void Reconciliation(DateTime ack, Vector3 _position)
    3.     {
    4.         DateTime serverTs = ack;
    5.         Vector3 position = _position;      
    6.  
    7.         savedMoves = savedMoves.Where(savedMove => savedMove.date > serverTs).ToList();      
    8.  
    9.         savedMoves.ForEach(savedMove => position += savedMove.direction);
    10.  
    11.         transform.position = position;
    12.     }
    13.  
     
  2. ep1s0de

    ep1s0de

    Joined:
    Dec 24, 2015
    Posts:
    123
    It is better to use a tick number instead of a timestamp, every fixed update must have a tick number
     
  3. Biell2015

    Biell2015

    Joined:
    Aug 8, 2017
    Posts:
    5
    Don't you have any code examples that can help me?
    And I noticed this from some code that I saw on a page that I forgot, I made a number (simply an int), to be added in each fixed update, and i store this with the current position of the player in a list (for later to know how to check the positions), but I'm still not sure what I do to reconcile the position coming from the server.
     
  4. ep1s0de

    ep1s0de

    Joined:
    Dec 24, 2015
    Posts:
    123
    It's easy to implement, there's nothing complicated...
    Here is a very good example... Based on it, I wrote my own implementation...
    https://www.codersblock.org/blog/client-side-prediction-in-unity-2018
    You just need to understand how it works and that's it...

    You can see the results on my channel
    https://www.youtube.com/channel/UCPQ04Xpbbw2uGc1gsZtO3HQ
     
    Last edited: Jan 13, 2021
  5. luke-unity

    luke-unity

    Unity Technologies

    Joined:
    Sep 30, 2020
    Posts:
    194
    One thing which could be causing the issue is that in your reconciliation code you are just adding the directions of movements which you have calculated in the past. That's not how reconciliation is often done. Usually you store the player inputs itself and not the result of the movement and then in your reconciliation code you apply those inputs again and move your CharacterController for each input individually.

    There is also an issue in how you handle time. Locally you use DateTime.now to store your position at a given time. Then you use a DateTime value received on the server to reconciliate. That won't work because you are comparing times of two different machines and disregard the networking delay between them. What you should do instead is:
    - Whenever you store inputs in your clientSide savedMoves attach an incrementing tick number to them.
    - Send that number to the server
    - On the server store the tick of the last processed input
    - Send that tick number back to the client together with the position
    - Now you can accurately reconciliate because you can map a position exactly to a value in your savedMoves

    Hope what I'm writing makes sense. Creating a working prediction/reconciliation model is definitely not an easy task. I'd also recommend to have a look at this series of articles, they explain it quite well: https://www.gabrielgambetta.com/client-server-game-architecture.html
     
    ep1s0de likes this.
  6. Biell2015

    Biell2015

    Joined:
    Aug 8, 2017
    Posts:
    5
    I read the website (https://www.codersblock.org/blog/client-side-prediction-in-unity-2018) that you told me to understand, but when I went to read it, there were things I didn't understand, for example, it had functions that are not cited or explained on the website itself. So I saw that there was a link to your github, and I went after it, and I read it and tried to do it the way it was there. But for some reason, on the server side, the player either walks faster, or there are more inputs arriving. And I remember that before I put the while loop inside the update on the server side, this problem was happening the other way around (on the client side, the player was walking more than on the server side).
    Here, what i did:

    Client Side

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System;
    5.  
    6. public class PlayerController : MonoBehaviour
    7. {
    8.     public float gravity = -9.81f;
    9.     public float moveSpeed = 5f;
    10.     public float jumpSpeed = 5f;
    11.     private float yVelocity = 0;
    12.  
    13.     public Vector2 inputDirection;
    14.     public Vector3 moveDirection;
    15.  
    16.     public bool teste;
    17.     public float latency = 0.1f;
    18.     public bool[] inputs;
    19.  
    20.     CharacterController controller;
    21.     public Queue<CurrentStateAndTick> clientSendCurrentStateAndTick;
    22.     public Queue<ServerSentStateAndTick> serverReceivedCurrentStateAndTick;
    23.     private Queue<PositionAndRotationState> positionAndRotation;  
    24.  
    25.     private int currentTick;
    26.  
    27.  
    28.     public struct CurrentStateAndTick
    29.     {
    30.         public int currentTick;
    31.         public float delivery_time;
    32.         public bool[] inputs;
    33.     }
    34.  
    35.     public struct PositionAndRotationState
    36.     {
    37.         public Vector3 position;
    38.         public Quaternion rotation;
    39.     }
    40.  
    41.     public struct ServerSentStateAndTick
    42.     {
    43.         public int currentTick;
    44.         public float delivery_time;
    45.         public Vector3 position;
    46.         public Quaternion rotation;
    47.     }
    48.  
    49.     private void Start()
    50.     {
    51.         controller = this.gameObject.GetComponent<CharacterController>();
    52.  
    53.         clientSendCurrentStateAndTick = new Queue<CurrentStateAndTick>();
    54.         positionAndRotation = new Queue<PositionAndRotationState>();
    55.         serverReceivedCurrentStateAndTick = new Queue<ServerSentStateAndTick>();
    56.  
    57.         currentTick = 0;
    58.  
    59.         gravity *= Time.fixedDeltaTime * Time.fixedDeltaTime;
    60.         moveSpeed *= Time.fixedDeltaTime;
    61.         jumpSpeed *= Time.fixedDeltaTime;
    62.     }
    63.  
    64.     private void Update()
    65.     {
    66.         inputDirection = new Vector2();
    67.  
    68.         SetInput();
    69.  
    70.         SetDirection();
    71.  
    72.         PositionAndRotationState PR = new PositionAndRotationState();
    73.         PR.position = controller.transform.position;
    74.         PR.rotation = controller.transform.rotation;
    75.         positionAndRotation.Enqueue(PR);
    76.  
    77.         Move();
    78.  
    79.         if (UnityEngine.Random.value > 0.05f)
    80.         {
    81.             CurrentStateAndTick CST = new CurrentStateAndTick();
    82.             CST.currentTick = currentTick;
    83.             CST.delivery_time = Time.time + latency;
    84.             CST.inputs = inputs;
    85.             clientSendCurrentStateAndTick.Enqueue(CST);
    86.             ClientSend.PlayerMovement(CST);
    87.         }
    88.  
    89.         currentTick++;
    90.  
    91.         if (this.ClientHasStateMessage())
    92.         {
    93.             ServerSentStateAndTick currentState = serverReceivedCurrentStateAndTick.Dequeue();
    94.  
    95.             while (this.ClientHasStateMessage())
    96.             {
    97.                 currentState = serverReceivedCurrentStateAndTick.Dequeue();
    98.             }
    99.  
    100.             PositionAndRotationState _PR = positionAndRotation.Dequeue();
    101.  
    102.             Vector3 position_error = currentState.position - _PR.position;
    103.             float rotation_error = 1.0f - Quaternion.Dot(currentState.rotation, _PR.rotation);
    104.  
    105.             this.gameObject.transform.position = currentState.position;
    106.             this.gameObject.transform.rotation = currentState.rotation;
    107.         }
    108.     }
    109.  
    110.     private bool ClientHasStateMessage()
    111.     {
    112.         return this.serverReceivedCurrentStateAndTick.Count > 0 && Time.time >= this.serverReceivedCurrentStateAndTick.Peek().delivery_time;
    113.     }
    114.  
    115.     private void Move()
    116.     {
    117.         Vector3 moveDirection = transform.right * inputDirection.x + transform.forward * inputDirection.y;
    118.         moveDirection *= moveSpeed;
    119.  
    120.         if (controller.isGrounded)
    121.         {
    122.             yVelocity = 0f;
    123.             if (inputs[4])
    124.             {
    125.                 yVelocity = jumpSpeed;
    126.             }
    127.         }
    128.         yVelocity += gravity;
    129.  
    130.         moveDirection.y = yVelocity;
    131.         controller.Move(moveDirection);
    132.     }
    133.  
    134.     private void SetDirection()
    135.     {
    136.         if (inputs[0])
    137.         {
    138.             inputDirection.y += 1;
    139.         }
    140.  
    141.         if (inputs[1])
    142.         {
    143.             inputDirection.y -= 1;
    144.         }
    145.  
    146.         if (inputs[2])
    147.         {
    148.             inputDirection.x -= 1;
    149.         }
    150.  
    151.         if (inputs[3])
    152.         {
    153.             inputDirection.x += 1;
    154.         }
    155.     }
    156.  
    157.     private void SetInput()
    158.     {
    159.         inputs = new bool[]
    160.         {
    161.             Input.GetKey(KeyCode.W),
    162.             Input.GetKey(KeyCode.S),
    163.             Input.GetKey(KeyCode.A),
    164.             Input.GetKey(KeyCode.D),
    165.             Input.GetKey(KeyCode.Space)
    166.         };
    167.     }
    168. }
    169.  
    Server Side
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using UnityEngine;
    6.  
    7. public class Player : MonoBehaviour
    8. {
    9.     public int id;
    10.     public string username;
    11.  
    12.     public CharacterController controller;
    13.     public float gravity = -9.81f;
    14.     public float moveSpeed = 5f;
    15.     public float jumpSpeed = 5f;
    16.     private float yVelocity = 0;
    17.  
    18.     public Vector2 inputDirection;
    19.     public Vector3 moveDirection;
    20.  
    21.     public Queue<InputMessage> received_input_msg;
    22.     public float latency = 0.1f;
    23.     public bool[] inputs;
    24.  
    25.     int server_tick_number;
    26.  
    27.     public struct StateMessage
    28.     {
    29.         public float delivery_time;
    30.         public int tick_number;
    31.         public Vector3 position;
    32.         public Quaternion rotation;
    33.     }
    34.  
    35.     public struct InputMessage
    36.     {
    37.         public float delivery_time;
    38.         public int start_tick_number;
    39.         public bool[] inputs;
    40.     }
    41.  
    42.     private void Start()
    43.     {
    44.         this.received_input_msg = new Queue<InputMessage>();
    45.  
    46.         server_tick_number = 0;
    47.  
    48.         gravity *= Time.fixedDeltaTime * Time.fixedDeltaTime;
    49.         moveSpeed *= Time.fixedDeltaTime;
    50.         jumpSpeed *= Time.fixedDeltaTime;
    51.     }
    52.  
    53.     public void Initialize(int _id, string _username)
    54.     {
    55.         id = _id;
    56.         username = _username;
    57.  
    58.         inputs = new bool[5];
    59.     }
    60.  
    61.     public void Update()
    62.     {
    63.         inputDirection = new Vector2();
    64.  
    65.         while (received_input_msg.Count > 0 && Time.time >= received_input_msg.Peek().delivery_time)
    66.         {
    67.             InputMessage input_message = received_input_msg.Dequeue();
    68.             inputs = input_message.inputs;
    69.  
    70.             SetInput();
    71.  
    72.             Move();
    73.         }
    74.  
    75.         if (UnityEngine.Random.value > 0.05f)
    76.         {
    77.             StateMessage state = new StateMessage();
    78.  
    79.             state.delivery_time = Time.time + latency;
    80.             state.position = controller.transform.position;
    81.             state.rotation = controller.transform.rotation;
    82.             state.tick_number = 0;
    83.  
    84.             ServerSend.PlayerPosition(this, state);
    85.         }
    86.     }  
    87.    
    88.     private void Move()
    89.     {
    90.         Vector3 moveDirection = transform.right * inputDirection.x + transform.forward * inputDirection.y;
    91.         moveDirection *= moveSpeed;
    92.  
    93.         if (controller.isGrounded)
    94.         {
    95.             yVelocity = 0f;
    96.             if (inputs[4])
    97.             {
    98.                 yVelocity = jumpSpeed;
    99.             }
    100.         }
    101.         yVelocity += gravity;
    102.  
    103.         moveDirection.y = yVelocity;
    104.         controller.Move(moveDirection);
    105.     }
    106.  
    107.     private void SetInput()
    108.     {
    109.         if (inputs[0])
    110.         {
    111.             inputDirection.y += 1;
    112.         }
    113.  
    114.         if (inputs[1])
    115.         {
    116.             inputDirection.y -= 1;
    117.         }
    118.  
    119.         if (inputs[2])
    120.         {
    121.             inputDirection.x -= 1;
    122.         }
    123.  
    124.         if (inputs[3])
    125.         {
    126.             inputDirection.x += 1;
    127.         }
    128.     }
    129. }
    130.  
     
  7. ep1s0de

    ep1s0de

    Joined:
    Dec 24, 2015
    Posts:
    123
    1. User Command (have tick, viewangle, key pressed states)


    2. We indicate the tick, the pressed keys and the viewing angle to the command ... This is done every frame since input at fixed update Input.GetKey may not work


    3. Each fixed update, we call the player movement method and send input data from UserCommand to it, then we write local data to the buffer (position, rotation, acceleration... all data affecting the movement) to compare the server and client data when receiving the player state. Then we add the user command to the buffer, i.e. we accumulate commands and send them some times per second. Well, we add +1 to the total tick of the player


    4. On the server side, we accept the command




    On the server side, there is also a buffer with commands in order to play the command every fixed update, if you play the received commands at once, the server side will be very heavily loaded (unity is single-threaded) and players can use speedhack and the player will run fast. I run 2 commands in my project for 1 update, because the client can send 2-3 commands more or less (depending on the network delay) and this gap between the movements will increase indefinitely each time, so I get rid of a large load and ensure the smoothness and accuracy of the player's movement on the server side. I just didn't have any more ideas...


    5. Checking the server data with the data in the buffer


    We get the state of the player from the server, it contains the tick number, position, etc. We look for the state in the client buffer with such a tick and compare the position, if the difference is greater than 0.01, then we replay the entire buffer for 1 update and adjust the data in the buffer.

    Why replay the entire buffer?
    Because the client does not know what commands it has lost, and the server sends states after certain moments that the client does not know exactly (the state may or may not come due to packet loss). And it is better to write to the buffer constantly and constantly compare the data and correct it when receiving it.
     
    Last edited: Feb 22, 2021
    SenseEater likes this.
unityunity