Server-initiated moves and client-side prediction

Started by
4 comments, last by brkho 3 years, 3 months ago

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!

Advertisement

This is the same problem as “player got hit by a bullet/explosion/whatever initiated by another player.”

This is the fundamental problem of latency: When you play the local player ahead of the server, you will end up with corrections that “snap” the player one way or another.

You will have to figure out what specific behavior here works best for your game – immediate snap? interpolation? Add camera shake and smoke? Update the physical entity directly but render the entity in the “old” place for a bit? There's no one-size-fits-all solution, and even the same game may have different solutions for different situations (grenade explosion vs being hit by vehicle, for example.)

In general, though, when you send a correction, you will want to send the actual-position at the actual-time, and let the player re-simulate from that point in time to the current time. You can either assume that the entire world is at the “current” state and just step the players simulation forward N times from the correction, or you can be fancier and store a log of the old states of other entities, and thus re-simulate each entity based on the new player position (and with the inputs that the player already gave since then.)

enum Bool { True, False, FileNotFound };

I see. Thanks for the response! I'll do some trial and error to see what feels best.

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).

This is the crux of the problem: if your design requires that things happen to players outside their control, you're going to also want to design in mitigations for network latency.

In this particular scenario, there's a few things to consider doing. For example, any effect like this should probably also include some kind of particles/sound/UI that indicates to the player what happened to cause their unexpected jump. These visual/audial results, being only really relevant client-side, should not be interpolated away even with high network latency, so the player will always know something happened even if they only see the tail end of the simulation results for whatever reason.

A second consideration is to employ telegraphing of the effect as much as possible. That is, instead of instantly teleporting the player, instead send state that something is about to happen, and don't apply the simulation effect until slightly later. This means that bad latency will eat into the time of this telegraph rather than the actual impact in the simulation. For bonus points, you can also notify the client when the actual simulation impact will start so the client can predictively apply that as well. This actually works really well for multiplayer abilities and effects too, as the client that initiated the action can play a longer telegraph and the other clients can play a time-compressed telegraph so that the simulation results take effect nearly simultaneously on all clients, despite (varying) network latencies.

A third potential consideration is the other half of the prior suggestion: when something is done to the player simulation by the server, leave some “fadeoff" in the effect. That is, if the player is teleported up, consider having the player lose all gravity for several ticks and just hang mid-air briefly. By itself, this would mean that the player is more likely to just see a shortened “fadeoff” in the event of bad latency because the client-side prediction would eat into this hang time before gravity is applied.

These things should really all be considered together, though, along with various other considerations that might be more specific to your game.

Sean Middleditch – Game Systems Engineer – Join my team!

Thanks for the detailed reply Sean! I ended up implementing something similar to your third suggestion where I disable gravity for a little bit after a server-initiated move, but I really appreciate the idea of telegraphing the event. After fixing the server-move problem, I started designing a stun effect, and I found myself running into the same issue as in my original post. However, I now realize that I can just telegraph the stun with a sufficiently long wind-up animation. It's not instant, but it “feels” good enough.

This topic is closed to new replies.

Advertisement