First, we need a way for the client to predict the player movement that will happen on the server. To do this we wrote a player controller class that uses player inputs to determine movement and we run it on both the client and the server. Although the server and client are running the same code, there are still many things that can cause them to diverge. Probably the most obvious is differing game state; for example, the player may have bumped into another player on the server that the client didn’t know was there. But there are other subtler things that can cause divergence. One is timesteps; if the server and client use different timesteps, the player controller can produce different results. You either need to ensure the server and client use the same timesteps, or use a client-side prediction algorithm that works independently of timesteps. We chose the latter approach for Kazap.io. Our server runs at a fixed-time step, but our client does not. But even if you use the same timesteps and the client is aware of all the game state that can affect player movement, there’s something else that can cause the client and server to diverge: floating points. Floating-point math is non-deterministic and can give different results when run on different hardware.

So we need to handle the case where the server and client diverge too far. One approach is to keep a history of your positions going back to the last time you have a server position for. When a new position arrives from the server, you test it against the predicted position for that time in your history, and if the difference exceeds some tolerance, correct the client position. To determine the new client position, you need to replay your inputs from the moment the client and server diverged, using the new game state from the server. This means in addition to keeping a history of positions, you also need a history of inputs, and, if not using a fixed-timestep, the time deltas for those inputs. Once you have the corrected position, immediately setting the player’s position to it can cause a jarring “snapping” effect. Instead, you’ll want to smoothly interpolate between the old and new positions.

Depending on your type of game this approach may work well for you. There is, however, a drawback with this method: the client is always ahead of the server. Let’s say you have 100 ms of lag and your character moves at a speed of 10 units per second. The client will see their player start moving 100 ms before they receive the server frame where they actually started moving. The client’s position will stay 100 ms ahead of the server, and at a speed of 10 units per second, 1 unit ahead of the server. In a game like Kazap.io where bullets can hit your ship, this can lead to situations where a bullet hits you when it looks like it did not. Given that we want to render the player locally at a location that’s different from the server location, this is always a possibility. But we want to reduce the separation between client and server positions to reduce the occurrence of hits that look like they should have missed. The result of our method is that it takes longer accelerate on the client so when you do reach your top speed, you’ll converge with the server location. Players with more latency will feel like they have more inertia—it takes longer for them to change their momentum.