How to make your networked game smooth as butter

"By avoiding a future arrow from hitting your present self, you ensure your past self will dodge the present arrow."

Suppose you are designing a networking system for a multiplayer game. One of the biggest factors in the quality of networking design is how smooth the game feels for its players. People are not happy when they see snapping, jittering or other unexpected non-smooth behaviours. Such things occur because of the latency over the internet, and games have to try to predict things that are unpredictable by nature (e.g. player movement). Each time the prediction is wrong, the user sees an unpleasant correction.

One way to reduce such negative effects is to try to mask the corrections, by using smoothing.

I propose another approach. We can try to rely less on information that is uncertain, and rely on what is known. The latency is still there, but we can work around it.

Imagine an internet game server with multiple players, where everyone has less than 100 ms round-trip latency (and under 50 ms single-way latency). Those are good internet conditions, but not too unrealistic for today.

That means each player can know exactly all other players have been 100 ms ago, without having to use prediction. Let's render them there. The local player still sees himself at present time, but he sees all other players 100 ms in the past, 100% smoothly (ignoring dropped packets).

We want to let players aim at enemies, and not where they think enemies are due to their current latency (ala Quake). So the server performs hit collisions with regard to what the player shooting saw. Bullets travel in present time, but they collide with players' positions 100 ms in the past.

All the above has been done before (see Half-Life 1). But here's the kicker of this post.

What if you try to dodge a bullet by doing something within 100 ms of it hitting you. With the above system, you physically can't. No action, however drastic, within 100 ms of the bullet hitting you can save you, since the bullet will still hit where you were 100 ms ago (before you took said action).

It's not that big a deal for bullets, since they travel quite fast so there's little you can do in 100 ms to change anything. We can accept that as is. But can we do better?

What if, instead of a bullet, we have a slower moving projectile like an arrow. The player might want to jump out of the harm's way just before it hits them, just to spite their opponent. With the above system, they will see themselves clearing the arrow, yet it still hits their 100 ms in the past alter-ego and the player quits in frustration.

Yes, we can do better. We take advantage of the principle that player movement is not easily predictable, but arrow projectiles are (all you need are the initial conditions). That's why we render other players 100 ms in the past, but we can try to render other player's projectiles 100 ms in the future.

Now, when you see an arrow just about to hit your face, and you duck at the last millisecond, you're actually ducking 100 ms before the real arrow will have hit you. By avoiding a future arrow from hitting your present self, you ensure your past self will dodge the present arrow. :D

Additional Notes

  • It's hard to render the arrow when it is first fired, as we have to wait to receive the information about its initial conditions. But a long firing wind-up animation helps mask this.
  • You're less likely to dodge arrows that are shot from point-blank range, or bullets that are fast. That's why it's okay if you don't see the bullet before it hits you.
  • If you think the above is a problem, consider the real-life fact that if you hear the shot that was aimed at you, nothing you do from that point on (even if you have instantaneous reaction time) will help you, as that means the bullet has already hit you. Bullets travel faster than sound, just like they do faster than UDP internet packets.

Comments

Bernardo commented 12 years ago

your post assumes the ping of a player is constant and that all players have the same ping. suppose a 16ms ping player shoots a bullet to a 100ms player. what is supposed to happen? i mean, it doesn't even make sense to say "to a 100ms player", there could a be a lot of different pings of others players around. the 100ms player is still going to receive the bullet 100ms later.

furthermore, do you realize the players and the servers most likely see different things? if the guy has 100ms ping, how can the server know he dodged the bullet at the last 5ms if it's only going to be informed about 100ms later?

Write Preview Markdown
dmitshur commented 12 years ago

Ok, I suppose I didn't mention this explicitly, but all players have synchronized (as well as networking conditions allow) logic clocks. This is what allows you to know where each other player has been 100 ms ago, no matter what your latency to the server may be (as long as it's under 100 ms, or else you'll need to extrapolate/predict).

That means each player knows the current logic time of the game. The updates from the server are time stamped, and added to a database of game state knowledge upon arrival. So a player with 10 ms latency will be using information from server updates that are 90 ms old (compared to current logic time). A player with 90 ms latency will be using server updates that are only 10 ms old. Of course, it doesn't use a server update directly. Rather, it takes all the time-relevant updates into account and interpolates between them.

Write Preview Markdown
Michal Marcinkowski commented 12 years ago

Hey this is MM, Nice thinking however :) ... You render the arrow by predicting it 100ms in the future. It works nicely for you but then you have a desynchronization between other players which are 100ms in the past. So this solution is not perfect, just better for the player.

Write Preview Markdown
dmitshur commented 12 years ago

Hey Michal, thanks for commenting.

First of all, I agree with you that this networking design isn't perfect. However, all networking designs are about making compromises. Each networking decision is a trade-off, better for one thing yet worse for another.

I'm not completely sure what you mean by "It works nicely for you but then you have a desynchronization between other players which are 100ms in the past."

Are you referring to the fact that from one player's point of view, there is 100 ms of temporal difference between another player's arrow (100 ms in future) and your local player (present time), but 200 ms of temporal difference between another player's arrow (100 ms in future) and other players (100 ms in past)?

If so, that is certainly a weakness in rendering strategy. You will see other arrows hitting other players when they shouldn't have (from your perspective), and vice versa. The server, however, has a fair view of the entire situation and makes the final hit-miss decision.

However, that is the trade-off being made here. I would imagine it's better to prioritize the accurate portrayal of the interactions between projectiles vs. local player, rather than projectiles vs. other players.

Thanks for bringing up a good point. :)

Write Preview Markdown
Anders Elfgren commented 12 years ago

Hi shurcool, just to clarify, with that explanation, are you referring to a solution similar to this?

http://altdevblogaday.com/2011/07/09/synchronous-rts-engines-and-a-tale-of-desyncs/

It doesn't sound quite like it so if you could expand on the differences that would be nice. :)

Write Preview Markdown
dmitshur commented 12 years ago

Anders,

The first half of that article mostly describes the concept of a "fixed timestep" (see Glenn Fiedler's excellent article on this, first google result). The system I describe in my original post actually uses this. The difference is that my system is not a synchronous lockstep, meaning my clients do not wait for all others to be at the same step.

The second half, where he gets into "desyncs", is largely irrelevant in my case, because I use a client/server model. The server is completely authoritative, and it keeps its clients in sync by sending out regular updates. The entire game cannot "desync" like the peer-to-peer synchronous RTS design in that article. Note that this is because I describe a more FPS-like game, with perhaps 16 or 64 players max, not large armies of units.

The article has little to do with my reply. I think it's best to give a concrete example.

Consider the point of view of client X. Let the current logic time be equal to 1000 ms. There was an update from the server, timestamped 850 ms, saying that another player Y was at location (100, 100). Another update from the server, timestamped 950 ms, said that player Y was at (200, 150).

Therefore, if we are trying to render player Y at 100 ms in the past, we can use linear interpolation and figure out that he was at (150, 125) at time 900 ms. It's as simple as that.

Write Preview Markdown
Anders Elfgren commented 12 years ago

Cool, that's about what I thought. :)

Write Preview Markdown
to comment.