Fast Paced Multiplayer Implementation: Smooth Server Reconciliation

Posted on March 13, 2018

Table of Contents


Welcome to the fourth article of the Fast-Paced Multiplayer Implementation series.


This article will conclude the server reconciliation technique by adding error smoothing.

Demo

Here’s the demo: https://fouramgames.com/posts/smoothing/main.html (Chrome recommended). The source is in the usual repository, in the smoothing branch.

Use the E and R keys to cause slight or large desyncs between the server and client states. Players display a green outline during server reconciliation smoothing. Use the Smoothing checkbox to toggle the technique on/off.

Outline

It’s possible that the client and server simulations diverge for a couple of reasons mentioned below. When this happens the client visually snaps to the position indicated by the server. This is jarring and it’s better to smooth this correction over a period of time instead.

Case 1: Predicted entity interactions with networked entities

Let’s pretend that we have a trap door that any player can open or close. When closed it would prevent movement through its position.

In the timeline below, the client and server states will diverge in T1. In T2 the client’s predicted movement becomes invalid. In T3 the client will detect the error and can begin correcting it.

Case 2: Floating point determinism

Differences between floating point math across CPUs can result in tiny errors that accumulate over time. Eventually they can affect the outcome of collisions for example which creates even more significant errors until it skyrockets.

I’ll refer once again to Glenn’s articles: Deterministic Lockstep and Floating Point Determinism. In summary, it’s very difficult to guarantee determinism on floats across different CPUs. I won’t even attempt to in this series. This means that client and server simulations slowly but surely drift apart.

Detection

Discrepancies can be detected on the client’s side:

  • During local prediction, store every input with the resulting physics state (simply the X position in our case)
  • On reception of the server’s response for an input, before applying reconciliation, look up the saved state for that input
  • Compare the two with a bit of leeway for floating point errors

Smoothing

Smoothing is done by separating the player’s entity’s true position from its render position (where the client is actually drawn):

  • When a desync happens, the client’s server reconciliation snaps the true player position to the authoritative server position before replaying saved inputs.
  • Smoothing behavior is activated, which will lerp the player’s display position to the true position with a given speed until they match again.
  • When there’s no desync, the display position is simply the true position.

Let’s bring this into a context with physics and collisions. This will mean that in cases where the desync causes a player to become stuck on a wall or causes him to miss a jump, the player will float through walls/floors/ceiling/etc during the client’s smooth correction to get back to the true position. It might seem counterintuitive, but ignoring collision and physics in the correction process is actually desirable, or else the player would snag on colliders and produce all sorts of irregular movement.

Code

Detect error:

reconcile(state: ServerEntityState) {
    // Set authoritative position
    this.x = state.position;

    let idx = 0;
    while (idx < this.pendingInputs.length) {
        var input = this.pendingInputs[idx];
        
        if (input.inputSequenceNumber == state.lastProcessedInput) {
            let offset = state.position - input.position;

            // Detect error
            if (Math.abs(offset) >= 0.00001) { // Epsilon
                this.error = true;
                this.errorTimer = 0.0; // Reset lerp timer
            }
        }
        
        idx++;
    }

    // Server Reconciliation. Re-apply all the inputs not yet processed by
    // the server.
    ...
}

Correct error by lerping by a constant factor over a constant amount of time:

// Called once per frame
errorCorrect(dtSec: number) {
    if (this.error) {
        // Lerp
        let weight = 0.65;
        this.displayX = this.displayX * weight + this.x * (1.0 - weight);

        // Update timer
        this.errorTimer += dtSec;

        // Have we been lerping for 0.25 seconds?
        if (this.errorTimer > 0.25) {
            this.error = false;
        }
    }
    else {
        // No error correction required
        this.displayX = this.x;
    }
}

Alternative smoothing method

Lerp becomes more aggressive over time, and the correction is finished when the positions actually match (more accurate than relying on a timer):

// Called once per frame
errorCorrect(dtSec: number) {
    if (this.error) {
        // Lerp, increasing the rate over time
        let weight = Math.max(0.0, 0.75 - this.errorTimer);
        this.displayX = this.displayX * weight + this.x * (1.0 - weight);

        // Update timer
        this.errorTimer += dtSec;

        // Check if the error has been corrected
        let offset = this.displayX - this.x;
        if (Math.abs(offset) < 0.00001) { // Epsilon
            this.error = false;
        }
    }
    else {
        // No error correction required
        this.displayX = this.x;
    }
}

Possible improvements

I’m not entirely satisfied with both smoothing methods so far, but I’ll call it quits for now.

The second smoothing method creates abrupt motion when errorTimer is reset. This is most visible at low server update rates and long desyncs.

The first method works better in this regard but treats both large and small errors with the same lerp constant. At high update rates and short desyncs its motion is inferior to the second method.

Next article

The next article will be about clock synchronization and converting to and from local and remote time. This will be key to the upcoming hack detection, remote entity extrapolation/interpolation, and other future systems.

You can subscribe to the mailing list to be notified when a new article appears.