Error correction

Once the server has every player's input state, positions, and intents, it can take a lockstep turn and update the entire game world. Since at the time when an individual player makes a move, he or she only knows about what is happening in that particular client, one thing that could happen is that another player could play in their local client in such a way that there is a conflict between the two players. Maybe, there was only one fruit and both players attempted to get to it at the same time, or it is possible that another player ran into you, and you're now going to be taking some damage.

This is where the authoritative server comes into play and puts all the clients on the same page. Whatever each client predicted in isolation should now match what the server has determined so that everyone can see the game world in the same state.

Here is a classic example of a situation where network latency can get in the way of a fun multiplayer experience. Let's imagine that, two players (player A and player B) start heading for the same fruit. According to each player's simulation, they're both coming from opposite directions and headed for the fruit, which is now only a few frames away. If neither player changes direction, they would both arrive at the fruit at the exact same frame. Suppose that, in the frame before player A eats the fruit, he decided to change direction for whatever reason. Since player B doesn't get player A's updated state and position from the server for a few frames, he might think that player A was indeed going to eat the fruit, so player B's simulation would show player A eating the fruit and getting points for it.

Given the previous scenario, what should player B's simulation do when the server sends the next turn's output that shows that player A swerved away from the fruit and didn't earn any points? Indeed, the two states are now out of sync (between player B's simulation and the server), so player B should get better synchronized with the server.

Play through the intent, but not the outcome

A common way to handle the scenario that was mentioned previously is to include some sort of animation that a client can start right away based on its current knowledge of the player's intent and the current state of the game world. In our specific case, when player B thinks that player A is about to grab the fruit and earn some points, his or her simulation could start an animation sequence that would indicate that player A is about to level up by eating a fruit. Then, when the server responds back and confirms that player A didn't actually eat the fruit, player B's client can fall back to some secondary animation that would represent that the fruit was untouched.

Those of you who are fans of Halo might have noticed this in action when you attempted to throw a grenade in the game during an online session with your mates. When a client decides to toss a hand grenade in Halo, the client will inform the server about this intent right away. The server will then run a bunch of tests and checks to make sure that this is a legal move. Finally, the server will respond back to the client and inform it whether it is allowed to continue with the tossing of the grenade. Meanwhile, during this time when the server confirmed that the client could throw that grenade, the client started playing through the animation sequence that it does when a player throws a grenade. If this is left unchecked (that is, the server doesn't respond back in time), the player will finish swinging his arm forward, but nothing will be thrown, which, in this context, will look like a normal action [Aldridge, David (2011), I Shot You First: Networking the Gameplay of HALO: REACH. GDC 2011].

How close is close enough?

Another use case is that a client has the current state of the game along with the player's input information. The player runs the next turn's simulation and renders the snake at a certain position. A few frames later, the server tells the client that the snake is actually at a different position now. How do we fix this?

In situations where we need to change a player's position, it might look strange if the player launches a blue robot into the air and over a pit with spikes at the bottom, and a few frames later (after the server syncs up all of the clients), we suddenly see the robot several pixels away from where the player expected it to be. However, then again, there are cases where the adjustment needed from an update from the server is small enough so that simply teleporting the player from point A to point B is not noticeable. This would be heavily dependent on the type of game and the individual situation.

How close is close enough?

For the purpose of our Snake game, we can choose to teleport if we determine that the discrepancy between our prediction of where the snake should be, and where the server tells us the snake is, is only off by one unit in either (not both) axis, except if the head is off by one unit in both the axes but adjusting one of the axis would put us at the neck of the snake. This way, the player would only see the snake change the position of its head by one place.

For example, if our prediction puts the player's head at point (8,15), and the snake is moving from right to left, but the server's update shows that it should be at point (7,16), we would not teleport to the new point because that would require adjusting two axes.

However, if we still have the snake moving to the left and its head is now at point (8,15), and the server update puts it at point (7,15), (8,14), (8,16), (9,15), (9,14), or (9,16), we can simply teleport the head to the new point, and in the next update, the rest of the body of the snake would be repositioned, as needed.

How close is close enough?
// ch4/snake-ch4/share/snake.js

Snake.prototype.sync = function(serverState) {
    var diffX = serverState.head.x - this.head.x;
    var diffY = serverState.head.y - this.head.y;

    if (diffX === 0 && diffY === 0) {
        this.inSync = true;
        return true;
    }

    this.inSync = false;

    // Teleport to new position if:
    //   - Off by one in one of the axis
    //   - Off by one in both axes, but only one unit from the neck
    if ((diffX === 0 && diffY === 1)
           || (diffX === 1 && diffY === 0)
           || (this.pieces[0].x === serverState.head.x && diffY === 1)
           || (this.pieces[0].y === serverState.head.y && diffX === 1)
    ){

        this.head.x = serverState.head.x;
        this.head.y = serverState.head.y;

        this.inSync = false;
        return true;
    }

    // TODO: Implement interpolation error correction strategy here

    return false;
};

You will notice that teleporting could put the head of the snake over itself, which, under normal circumstances, would result in the player losing the game. However, when this happens, the game won't check for that collision again until the next frame is updated. At this point, the head will be first moved forward, which will readjust the rest of the body of the snake and thus remove any possible collisions.

Smooth user experience

Another way to adjust between the player's current position and the position set by the server is to gradually and smoothly move towards that point through the course of multiple frames. In other words, we interpolate between our current position and the position we want to get to.

The way interpolation works is simple, as explained here:

  1. First determine how many frames you want the interpolation to take.
  2. Then determine how many units you will need to move in each direction per frame.
  3. Finally, move each frame a little bit until you get to the destination point in the desired amount of frames.

Essentially, we simply move a percentage of the way towards the target point at the same percentage of the time we wish to get there. In other words, if we would like to get to the target position in 10 frames, then at each frame we move 10 percent of the total distance. Thus, we can abstract away the following formula:

a = (1 – t) * b + t * c

Here, t is a number between zero and one, which represents a percentage value between 0 percent and 100 percent (this is the current distance between the starting point and the target point).

Smooth user experience

We can implement the linear interpolation method in the snake class directly; however, the obsessed object-oriented designer inside of you might argue that this mathematical procedure might be better suited inside an entirely separate utility class that is imported and used by the snake class.

// ch4/snake-ch4/share/snake.js

Snake.prototype.interpolate = function(currFrame, src, dest, totalFrames) {
    var t = currFrame / totalFrames;

    return (1 - t) * src + dest * totalFrames ;
};

This interpolation method will take (besides the source and destination points) the current frame within the animation as well as the total number of frames that the animation will last. As a result, we'll need some way to keep track of the current frame and reset it to zero when we wish to start the animation again.

A good place to reset the interpolation sequence is in the socket callback, which is where we first learn that we might need to interpolate towards a different position.

// ch4/snake-ch4/share/app.client.js

socket.on(gameEvents.client_playerState, function(data){
    otherPlayers = data.filter(function(_player){

        if (_player.id == player.id) {
            serverState = _player;
            serverState.currFrame = 0;

            return false;
        }

        return true;
    });
});

We will then also need to update the snake class so that we can configure the maximum amount of frames that each interpolation cycle can handle.

// ch4/snake-ch4/share/snake.js

var Snake = function (id, x, y, color_hex, width, height, interpMaxFrames) {
    this.id = id;
    this.color = color_hex;
    this.head = {x: x, y: y};
    this.pieces = [this.head];
    this.width = width || 16;
    this.height = height || 16;
    this.interpMaxFrames = interpMaxFrames || 3;
    this.readyToGrow = false;
    this.input = {};
    this.inSync = true;
};

With this in place, we can now implement linear interpolation in our sync method so that the snake can interpolate smoothly to its actual position over the course of a few frames. The number of frames that you choose to arrive at the target destination can be dynamically set depending on the distance to travel, or you can leave it constant as per your game's individual case.

// ch4/snake-ch4/share/snake.js

Snake.prototype.sync = function(serverState) {
    var diffX = serverState.head.x - this.head.x;
    var diffY = serverState.head.y - this.head.y;

    if (diffX === 0 && diffY === 0) {
        this.inSync = true;

        return true;
    }

    this.inSync = false;

    // Teleport to new position if:
    //   - Off by one in one of the axis
    //   - Off by one in both axes, but only one unit from the neck
    if ((diffX === 0 && diffY === 1) ||
        (diffX === 1 && diffY === 0) ||
        (this.pieces[0].x === serverState.head.x && diffY === 1) ||
        (this.pieces[0].y === serverState.head.y && diffX === 1)) {

        this.head.x = serverState.head.x;
        this.head.y = serverState.head.y;

        this.inSync = true;

        return true;
    }

    // Interpolate towards correct point until close enough to teleport
    if (serverState.currFrame < this.interpMaxFrames) {
        this.head.x = this.interpolate(
            serverState.currFrame,
            this.head.x,
            serverState.head.x,
            this.interpMaxFrames
        );
        this.head.y = this.interpolate(
            serverState.currFrame,
            this.head.y,
            serverState.head.y,
            this.interpMaxFrames
        );
    }

    return false;
};

Finally, you will notice that, in this current implementation of our client-server setup, the client receives the exact positions of the other players, so no prediction is made about them. Thus, their positions are always in sync with the server and need no error corrections or interpolations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.77.158