Netcode Series Part 3: Player Movement (Input and Physics)

Nicola Geretti
5 min readAug 24, 2021

--

Your player exists in infinite parallel… simulations.

Player movement is the most important aspect of physics in an action game. Speed, acceleration, friction, and each impulse applied defines the feel for the game.

With ARMAJET, we spent a ton of time defining what makes player movement responsive, yet challenging enough to maneuver in the right conditions. And a lot of that has to do with networking in a server-authoritative game.

Input Prediction

All clients send a ring buffer of compressed player inputs to the server at regular intervals. Some games send these as fast as the framerate allows.

A ring buffer means that the client doesn’t just send the latest inputs pressed, but the last several inputs, delta compressed, so if (when) there is packet loss, the server can reconstruct tthe input queue and avoid data gaps where it doesn’t know what the player did.

Because the game runs on the server and sends regular updates to all clients, there is an inherent delay from when the player presses a button, when the player actually moves, and when all other clients receive player’s inputs to render animations.

There isn’t much we can do about the delay in rendering other player’s animations, but for our own player, we can predict what’s about to happen.

Input prediction bridges the gap by running the game simulation instantly for the local player. When the client receives Gamestate updates from the server, the snapshot includes the player’s own inputs. If the local and the server simulation match, everyone’s happy. If they don’t, you get what’s called a de-sync, and the client smoothly lerps (linearly interpolates) to the server’s simulated position.

Small de-syncs happen frequently. They’re normal and expected, especially in a non-deterministic physics simulation. On the client, they trigger context synchronization, and one technique to run corrections smoothly is called rollback.

Client’s Context Synchronization (Rollback)

The engine takes care of context synchronizations constantly. It corrects small offsets smoothly, while larger de-syncs caused by network interruptions or high congestion can trigger the infamous rubber-banding.

Rubber banding happens when positions are incorrectly predicted, and the simulation needs to roll back the position because the server played a different set of inputs than the expected one. The simulation on the server therefore does not match the player’s local one.

So, for instance, if the player is stopped, and starts running, but the server doesn’t receive inputs showing that the player started running, it will be stopped on the server until the new inputs are received. When the client re-synchronizes, it’ll be warped back, where it actually is (remember, the server is the authority!).

In most cases though, synchronizations happen without the player noticing, and they manifest themselves in almost imperceptible speedups or slowdowns. This is how it all unfolds:

  • Client sends inputs up to the server and instantly simulates the movement. We’ll call the local simulation the primary context.
  • Server plays the inputs and simulates the movement, then sends back down to the client a snapshot containing the latest player input played.
  • If the client detects the server has played a different input than the local one, or there is a significant positional divergence, a de-sync is triggered.
  • During the de-sync, the client continues running the local simulation on the primary context, but also starts simulating a secondary context. This context is the result of the last known correct player state (right before the point of divergence), plus all local inputs simulated up to the last. The result is the final, correct position to the best of the client’s knowledge.
  • Now, we can transition from the primary context to the secondary context over a short amount of time, all while still simulating both.

This approach allows for a smooth transition, constantly adjusting for unexpected changes in position, velocity or inputs.

Many different types of games can apply rollback networking successfully. In recent years, fighting games such as MK11 and Smash have jumped on the bandwagon and adapted GGPO, a popular rollback SDK for peer-to-peer games, much to the fighting community’s satisfaction.

Client’s Input extrapolation and buffering

So we covered how we move our own player and synchronize it with the server. What about everyone else?

Snapshots deliver all the necessary positional deltas. We move other players to the positions, play animations based on inputs pressed, and interpolate over time to make them look smooth. Because our physics are not fully deterministic, we don’t simulate other player movement with inputs and real physics, which saves a lot of CPU time on the client (remember, ARMAJET was built to run at 60fps on an iPhone4!).

Regardless, issues arise when there are delays in snapshot processing and the client is left with no data to process.

So what do we do? Stop the player and resume when we have the data? Here are 2 ways to combat that:

  • We buffer snapshot and delay player’s rendering. The delay is called Interpolation back time. A high value increases jitter tolerance on unreliable connections, but it also tells the client an ugly truth: the enemies are way past where they seem to be. Try hitting someone that’s rendered 200ms in the past when boosting off a jumper. This effect is what forces players to “lead the shot” in order to hit an enemy in certain shooters.
  • As soon as there’s data missing, we start a physics simulation, repeating the last player inputs for a period, similar to the local player’s simulation. This is called Input extrapolation. Allowing extrapolation means the client is now guessing where the enemy is headed. Over the course of a couple of frames, it’s likely that the opponent has not changed their course, predicting correctly. When doing it for longer, it’s most certainly headed towards de-syncs and the eventual enemy’s positional warping.

Our solution? A little bit of both. We keep a static amount of interpolation back time, something that requires a light amount of projectile lag compensation (more on this on the next article), and run input extrapolation on the more rare occasions where downstream connections are being spotty.

The result is latency tolerance of 150–200ms worth of jitter in most circumstances without any discernible visual glitch.

Server’s Input extrapolation and buffering

The same logic is applied to the Server. The server holds an input buffer, which delays input processing to allow for constant, smooth, uninterrupted simulation.

Unlike the client’s static Interpolation back time, we have implemented an adaptive input buffer, that changes based on the client’s connection quality. In short, if the client’s connection is fairly stable, there is little to no buffering, making firing and moving run almost immediately once received. As instability kicks in, the buffer increases, allowing the client to recover at the cost of some processing latency.

When the buffer runs out, extrapolation is applied for a short period, repeating the last player’s inputs just like the client does.

While there’s much more under the hood, these are the main techniques used to maintain smooth player positioning keeping in mind packet loss, latency and jitter.

In the next article, we’ll go over how projectiles work and what lag compensation is, an important technique to make the clients hit their targets where they see them on a variety of network conditions.

--

--

Nicola Geretti

Unreal Tournament world champ turned 🎮 game developer. Living the dream, fueled by multiplayer and love for engineering at scale.