Netcode Series Part 4: Projectiles (Lag Compensation and Prediction)

Nicola Geretti
5 min readAug 31, 2021

--

A whole lot of projectiles in need of hit detection.

Great, so now you know how players move and how they’re synchronized between client(s) and server. Projectiles, like players, are entities that can spawn, change their properties, and despawn.

The only difference is that they’re fully deterministic, and that makes a big difference. It means given an origin point and a vector direction, every single projectile will behave exactly the same.

Because ARMAJET is a 2.5D platformer where you can literally dodge projectiles, the game needs to consistently, and clearly, visually represent projectiles in all latency situations. Let’s dive right in.

Projectile Data

Projectiles are relatively light entities. Their main properties, carried by the net state, are:

  • Instigator ID (the player that fired)
  • Weapon ID
  • Firing time
  • Position Vector
  • Direction+Power Vector

Unlike players, which have their input/position constantly communicated to all clients, projectiles have their payload sent only when spawning in. When the projectile despawns, it means they hit static geometry, they expired, or they hit another player, which will trigger a separate damage event.

It’s common for large scale games (the Battlefield series or any Battle Royale game out there) to carry update masks that reduce the amount of updates to pack onto snapshots, based on entity type or distance from the player. Another way of culling data is to calculate the frustum viewport on the serverside as well as the distance between the player and the projectiles, to gauge whether the player can even see or hear the projectiles based on their trajectory.

In ARMAJET, we transfer less than 20 bytes once per projectile spawn, so culling this data isn’t a major concern.

Projectile Lag Compensation

Lag comp, for short, comprises a series of techniques that enable your projectiles to hit where you expect them to.

In the previous article, we covered player physics synchronization, and the fact that the client sees enemies in the past due to latency and interpolation. So if a player were to aim at a moving enemy where they are, their projectile would most certainly miss the target.

The are a multitude of ways, depending on the type of game, to remedy this issue. In our case, we implement a mix of client and server lag compensation techniques to significantly reduce player rage quits.

Client and Servers are constantly measuring the latency between them. RTT, or Round Trip Time, is the measure we use to determine the latency between client and server. It’s not determined by classic ICMP pinging as that doesn’t take into consideration application-layer delays due to packet processing, tickrate, etc., so half of the RTT gives us a good picture of how long it takes for a message to be sent, received and unpacked for processing.

This is what happens, every time you fire your projectile in ARMAJET:

  • Client presses the fire button,
  • Client locally predicts the firing of the projectile, based on known rules of the game (Is there ammo? Is it reloading? Is the player stunned? Etc.),
  • On high latency situations, the client artificially delays firing prediction, to communicate latency to the user and reduce the burden on server lag comp. More on this later.
  • When the server receives the input, it is put in a queue for simulation.
  • During each projectile simulation tick, all entities are rewinded to a certain amount of time, the projectiles are moved, collision logic is run, and all entities are then placed back where they were. All of this happens in a fraction of a ms. This rewind time is equal to:
rewindTime = inputQueueDelay + interpolationBackTime + clientLatency - clientFiringDelay;
  • interpolationBackTime and clientLatency comprise the delay the client suffers from displaying the correct opponent position.
  • clientFiringDelay also helps reduce the rewind time by placing a delay on the client’s local simulation itself, at the expense of responsiveness, which is why the higher the delay on the client, the less lag comp is required.
  • rewindTime has a maximum value cap, so players with high latency can’t hit enemies with a significant delay advantage.

This last part is very important because lag comp isn’t free. On the receiving end (the enemy’s client viewport), the paradoxical “hit behind the corner” effect is possible, where the enemy has turned a corner, and yet are still vulnerable to hitting, because lag comp rewinds their server simulation in the past to assist with the instigator’s latency. This is why shifting some of the delay onto the client with high latency helps keep the game fair for everyone.

From my experience as a pro-player (and the overwhelming customer feedback), it’s far more frustrating to not get your projectile to hit where you’re firing as opposed to getting damaged while fleeing. And if you don’t understand why you got killed, this is one of the reasons why Call of Duty invented the killcam: to unmistakably show what the server saw when you got headshotted by the guy you just called a cheater.

There are other tricks related to physics that we implement under the hood, particularly effective when there’s high jitter and latency spikes, but they go beyond the scope of this article. If you’re interested in more about this, leave a comment or shoot me a message.

Projectile Prediction

Because projectiles are fully deterministic, and their trajectory is determined by their origin position, velocity vector, and properties of the weapon they were fired from, we can safely predict their full lifetime.

When colliding with dynamic entities, however, such as other players or even other projectiles, predictions can start drifting from the real simulation, such as in cases where projectiles:

  • Stick to other players (arrows)
  • Push other players (explosives)
  • Are pushed or triggered by other projectiles (chain explosions)

The explosives with knockback are the most evil of them all. If you simulate their explosion, because the local client detected colliding with an enemy, you then also have the option of simulating pushing the enemy away. If your simulation is right, the effect is smooth and instantaneous, but rewinding when you’re wrong will look terrible, teleporting the enemy. In ARMAJET, we use a blend, where the hit is locally simulated, but the push is only applied when we receive server confirmation, which maintains the snapshot positional interpolation contract.

Matt Delbosc has a very interesting GDC talk about peer-to-peer vehicle replication to deal with vehicle collisions in Watch Dogs 2.

Today we looked at the data payload of a projectile and how client and server simulate projectiles in the interest of keeping the game fair, while assisting players in high latency scenarios.

In the next article, we’ll go over our infrastructure, what it takes to quickly and securely transport a player from matchmaking to the server in the right region, and how we scale it all.

--

--

Nicola Geretti

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