1
\$\begingroup\$

I'm building a multiplayer FPS in the vein of Quake. This is my setup:

  • Clients send the server a "user command" on every simulation tick (which is fixed at 60 simulation steps per second)
  • User commands state which keys were pressed and the camera vectors of the player on that simulation step.
  • The server processes "user commands" from clients as soon as they arrive. This allows it to replicate the simulation as it happens on the client and leads to very nice prediction/reconciliation code.
  • The server sends clients a snapshot of the world every 50 ms (i.e 20 times per second). The snapshot contains the positions and orientation of all players in the game
  • Clients render other players by linearly interpolating between snapshots 100 ms in the past:
snapshotA.timeEmittedFromServer < client.currentTime - 100 ms < snapshotB.timeEmittedFromServer).

This setup works great except for interpolation. Due to latency jitter the following can happen:

  1. I am the server. Game has just begun. Send snapshot of the world to all clients:
S0 = { playerA: { x: 0, y: 0 } }
  1. Received user command. Player A pressed W key. Simulate it. Player A is now at { x: 5, y: 0 }

  2. Received another user command. Player A pressed W key. Simulate it. Player A is now at { x: 10, y: 0 }

  3. 50 ms have passed on the server. Time to send snapshot to all clients:

S1 = { playerA: { x: 10 y: 0 } }
  1. Clients will now linearly interpolation between S0 and S1. Other players will see Player A move from X = 0 to X = 10 over 50 ms (the time difference between when the snapshots were created)

  2. Received user command. Player A pressed W key. Simulate it. Player A is now at { x: 15, y: 0 }

  3. Received another user command. Player A pressed W key. Simulate it. Player A is now at { x: 20, y: 0 }

  4. Received another user command. Player A pressed W key. Simulate it. Player A is now at { x: 25, y: 0 }

  5. 50 ms have passed on the server. Time to send snapshot to all clients. This time the server processed 3 w-pressed commands from Player A. This is possible due to all the weirdness of a network (one of the user commands could have been delayed, among other causes).

S2 = { playerA: { x: 25 y: 0 } }

Clients will now linearly interpolate between S1 and S2. Other players will see Player A move from X = 10 to X = 25 over 50 ms (the time difference between when the snapshots were created). Since the interpolation period is 50 ms but the player moved a longer distance other clients will see it move slightly faster!

This results in incredibly annoying visual bugs where you see a player speed up and down constantly. It makes the motion not smooth.

How do games deal with this problem? I'm surprised I haven't been able to find any information about it anywhere. Is something other than linear interpolation used to make sure the movement is smooth? Do the snapshots contain more information about the 'chunk of time' over which interpolation should happen?

Thanks for your time!

\$\endgroup\$
2
  • \$\begingroup\$ Have you considered using a higher-order interpolation scheme? A cubic Bézier interpolation would allow you to set a velocity at the beginning and end of the interval and smoothly blend between them, so that you maintain C1 continuity (no instantaneous discontinuity in speed/direction) whenever new server data arrives. \$\endgroup\$
    – DMGregory
    Jun 29, 2021 at 13:37
  • \$\begingroup\$ @DMGregory thanks for the suggestion. I'll give it a try but ideally I could keep using linear interpolation. It's just so simple and based on my read-through of Quake's source code it's what they use for interpolating between snapshots. I haven't been able to figure out how they get around the problem I mentioned above. \$\endgroup\$
    – user115675
    Jun 29, 2021 at 15:46

0

You must log in to answer this question.

Browse other questions tagged .