Let’s make a multiplayer game (part 7)


In the previous article I demonstrated how latency impacts the smoothness of the gameplay experience. The deterministic part of the game simulation i.e. the asteroids are not affected as much due to the fact that their movement will follow a predictable course. The player movement however, is very unpredictable with the jittery effect being magnified by frequent player movement changes.

The way to correct the snapping of networked entities is to make use of some kind of smoothing technique. The simplest of these is to make use of Linear Interpolation. Here are some links to articles I found useful.

http://gafferongames.com/game-physics/networked-physics/. This article is really good with a LOT of useful information in the comments section.

This Gamasutra article contains a good general description of dead-reckoning.

The Source engine articles from Valve are also really useful.

You will notice that this code makes use of the ideas from Shawn Hargreaves’ Blog entry.

In my previous articles I showed how I keep three versions of the EntityState (Position, Velocity, Rotation) for each of my sprites. The reason for this is so that we can separate the display state from the simulation state. The simulation state will be used for all game logic decisions such as collision detection. The display state will be used for rendering.

In order to perform linear interpolation we will be interpolating between the previous display state (where we last rendered) and the simulation state (where the object currently is). A linear interpolation requires the from state, to state and an interpolation factor. I’m sure there are sophisticated techniques for determining the interpolation factor. My calculation is based on the fact that by default XNA calls the update method 1000ms / 60 = every 16.67ms. I reasoned that I would like the display state to reach the simulation state after 200ms meaning I would need 200ms / 16.67ms = 12 updates to achieve it. The resulting interpolation factor is therefore 1 / 12.

The following code shows the Update method for all my sprites.

public virtual void Update(GameTime gameTime)
{
    var elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;

    if (this.frameTimer.Stopwatch(100))
    {
        this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.Count;
    }

    this.SimulationState.Position += this.SimulationState.Velocity * elapsedSeconds;

    if (this.EnableSmoothing)
    {
        this.PrevDisplayState.Position += this.PrevDisplayState.Velocity * elapsedSeconds;

        this.ApplySmoothing(1/12f);
    }
    else
    {
        this.DisplayState = (EntityState)this.SimulationState.Clone();
    }
}

First I get the elapsed seconds since the last update. This value is used to advance the sprite position by it’s velocity.

The FrameTimer code is just looping between the animation frames every 200ms.

Next we use the elapsed time to update the SimulationState of the entity.

If smoothing is disabled then the DisplayState is equal to the SimulationState.

When smoothing is enabled we need to apply smoothing. Firstly we update the previous display state position based on its velocity and the elapsed time. Then we apply the smoothing based on the interpolation factor as described above.

private void ApplySmoothing(float delta)
{
    this.DisplayState.Position = Vector2.Lerp(
        this.PrevDisplayState.Position, this.SimulationState.Position, delta);

    this.DisplayState.Velocity = Vector2.Lerp(
        this.PrevDisplayState.Velocity, this.SimulationState.Velocity, delta);

    this.DisplayState.Rotation = MathHelper.Lerp(
        this.PrevDisplayState.Rotation, this.SimulationState.Rotation, delta);

    this.PrevDisplayState = (EntityState)this.DisplayState.Clone();
}

Using the Vector2.Lerp method our DisplayState values now become the interpolated value from the PrevDisplayState to the SimluationState according to the interpolation factor.

Lastly the PrevDisplayState is updated to the current DisplayState. The above code will enable a smooth linear interpolation of the DisplayState to the SimulationState within 200ms.

I have modified the Draw method to render the SimulationState when smoothing is enabled. This is purely for debugging purposes and will be disabled during actual gameplay.

The HandleUpdatePlayerStateMessage method in the game class is now as follows.

private void HandleUpdatePlayerStateMessage(NetIncomingMessage im)
{
    var message = new UpdatePlayerStateMessage(im);

    var timeDelay = (float)(NetTime.Now – im.SenderConnection.GetLocalTime(message.MessageTime));

    Player player = this.playerManager.GetPlayer(message.Id) ??
                        this.playerManager.AddPlayer(message.Id, message.Position, message.Velocity, message.Rotation, false);

    player.EnableSmoothing = true;

    if (player.LastUpdateTime < message.MessageTime)
    {
        player.SimulationState.Position = message.Position += (message.Velocity * timeDelay);
        player.SimulationState.Velocity = message.Velocity;
        player.SimulationState.Rotation = message.Rotation;

        player.LastUpdateTime = message.MessageTime;
    }
}

No major changes, except for the fact that smoothing is now enabled. I had some code referencing the PrevDisplayState. This was a bug and has been removed.

I played around with the idea of ignoring LOCAL player state updates. The effect of is a 100% match between the DisplayState and SimulationState for the local player with no snapping. The only issue is that the player will never be updated with the server values which could lead to some inconsistencies. I did however, reduce the player update heartbeat to once per second, reasoning that I could tolerate local player adjustments in high latency conditions every second.

The following video shows the effects of enabling player smoothing.

Smoothing Enabled (200ms latency)
Advertisements

4 thoughts on “Let’s make a multiplayer game (part 7)

  1. Been following your series.
    You’ve laid things out in a very simple way and I appreciate that.
    Clutter often confuses me.

    I working this into a pet project using SFML instead of XNA.
    Also trying to find a nice way to seamlessly wire it into my entity component framework.

    • glad you are enjoying it.

      unfortunately I haven’t posted anything in a while.

      maybe the bug will bite again in the near future.

  2. I am also working on something similar. 2 things that are missing for me though: when are you turning smoothing off? (EnableSmoothing = false) ? Also, what happens during sudden big changes in position? since you only smooth out the data for a fixed amount of time (200 ms). What happens, for example, if there was a big change in position while you are still in the middle of smoothing a previous state update? the position would start to move towards its end goal, then jumping (snapping) to its new target…. is there any way to overcome this ?

    • Hi Lior

      Lag is lag and I have found that you can’t avoid it. So you will always sudden changes or big jumps when the simulation becomes out of sync. To avoid big jumps you could build in a system that will play catch-up in a more smoother manner rather than snapping to the target location.

      The smoothing on/off flag is there for debugging purposes. I think it should probably always be on when you have a good strategy for getting the client’s back in sync.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s