You predict your own character, but other players arrive as delayed snapshots. Entity interpolation makes them move smoothly, and lag compensation makes shooting them fair despite everyone’s latency.
You predict yourself, but other players come as periodic snapshots at the send rate (say 20 Hz), which would look choppy if shown directly. Entity interpolation renders remote entities slightly in the past — a hundred milliseconds behind — and smoothly interpolates between the two most recent snapshots that bracket that render time. The result is smooth movement of everyone else, at the cost of showing them a little behind reality.
INTERP_DELAY = 0.1 # render remote entities 100ms in the past
def interpolated_position(snapshots, entity_id, now):
render_time = now - INTERP_DELAY
# find the two snapshots that bracket render_time
older, newer = bracketing_snapshots(snapshots, render_time)
if not newer:
return older.pos_of(entity_id)
t = (render_time - older.time) / (newer.time - older.time)
return lerp(older.pos_of(entity_id), newer.pos_of(entity_id), t) # smooth
# other players arrive as ~20 Hz snapshots. rendering them 100ms in the PAST
# means we always have two snapshots to interpolate BETWEEN -> smooth motion.
# trade-off: you see everyone else slightly behind "now" (fixed by lag comp next).A subtle but crucial insight: different things are shown at different times. Your own character is predicted at "now" (ahead, from your inputs); other players are interpolated in the recent past (behind, for smoothness). So when you aim at an enemy, you are aiming at where they were ~100ms ago, not where the server currently has them. Reconciling these time frames fairly is exactly what lag compensation solves.
At any instant, a client shows things at DIFFERENT times:
YOUR character PREDICTED at "now" (ahead — from your own inputs)
OTHER players INTERPOLATED ~100ms in the PAST (behind — for smoothness)
the SERVER's truth is somewhere in between, delayed by your latency.
Consequence: when you place your crosshair on an enemy, you're aiming at where
they were ~(interp delay + your ping) ago — NOT where the server has them now.
If the server checked your shot against its CURRENT positions, you'd miss
targets you clearly hit on screen. Lag compensation (next) fixes this.Lag compensation makes hit detection fair: the server keeps a short history of every entity’s past positions, and when it processes your shot, it rewinds the world to the exact moment you fired on your screen (accounting for your latency and interpolation), tests the hit against those historical positions, then returns to the present. This is why in a well-made shooter, if your crosshair was on the target, you hit — even at 100ms ping.
class LagCompensatedServer:
def __init__(self):
self.history = {} # entity_id -> [(time, pos), ...] recent
def process_shot(self, shooter, shot, client_time):
# rewind the world to when the shooter actually fired (on their screen)
rewind_to = client_time - shooter.latency - INTERP_DELAY
for target in self.entities:
past_pos = self.position_at(target.id, rewind_to) # historical pos
if ray_hits(shot.ray, past_pos, target.radius):
self.apply_damage(target, shot.damage) # fair hit!
# then continue simulating the present normally
# the server stores recent positions, REWINDS to the moment you fired, checks
# the hit against where targets WERE, then returns to now. "if it was on your
# screen, it hits" — even at high ping.Lag compensation favors the shooter, which is usually the right feel but has a famous side effect: "I got shot behind cover." A high-latency attacker fires while, on their delayed screen, you were still exposed; the server rewinds and counts the hit even though you had already ducked on your own screen. Every networked shooter tunes this balance, and understanding it explains the classic complaints players have about netcode.
Lag compensation FAVORS THE SHOOTER — mostly good, but with a cost:
the "shot behind cover" / "peeker's advantage" complaint:
a high-ping attacker sees you (delayed) still in the open, fires, and the
server rewinds + counts the hit — even though on YOUR screen you'd already
ducked behind cover. You die "behind the wall."
Every shooter tunes this balance:
- cap how far back the server will rewind (limit max compensated latency)
- clamp/penalize very high-latency players
- some games favor the VICTIM in edge cases instead
There's no perfect answer — someone's reality is always slightly wrong. Netcode
is choosing WHICH compromise feels fairest for your game.