Implementing reconciliation. Looking for the linking pin for comparing latest server state with a cached client state.

Hi, I am working on a kind of racing game (cars). I have a server authoritative setup with client side prediction. I am testing this with the replication server and simulator running locally on my PC.

I store input and state in a custom coded buffer on the client.
I subscribe to the OnNetworkSampleReceived events for position and rotation changes coming from the server.

When detecting mispredictions I compare the simulationFrame number that is given with the latest sampleData on one side, with on the other side the .NetworkTime.ClientSimulationFrame.Frame that I used to cache the state client side . Is this the correct way to compare states?
I noticed that sometimes the server gives it data for a frame before my client has stored his local state and input for that frame. So detecting mispredictions won’t work, because cached values for that frame are still zero on the client.

  1. Should I not compare the data on simulationFrame with client side with ClientSimulationFrame.Frame? Could then running the simulator running locally have anything to do with this (because 0 latency)?
  2. What is the correct linking pin to compare incoming state from the server with the cached state on the client in the past?

Thanks in advance, I love working with Coherence so far.

Hi MisterCaveman - glad you’re loving coherence so far!

From the details of your question it looks like you are well versed and have already gone over the Server-auth setup and Simulation Frame in our docs? Posting here just for reference.

Your high level description looks sound and you are using the correct link - client sim frame and simulationFrame - so there may be something in the specific of the implemenation:

  • where in your update loop are you caching the inputs based by bridge.NetworkTime.ClientSimulationFrame.Frame?
  • Specifically are you caching it after you’ve sent the inputs through your CoherenceInput component?

Perhaps dropping some psuedo code of your input loop layout may help debug?

2 Likes

Hi Brit, thanks for the reply and diving in, much appreciated.

Below is my prototype setup.

I have a script on my player object which has the following logic to cache, send and process input:

void Start()
{
_bridge.OnFixedNetworkUpdate += FixedNetworkUpdate;
}

private void FixedNetworkUpdate()
{

    if (_coherenceSync.HasInputAuthority)
    {
             long index = _bridge.NetworkTime.ClientSimulationFrame.Frame % BufferSize;
             (I'm using a circular buffer with an array of length/bufferSize 512)
        
             SetInputs();   (There I'm sending inputs through CoherencedInput with SetButton, SetAxis etc)
             AddInputBuffer(index); 
             AddStateBuffer(index);   
    }

    if (_coherenceSync.HasInputAuthority || _coherenceSync.HasStateAuthority)
    {
        ProcessInputs();
    }        
}

I have also tried to use update to collect and set the inputs and buffers (not the processing part) but that doesn’t fix the problem.

1 Like

I can’t find an edit function, so to add here. I have debug line in the AddStateBuffer function:

Debug.Log($“ADD STATEBUFFER FOR FRAME: {_bridge.NetworkTime.ClientSimulationFrame.Frame} - INDEX: {index} POS: {_rb.position} ROT: {_rb.rotation}” );

So I know at every tick what frame and position was cached. I also have a debug line in my misprediction method in my reconciliation script to tell me what the server pos is on every latest frame.

Debug.Log($"Server: {(Vector3)sampleData} on simulation Frame: {simulationFrame} ");

and then sometimes in the console this last debug line from the server for frame X is displayed before the debug line in the AddStateBuffer method for that same frame X.

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!

2 Likes

Hi Filip!

First of all, thank you for your extended reply. You’re spot on with what I’m trying to reach and I will definitely try your suggestion of making the frame# part of the synced state.

I know both the Overwatch talk and the Gambetta articles, they are very useful indeed. I can also very much recommend this blog by Joe Best-Rotheray, that covers CSP and reconciliation.

I will report back after I tried your suggestion.
And thanks again for your time and effort, I really appreciate it.

How’s your progress @MisterCaveman ?

Hi @per_coh,

I have implemented the suggestion made by Filip this weekend seems to work. To use a [sync]-ed attribute was the thing I was looking for to communicate the correct frame client side to compare with, really clever.

So for that this topic can be closed I think! Thanks @Filip!

For me the main issue now is how to solve the reconciliation part and if that is doable at all. The position and rotation of the car still divert quickly between client and server, which makes the car jitter when reconciling (with Physics.Simulate() to simulate the frames after the misprediction up until the current frame). Many reasons left that can cause this, I have to find out next. Complex stuff! :slight_smile:

2 Likes

Thanks for the update :slight_smile:
Feel free to start a new thread if a new issue pops up :+1:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.