Hi all!
A bit of background: I’m writing a fast-paced multiplayer game, and my netcode is a fairly honest implementation of the client-server architecture described in a number of articles floating around the internet. In short, I have a client that sends input updates to the server every tick, and on the other side, I have a server that broadcasts an authoritative game state to all clients every tick. Clients store the last received game state from the server and perform client-side prediction for a number of ticks to hide latency.
Let’s say a client presses the space bar which teleports them upwards from (1, 0, 1) to (1, 10, 1). The client will immediately act on this input via client-side prediction, and the player will see his/her avatar move up 10 whole units. The server will acknowledge the jump and send the appropriate game state back to the client; all is well.
However, I’m running into problems when the jump is initiated by the server without any input from the client. For example, assume the server decides to teleport the client up 10 units (for whatever reason). The server sends the new position to the client as part of a regular game state update on tick X. The client then receives this update but is already on tick X+2 due to network latency. The client takes the starting position of (1, 10, 1) and performs 2 ticks of client-side prediction- in particular, 2 ticks of gravity calculations. To keep things simple, let’s say our acceleration due to gravity is 1 unit/tick^2, so after 1 tick the avatar is at (1, 9, 1) and after 2, the avatar is at (1, 7, 1). Thus, the player sees a “dampened” jump of (1, 7, 1) on the very first tick instead of (1, 10, 1), and if their ping is higher, they may see no jump at all.
I’m struggling with the best way to deal with this scenario. Currently, I have a half-baked solution where the server sends down a position delta when it moves an entity without external input. This delta is updated for gravity every tick along with the moved player’s position, and the general intuition is that |player_pos| - |delta| = the player’s position as if the server never made the move in the first place. For example, the server would send down { pos: (1, 10, 1), delta: (0, 10, 0) } the first tick and { pos: (1, 9, 1), delta: (0, 9, 0) } the next. The client can then subtract the delta from the player’s position before client side prediction and re-apply when predicting tick X + 2 (the tick the client was on when it first received the move from the server).
This… seems to work (???), but it feels a bit hacky. For example, if I add another force similar to gravity, I’ll have to write logic to account for it in the delta. Before I double down on this and finish the implementation, am I fundamentally misunderstanding something? Apologies for the lengthy post, but any suggestions would be very welcome!