Hey MisterCaveman!
All clients try to stay with their ClientSimulationFrame
as close to the ServerSimulationFrame
as possible. There are always some fluctuations caused by network latency and game update rate. That’s why you might experience a situation where data from the server produced at the server’s frame #10 arrives on the client at the client’s frame #9.
You shouldn’t however rely on the exact matching frame for reconciliation. That’s because, in a real-world environment, clients could be far away from the server, so what you’ll see most of the time is, the client’s input from frame #20 arriving at the Simulator on frame, say, #23. The “gap” between client and simulator frames is dependent on the ping of both to the Replication Server.
Details aside, that means, when the client receives the state produced by the Simulator at frame #23, it should compare it with its state as of frame #20, because that’s when the client executed that input and predicted the state. One way to approach this is to make the frame at which input was executed part of the synced state:
public class Player : MonoBehaviour
{
// Synced as part of the player state
[Sync] public long InputFrame;
private CoherenceInput input;
private CoherenceSync sync;
private Vector3? serverPositionToTest;
void Start()
{
sync = GetComponent<CoherenceSync>();
input = sync.Input;
// Since we use a prediction we need to store the synced position ourselves
var positionBinding = sync.Bindings.First(b => b is PositionBinding) as PositionBinding;
positionBinding.OnNetworkSampleReceived += (sample, _) => serverPositionToTest = (Vector3)sample;
}
void FixedUpdate()
{
if (sync.HasInputAuthority)
{
// We received a position update
if (serverPositionToTest.HasValue)
{
// Get a state from the past that was saved via `StoreState`
var historicalState = GetState(InputFrame);
// Check whether the position we received matches the predicted one.
// If not, possibly resimulate the state.
TestMisprediction(serverPositionToTest.Value, historicalState);
serverPositionToTest = null;
}
bool jumped = Input.GetButton("Jump");
input.SetButton("Jump", jumped);
Simulate(jumped);
// Save the predicted position so we can compare it with the authoritative state.
StoreState(transform.position, input.CurrentSimulationFrame);
}
else if (sync.HasStateAuthority)
{
// Frame that we received. Most likely in the past in regards to local ClientSimulationFrame.
InputFrame = input.LastReceivedFrame;
bool jumped = input.GetButton("Jump");
Simulate(jumped);
}
}
// ... implementation ...
}
This way, if the Simulator, on frame #23, receives the client’s input from frame #20, value #20 will be synced together with the updated state. The client can then use this information to compare the received state with a historical state for that frame.
There are ways to tune the input buffering to make all of this more robust but they are out of the scope of this answer. There’s a great talk from one of the Overwatch developers that goes into details of all of this: prediction, input buffering, and reconciliation. Another great resource on the subject is a series of articles by Gabriel Gambetta. I highly recommend both!
On a last note, we realize that things are pretty bare-bones when it comes to the typical input-based simulation. Full support for this model is on our roadmap so stay tuned!