Search Unity

Understanding the tick/time synchronization in the FPS Sample Game

Discussion in 'FPS.Sample Game' started by CPlusSharp22, Jul 31, 2019.

  1. CPlusSharp22

    CPlusSharp22

    Joined:
    Dec 1, 2012
    Posts:
    111
    Hello,

    I'm trying to follow a similar approach to my own Unity project with the tick synchronization that is used for client prediction of commands.
    My understanding is that:
    1. The game has started, you've received a packet from the server with world state and the tick it is currently at when it sent that packet.
    2. You then set your own tick counter to that plus half RTT and some offset - this is an approximation of the server's current tick.
    3. Now when you send a command on this tick, you can reasonably expect that the game server will process the command sometime in the future.
    However, if your approximation is off or you slowly drift away from the real server tick - you run the risk of the server ignoring your commands if you fall behind.

    Eg. You assumed the server would be at tick 9 when it gets a jump command you send. However, the server is at tick 10, it will ignore the jump command meant for tick 9.

    There is an idea in the implementation where if the client falls too far behind or too far ahead, it will do a "hard reset". How is this supposed to work? In my implementation, following Unity's, what ends up happening is the hard reset brings my client back to the server's tick, but then the server will receive duplicate commands.

    Eg I'm at tick 10, the server is at tick 5. Woah, we're too far ahead. So, roll back to tick 6.
    The problem? I've already sent commands for tick 6, 7, 8, 9. So now I'll be sending those commands again. The server is going to reject the new commands in favor of the old ones. Is this how it's supposed to work? Or should I disregard the old commands in favor of the new commands?

    Here's some code that I have:
    Code (CSharp):
    1.  void FixedUpdate()
    2.         {
    3.             if (LastServerTick == 0)
    4.             {
    5.                 Log("ClientFramework: Waiting for first server snapshot.");
    6.                 return;
    7.             }
    8.             uint prevTick = CurrentTick;
    9.             CurrentTick++;
    10.             int preferredBufferedCommandCount = 3;
    11.             uint rtt = 60; // assuming 60ms round trip time for baseline.
    12.             int offsetTick = (int) ((((realtimeSinceStartupInSec * 1000) - SnapshotReceiveTimeInMs + rtt) / 1000.0f) * Time.fixedDeltaTime) + 1;
    13.             uint preferredTick = (uint)(LastServerTick + offsetTick + preferredBufferedCommandCount);
    14.             Log($"CurrentTick {CurrentTick} Last Server Tick Ack {LastServerTick} Trying delta {offsetTick} for preferred {preferredTick}");
    15.             bool resetTime = false;
    16.             if (!resetTime && CurrentTick < preferredTick - 3)
    17.             {
    18.                 Log($"Client hard catchup {CurrentTick} : {preferredTick}");
    19.                 resetTime = true;
    20.             }
    21.             if (!resetTime && CurrentTick > preferredTick + 6)
    22.             {
    23.                 Log($"Client hard slowdown. {CurrentTick} : {preferredTick}");
    24.                 resetTime = true;
    25.             }
    26.             if (resetTime)
    27.             {
    28.                 CurrentTick = preferredTick;
    29.             }
    30.             if (CurrentTick > prevTick)
    31.             {
    32.                 FixedUpdateTick();
    33.             }
    34.             else
    35.             {
    36.                 Warning($"Skipping tick {CurrentTick} > {prevTick}");
    37.             }
    38.         }
     
  2. AggressiveMastery

    AggressiveMastery

    Joined:
    Nov 19, 2018
    Posts:
    206
    Did you see this youtube, I just want to be sure, as it goes into this.,.. and is the only training I think we have for it :)

    If it sends you to any specific time, don't mind that, watch the whole thing :)

    I believe client send "move" commands 60 times a second to the server, but these commands are just "move forward" or "move left" not "a am at x/y/x" as the server is authoritative.

    The client is receiving packages from the server, telling it where everything is, 15-20 times a second.

    The client uses the packages from the server to true up its client-side predictions, basically snapping back every 15-20 updates on the client side, to where everything is. Depending how good the client's predictions are, results in how much snapping you see in the moveable objects.

    So when the client sends "move forward" it doesn't matter what time it was sent, the server just tries to move the character forward up to the limited amount of requests to move forward within an update cycle. So 60 move forwards max, per second as expresses as server ticks.

    Like you said, if you send requests/packets in excess or in the future, then past requests show up, I believe they are discarded. So if packets got delayed on the internet, and arrived well out of order, you most likely would see jerkiness due to a lack of move forward requests, instead of a stack of them being accepted and processed as a big jump forward.

    GL HF!
    Micah
     
    Stexe likes this.
  3. CPlusSharp22

    CPlusSharp22

    Joined:
    Dec 1, 2012
    Posts:
    111
    Yep I've seen it and another talk that covered the same subject (theres 2 variations of it)
    What I don't understand is how the client is keeping in sync with the server's ticks so it can do a prediction of the time it sends forward.

    For example,
    Server is at tick 20, it sent a world state.
    1/2 RTT later the client receives it and says "ok, server was at tick 20, it's probably at tick 24 now. If we want to send a command to move, we need to send a command marked for tick 28"

    When a "hard slowdown" occurs, the client has to reset its tick because its too far ahead from the server. However, if it starts sending commands, it will be effectively "overwriting" commands it did prior. For example, client sends a command move forward at tick 28. Hard slowdown brings us back to tick 24. What happens when the client sends another command at tick 28? Does it just get rejected or is the client not running those ticks and waiting until tick 29?

    Right now, I think it means that the client doesn't run ticks it already ran if it has a hard slowdown but I'm not positive. So once it sends a command at tick 28 in the example, it won't run tick 28 again if it slows down back to tick 24 (it won't even run 24, 25, 26, 27, 28, as it's waiting)
     
  4. AggressiveMastery

    AggressiveMastery

    Joined:
    Nov 19, 2018
    Posts:
    206
    I think the client is sending commands on your actions (player/human) and not predictions. So you say "ahh i need to move this way" based on what is updated to you on the client side. It sends forward the commands to the server. You are right, there is some push and pull with prediction, but the server packets set that tone when they hit the client.

    So as a player/client, you are reacting much slower than the client-side prediction is, and the client prediction is just rendering an image for you/human to respond to.

    It is more of a shoot and forget layout as I look at it. The client is shooting out requests, looking at the incoming packets and updating its screen. The request being sent out are separate from the prediction, as the client (you/human) sends the requests off that data(move requests) they see, which is the client predicting the state and displaying it to them. When the client prediction is 'bad' for a fraction of a second, I guess the worst case, is that the human sends the wrong move request.

    How the packets hit the server, again, 60 times a second, means if you don't have a substantial flaw in your communication and prediction methods, pure brute force should win out. So if 1 or 10 of 60 each second arrive late, you still get 50 inputs to blend over for 60 seconds.

    So if you limit say 20 'move forward's a second, and get 30 move forward requests from the client, it would limit to that max of 20, and not allow a speed hack.

    So to summarize, The client and player are two different things, the client-app is a presenter of the input sent down from the server 20 times a second. However, the client can do predictions to smooth that 20 inputs into whatever the frames are for the client. That is trued up with each packet that comes down from the server as well.The client uses the human/player input to adjust its client side predictions (aka "ok hes asking to move forward, I will predict and show he gets to move forward, and if I am wrong, the next server update will correct me" is the prediction "thought process " of the client app.)

    The client is being viewed and used by a player/human, who is sending request via that client based on the view he/her sees to move/shoot/ect at 60 times to the server. The server accepts these packets, looks at the time stamp in them, and moves forward a counter that ensures if a late packet shows up, and its timer/count is from before the current time, it is ignored. In this way, only 'current move request on the server, then move the server objects, that is then sent back as 'authoritative placement data to each client to then be trued-up-to aka synced :)

    I really appreciate you digging into this code! I hope I can help spur further discussion! If I repeated too much vs what you know @Omega, its mainly also for future reference of others hitting this forum :) -- AND so you can correct me if I am out of touch :D

    Cheers
    Micah
     
    Last edited: Aug 2, 2019
  5. AggressiveMastery

    AggressiveMastery

    Joined:
    Nov 19, 2018
    Posts:
    206
    I keep thinking about the Jellow, which I think is the client side prediction. I think it is trying to predict the server tick, but only has the client inputs, and the server updates 20 times a second to do so. So it predicts and displays a little ahead of where the player is based on that. I think server 'corrections' are then done to the prediction's source updates, so it may smooth the snap, as it moves forward to the current client frame.

    I remember thinking "ok thats a bit extreme - how much lag are we dealing with... ill deal with it later" when watching it.