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.
private void FixedUpdate()
{
_inputDirection = Vector2.zero;
realDirection = Vector3.zero;
//Basically gets what the player is pressing, and by that the _inputDirection.x and _inputDirection.y
//would be 1 or -1
CalcDirectionalVector();
realDirection = transform.right * _inputDirection.x + transform.forward * _inputDirection.y;
realDirection *= moveSpeed;
//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
//working, then I didn't change.
directionToMove = new Vector3(realDirection.x, 0, realDirection.z);
//Just send a packet to the server.
ClientSend.PlayerMovement(directionToMove, inputs[4]);
//controller.Move(directionToMove);
Move();
savedMoves.Add(new saveMoves(directionToMove, DateTime.Now));
while (savedMoves.Count > 30)
{
savedMoves.RemoveAt(0);
}
}
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.
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: Client-Server Game Architecture - Gabriel Gambetta
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
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System;
public class PlayerController : MonoBehaviour
{
public float gravity = -9.81f;
public float moveSpeed = 5f;
public float jumpSpeed = 5f;
private float yVelocity = 0;
public Vector2 inputDirection;
public Vector3 moveDirection;
public bool teste;
public float latency = 0.1f;
public bool[] inputs;
CharacterController controller;
public Queue<CurrentStateAndTick> clientSendCurrentStateAndTick;
public Queue<ServerSentStateAndTick> serverReceivedCurrentStateAndTick;
private Queue<PositionAndRotationState> positionAndRotation;
private int currentTick;
public struct CurrentStateAndTick
{
public int currentTick;
public float delivery_time;
public bool[] inputs;
}
public struct PositionAndRotationState
{
public Vector3 position;
public Quaternion rotation;
}
public struct ServerSentStateAndTick
{
public int currentTick;
public float delivery_time;
public Vector3 position;
public Quaternion rotation;
}
private void Start()
{
controller = this.gameObject.GetComponent<CharacterController>();
clientSendCurrentStateAndTick = new Queue<CurrentStateAndTick>();
positionAndRotation = new Queue<PositionAndRotationState>();
serverReceivedCurrentStateAndTick = new Queue<ServerSentStateAndTick>();
currentTick = 0;
gravity *= Time.fixedDeltaTime * Time.fixedDeltaTime;
moveSpeed *= Time.fixedDeltaTime;
jumpSpeed *= Time.fixedDeltaTime;
}
private void Update()
{
inputDirection = new Vector2();
SetInput();
SetDirection();
PositionAndRotationState PR = new PositionAndRotationState();
PR.position = controller.transform.position;
PR.rotation = controller.transform.rotation;
positionAndRotation.Enqueue(PR);
Move();
if (UnityEngine.Random.value > 0.05f)
{
CurrentStateAndTick CST = new CurrentStateAndTick();
CST.currentTick = currentTick;
CST.delivery_time = Time.time + latency;
CST.inputs = inputs;
clientSendCurrentStateAndTick.Enqueue(CST);
ClientSend.PlayerMovement(CST);
}
currentTick++;
if (this.ClientHasStateMessage())
{
ServerSentStateAndTick currentState = serverReceivedCurrentStateAndTick.Dequeue();
while (this.ClientHasStateMessage())
{
currentState = serverReceivedCurrentStateAndTick.Dequeue();
}
PositionAndRotationState _PR = positionAndRotation.Dequeue();
Vector3 position_error = currentState.position - _PR.position;
float rotation_error = 1.0f - Quaternion.Dot(currentState.rotation, _PR.rotation);
this.gameObject.transform.position = currentState.position;
this.gameObject.transform.rotation = currentState.rotation;
}
}
private bool ClientHasStateMessage()
{
return this.serverReceivedCurrentStateAndTick.Count > 0 && Time.time >= this.serverReceivedCurrentStateAndTick.Peek().delivery_time;
}
private void Move()
{
Vector3 moveDirection = transform.right * inputDirection.x + transform.forward * inputDirection.y;
moveDirection *= moveSpeed;
if (controller.isGrounded)
{
yVelocity = 0f;
if (inputs[4])
{
yVelocity = jumpSpeed;
}
}
yVelocity += gravity;
moveDirection.y = yVelocity;
controller.Move(moveDirection);
}
private void SetDirection()
{
if (inputs[0])
{
inputDirection.y += 1;
}
if (inputs[1])
{
inputDirection.y -= 1;
}
if (inputs[2])
{
inputDirection.x -= 1;
}
if (inputs[3])
{
inputDirection.x += 1;
}
}
private void SetInput()
{
inputs = new bool[]
{
Input.GetKey(KeyCode.W),
Input.GetKey(KeyCode.S),
Input.GetKey(KeyCode.A),
Input.GetKey(KeyCode.D),
Input.GetKey(KeyCode.Space)
};
}
}
Server Side
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Player : MonoBehaviour
{
public int id;
public string username;
public CharacterController controller;
public float gravity = -9.81f;
public float moveSpeed = 5f;
public float jumpSpeed = 5f;
private float yVelocity = 0;
public Vector2 inputDirection;
public Vector3 moveDirection;
public Queue<InputMessage> received_input_msg;
public float latency = 0.1f;
public bool[] inputs;
int server_tick_number;
public struct StateMessage
{
public float delivery_time;
public int tick_number;
public Vector3 position;
public Quaternion rotation;
}
public struct InputMessage
{
public float delivery_time;
public int start_tick_number;
public bool[] inputs;
}
private void Start()
{
this.received_input_msg = new Queue<InputMessage>();
server_tick_number = 0;
gravity *= Time.fixedDeltaTime * Time.fixedDeltaTime;
moveSpeed *= Time.fixedDeltaTime;
jumpSpeed *= Time.fixedDeltaTime;
}
public void Initialize(int _id, string _username)
{
id = _id;
username = _username;
inputs = new bool[5];
}
public void Update()
{
inputDirection = new Vector2();
while (received_input_msg.Count > 0 && Time.time >= received_input_msg.Peek().delivery_time)
{
InputMessage input_message = received_input_msg.Dequeue();
inputs = input_message.inputs;
SetInput();
Move();
}
if (UnityEngine.Random.value > 0.05f)
{
StateMessage state = new StateMessage();
state.delivery_time = Time.time + latency;
state.position = controller.transform.position;
state.rotation = controller.transform.rotation;
state.tick_number = 0;
ServerSend.PlayerPosition(this, state);
}
}
private void Move()
{
Vector3 moveDirection = transform.right * inputDirection.x + transform.forward * inputDirection.y;
moveDirection *= moveSpeed;
if (controller.isGrounded)
{
yVelocity = 0f;
if (inputs[4])
{
yVelocity = jumpSpeed;
}
}
yVelocity += gravity;
moveDirection.y = yVelocity;
controller.Move(moveDirection);
}
private void SetInput()
{
if (inputs[0])
{
inputDirection.y += 1;
}
if (inputs[1])
{
inputDirection.y -= 1;
}
if (inputs[2])
{
inputDirection.x -= 1;
}
if (inputs[3])
{
inputDirection.x += 1;
}
}
}
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
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
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…
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.
@ep1s0de How do you simulate the player’s physics n times within a single frame though? In your line 200 of the last screen you wrote “CreateMove” and i don’t know what this does exactly. Physics.Simulate(delta) would word but it’s for an entire scene and not just for one object.
Agreed, Physics.Simulate can perform multiple simulation steps in one FixedUpdate but only on objects in the scene or physics scene of interest. From everything I’ve read while making my own multi player, this is one of the first things you have to deal with going past “lockstep”, where the server waits for all player inputs before stepping and updating the clients.
I found this thread that talked about how to isolate one player for a physics.simulate call. Some thought you could create a separate physics scene for each player, someone tested it with 100 scenes of just a cube and it bogged down beyond playability.
What I’ve found is that if you perform the physics.simulate step inside the loop of all client inputs on the server side during an update, what I observe is that the server will outpace the client with additional players. The server is constantly grabbing inputs and waiting for it’s next timed step to go, my thinking here is that there are steps where your input is not in the list when the server steps, and so you drift in the server simulation with your velocity, outpacing the client prediction of the same tick #. I think. My plan is to have the updates applied and then do a physics.simulate on the server, to prevent it running ahead of client with additional players.
What I am working on is a sort of the opposite, a way to prevent players from being updated in a physics.simulate tick. With that, I can “lock” all players except those with an input until the step is over. I’ve started running tests and hope to post results soon, but basically to lock or unlock a player I do this:
The plan is to have players in a locked state from the moment they are authenticated and join the game, after which they are only unlocked for the server physics step if that step has their input.
Why’s this the case? I thought the standard is to use timestamps, because even FixedUpdate can have anomalies in timing. Maybe I’m interpreting you incorrectly though.
Can you calculate the tick through a timestamp + server tickrate instead? I feel like that’d be a more consistent solution.
That’s a good question, I pondered the same thing myself. The conclusion I came to is that you need both a tick counter AND a timestamp. If you use only timestamps, you have to synchronize clocks between server and clients, right? But latency varies, which will screw it all up.
So here is how I’ve found you do it with a unix timestamp, like this:
public static long GetUnixTS()
{
DateTimeOffset tDTOffset;
tDTOffset = new DateTimeOffset(DateTime.UtcNow);
return tDTOffset.ToUnixTimeMilliseconds();
}
1) Server caches current unix time, then sends a ping request to clients 2) Client(s) receive ping request, package their current unix time, and send back to server 3) Server receives client reply with local unix time, and knows the time it took to send and receive 4) Server calculates that client’s latency as : Latency (ms) = ( Time of Server Receipt - TIme of Server Send ) / 2L;
Then the server to client clock offset would be
ServerUnixTimeOffset (ms) = ( Time of Server Receipt - Client time sent in reply - Latency )
Since latency is variable and not rock solid consistent, this would result in out of order message tracking by the server, which needs to send clients their last message processed in order to do reconciliation.
How can they do that if they are not 100% sure which message that is? The variable latency to me would totally throw that off.
I use timestamps to do the best clock sync possible as above, then also tag each clients’ message with a unique integer, so tracking which was the last processed by the server is not dependent upon the latency.