Multiplayer games present complexities absent from single-player development, making them more time-consuming and expensive to create.
For server-authoritative online games, developers must implement client-side prediction and smoothing to ensure responsive player movement across varying latency conditions.
The fundamental approach involves clients responding to player input by moving characters locally before server confirmation arrives. This creates potential divergence between client and server positions. The basic challenge: preventing these states from drifting too far apart while maintaining gameplay responsiveness.
Multiple implementation variants exist, each suited to different game types. The article demonstrates the approach used in Kazap.io, a 2D space-shooter multiplayer game.
Key Technical Considerations
The implementation requires running identical player controller code on both client and server. However, several factors cause divergence:
- Differing game state awareness
- Timestep differences between systems
- Floating-point non-determinism across hardware
The solution involves maintaining position and input history spanning the round-trip latency period. When server updates arrive, clients compare predicted positions against actual server data. Exceeding tolerance triggers position correction through input replay using updated server state.
Rather than instantly snapping to corrected positions, smooth interpolation prevents jarring visual discontinuities.
The Kazap.io Algorithm

The article introduces a specific technique reducing client-server position separation. Instead of clients perpetually leading servers, this method gradually converges toward server positions during movement.
Key variables tracked: position, rotation, velocity, input history, and time deltas.
Upon receiving server updates, the system removes historical frames until the remaining history duration equals round-trip latency. Position and rotation deltas are summed and added to server state, producing predicted position.
Velocity divergence exceeding tolerance triggers input replay, recalculating deltas using actual server velocity.
Each client frame executes the player controller, generating position deltas added to history. The predicted position extrapolates using adjusted velocity multiplied by latency and convergence constant (0.05 in Kazap.io).
Client position then interpolates toward the extrapolated position, controlled by latency and convergence parameters.



Pseudo Code Implementation
// Called when we receive a player state update from the server.
function OnServerFrame(serverFrame)
{
// Remove frames from history until its duration is equal to the latency.
dt = Max(0, historyDuration - latency);
historyDuration -= dt;
while (history.Count > 0 && dt > 0)
{
if (dt >= history[0].DeltaTime)
{
dt -= history[0].DeltaTime;
history.RemoveAt(0);
}
else
{
t = 1 - dt / history[0].DeltaTime;
history[0].DeltaTime -= dt;
history[0].DeltaPosition *= t;
history[0].DeltaRotation *= t;
break;
}
}
serverState = serverFrame;
// If predicted and server velocity difference exceeds the tolerance,
// replay inputs.
if ((serverState.Velocity - history[0].Velocity).Magnitude >
velocityTolerance)
{
predictedState = serverState;
foreach (frame in history)
{
newState = playerController.Update(predictedState,
frame.DeltaTime, frame.Input);
frame.DeltaPosition = newState.Position -
predictedState.Position;
frame.DeltaRotation = newState.Rotation -
predictedState.Rotation;
frame.Velocity = newState.Velocity;
predictedState = newState;
}
}
else
{
// Add deltas from history to server state to get predicted state.
predictedState.Position = serverState.Position;
predictedState.Rotation = serverState.Rotation;
foreach (frame in history)
{
predictedState.Position += history.DeltaPosition;
predictedState.Rotation += history.DeltaRotation;
}
}
}
// Called every client frame.
function Update(deltaTime, input)
{
// Run player controller to get new prediction and add to history
newState = playerController.Update(predictedState, deltaTime, input);
frame = new Frame(deltaTime, input);
frame.DeltaPosition = newState.Position - predictedState.Position;
frame.DeltaRotation = newState.Rotation - predictedState.Rotation;
frame.Velocity = newState.Velocity;
history.Add(frame);
historyDuration += deltaTime;
// Extrapolate predicted position
// CONVERGE_MULTIPLIER is a constant. Lower values make the client
// converge with the server more aggressively. We chose 0.05.
rotationalVelocity = (newState.Rotation - predictedState.Rotation) /
deltaTime;
extrapolatedPosition = predictedState.Position + newState.Velocity *
latency * CONVERGE_MULTIPLIER;
extrapolatedRotation = predictedState.Rotation + rotationalVelocity *
latency * CONVERGE_MULTIPLIER;
// Interpolate client position towards extrapolated position
t = deltaTime / (latency * (1 + CONVERGE_MULTIPLIER));
clientState.Position = clientState.Position +
(extrapolatedPosition - clientState.Position) * t;
clientState.Rotation = clientState.Rotation +
(extrapolatedRotation - clientState.Rotation) * t;
predictedState = newState;
}
Conclusion
Client-side prediction represents one latency compensation technique among several. For additional strategies particularly relevant to first-person shooters, Valve’s documentation on latency compensation techniques is an excellent resource.
Written by Alyosha Pushak, Senior Developer at KinematicSoup.