Search Unity

Question How would I keep inputs in lockstep?

Discussion in 'NetCode for ECS' started by HeyZoos, Jul 29, 2021.

  1. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    Assuming I had a deterministic game, can I rely on the server tick/snapshot updates of player inputs to be consistent enough to simulate on?

    For example, I have a server system that simply publishes each client's right clicks to one another.

    Code (CSharp):
    1. protected override void OnUpdate()
    2. {
    3.     var group = World.GetExistingSystem<GhostPredictionSystemGroup>();
    4.     var tick = group.PredictingTick;
    5.     var deltaTime = Time.DeltaTime;
    6.  
    7.     Entities
    8.         .ForEach((
    9.             DynamicBuffer<LocalInputCommand> inputBuffer,
    10.             ref Translation trans,
    11.             ref PredictedGhostComponent prediction,
    12.             ref PlayerInputComponent playerInput
    13.         ) =>
    14.         {
    15.             if (!GhostPredictionSystemGroup.ShouldPredict(tick, prediction)) return;
    16.        
    17.             inputBuffer.GetDataAtTick(tick, out var input);
    18.  
    19.             playerInput.Position = input.Position;
    20.         });
    21. }
    Would something like this system update consistently across the clients?

    Code (CSharp):
    1. [UpdateInGroup(typeof(ClientSimulationSystemGroup))]
    2. public class ReactToPlayerInputComponentSystem : SystemBase
    3. {
    4.     protected override void OnUpdate()
    5.     {
    6.         var serverTick = World.GetExistingSystem<ClientSimulationSystemGroup>().ServerTick;
    7.      
    8.         Entities
    9.             .WithChangeFilter<SnapshotData>()
    10.             .ForEach((Entity entity, in PlayerInputComponent inputSnapshot) =>
    11.             {
    12.                 if (!inputSnapshot.Position.Equals(default(float2)))
    13.                 {
    14.                     Debug.Log($"{entity} input snapshot updated.");
    15.                     Debug.Log($"{entity} input value is now {inputSnapshot.Position}.");
    16.                     Debug.LogError($"{entity} reacted at tick #{serverTick}.");
    17.                 }
    18.             })
    19.             .Schedule();
    20.     }
    21. }
     
    Last edited: Jul 29, 2021
  2. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    Doing some more research, they definitely do not react at the same time. upload_2021-7-29_13-17-1.png
     
  3. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    I don't quite understand what you are trying to do, but can give some general information.
    A client is presenting state from two different ticks, `ServerTick` is the current prediction target which predicted ghosts is presented at. `InterpolationTick` is the tick which interpolated ghosts is at. If the ghosts are interpolated I would expect the change to often happen at the same interpolation tick, but very different prediction ticks (ServerTick). I say often and not always because there is no guarantee that all snapshots are received by all clients and there is no guarantee that all snapshots include all entities so it will be different from time to time. You don't have any absolute determinism guarantees, only eventual consistensy.
     
    HeyZoos likes this.
  4. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    Thanks for the reply, so then should I really be using RPCs to guarantee that a client receives a message?
     
  5. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    I don't know enough about the setup you're targeting to know what will work best. The model we have does not rely on determinism or every client receiving everything, it relies on being able to recover from lost inputs because the server has authority and all clients auto correct to the server state.
    If you are trying to do something more like deterministic lockstep you need to get all the inputs in the correct order and then RPCs might be closer to what you want, or just opening your own transport connection. You'd probably need to use your own tick which does not advance until inputs from all players have arrived since RPCs do no have any guarantee about when they arrive.
    We do not support deterministic lockstep in the current package, we consider it a completely separate network model which we are planning to have a solution for at some point in the future - but right now the auth server model is our priority.
     
    Dan-Foster likes this.
  6. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    Thanks! I was struggling to get it working using the built-in server tick, it's helpful to know about the guarantees
     
  7. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    In lock step inputs are not synchronized per say.

    So first off equating determinism to fixed time step is a common misconception, and also very very wrong.

    Deterministic means given the same inputs you get the same output. Time is just an input.

    And fixed timesteps are precise but not accurate. OS ticks drift over time, and by quite a lot even over short time periods in terms of amounts that matter in games.

    StopWatch might say it took 10k ticks but it was really 9900k ticks. It's precise but not accurate. Your computer's clock can keep accurate time over months and years, but only down to a precision of 100+ms or so.

    So a multiplayer RTS game needs a single source for ticks and timestamps, it can't use local time. You can't just offset local time by a server time either, because your local time is drifting.

    Now back to inputs. The simulation or simulations are running off of a single source of inputs. So player inputs are just merged in, placed at a specific tick. And then that is sent to the simulation(s) to simulate.

    You probably don't really need determinism.


    If you just want to control inputs, go back to how OS ticks work above. Clients and server are all running on their own time, there is no such thing as client and server ticks in sync. You apply inputs as they arrive, or some time later. It's really that simple. Trying to reason about this through client/server ticks will just make this difficult to reason about, and won't provide a good solution either.
     
  8. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    Sorry I'm not 100% I digested what you're describing.

    "Now back to inputs. The simulation or simulations are running off of a single source of inputs. So player inputs are just merged in, placed at a specific tick. And then that is sent to the simulation(s) to simulate."

    "Clients and server are all running on their own time, there is no such thing as client and server ticks in sync. You apply inputs as they arrive, or some time later. It's really that simple."

    So I think what you're saying is that trying to ensure that both simulations execute things on the same "ticks" at the same time is problematic. If I just keep a buffer of inputs on the server, let the server authoritatively decide when the inputs should be executed (maybe scheduling them a bit in the future), and then broadcast those inputs to the clients. That might be sufficient enough?

    The clients all run on their own ticks? I'm worried that the client simulations will desynchronize.

    "So a multiplayer RTS game needs a single source for ticks and timestamps"

    Would that just be the server's job (or the master peer's job) in this case?

    Thank you for your feedback, this stuff has been really difficult to understand and get working.
     
  9. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    How do I address a situation where a client receives a input that should have been executed a few ticks ago? I've been keeping a history buffer of the translations and applying movement through lerping like this:

    lerp(historyBuffer[commandStartTick], command.targetPosition, currentTick - commandStartTick / totalTicksToGetToTarget); 


    But this has also been causing problems. I can't get the clients to produce the same historyBuffer exactly so the timings for when they arrive have been off.
     
  10. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
  11. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    I think you are try to misuse the the networking model here. NetCode is a server authoritative with client-prediction model.
    Not saying you can't make it work with RTS (but definitively requires some changes), however the RTS system described in the book IIRC is a lockstep model. All peers must waits to receive all the inputs from all other peers before doing the simulation step. There is no authority there (well, we can make on peer have it) and the only thing dispatched is the input for that tick. The simulation tick advance only when all input are presents.

    In NetCode, the server will not wait, will just execute the loop every XX ms, and authoritively send snapshot to replicated the ghost state. Regardless of whatever you do on client, the state of the ghosts will reconcile with the one on the server for tick X.
    The server will never rollback and rely on the fact the client will provide input for the next tick in time. If they don't, well, the input is not processed.

    To implement an RTS like the one described in book, you probably don't need to use the current NetCode package at all. You may get inspiration from it for doing certain things but you can probably rely only on Transport package and sending rpcs/commands pretty much if your simulation is kept deterministic (that is another problem by per se). If it is not deterministic (or not completely), you need to rely on the fact a master peer (or server) will send the game state to everyone to re-sync.
    If you think/wanna use netcode for that, there a lot of things you need to customise and I would suggest first to change when and how the server and the client advances the simulation ticks, otherwise is going to be really hard (if not impossible given how it currently work) to step the same tick at "relatively" same time on all peers.
     
    MintTree117 likes this.
  12. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    Btw do u mean when go with lockstep model, we should expect the response speed will not as fast as the current server authoritative with client-prediction model Netcode anymore?
     
  13. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    The one I described is the classic deterministic lockstep. There are possible variations on it that allow to reduce the necessity to wait for all the inputs and mitigate some of the issues with the latency using smart relay server.

    But with the basic lockstep model latency is largely perceived. That is why usually local input latency and smart animations are used to mask that latency out.
     
    Rlaan likes this.
  14. HeyZoos

    HeyZoos

    Joined:
    Jul 31, 2016
    Posts:
    50
    Yeah sorry I should clarify that I ended up implementing lockstep using the MLAPI over the Facepunch transport