Hello,
I’m creating a multiplayer game using the lockstep technique (i.e. deterministic, inputs are sent to the server, consolidated, and then back to clients). My game is similar to an RTS like WC3 / SC2 / AoE.
It’s working great so far; and, I’m now trying to reduce the latency, as my protocol has introduced ~200ms latency between the user issuing a command and it actually being executed - this is over and above network latency. I would like to remove it, and have a few ideas, but thought I would ask before undertaking them.
This is roughly how my protocol works (TCP part):
- User clicks to issue a command (e.g. tell a unit to move to a new location)
- Client sends a “command request” packet over TCP to the server
- Server takes this “command request”, combines it with requests from other clients (if there are any), gives it a sequence number and a designated “frame” (i.e. exactly which iteration of the lockstep engine it must be executed on) , and broadcasts it back to all clients
- Clients receive the commands(s), and execute them on exactly the specified frame in exactly the specified order
(note: I use my own fixed point library to guarantee determinism)
...so far so good: I get a round trip of about 10ms between the click and the command coming back from the server.
But you may notice an issue: what if the client has already proceeded past the designated “frame” of an incoming command? Presently I solve this as follows:
- A client cannot proceed to a frame unless it is certain there are no missed commands for previous frames. How?
(UDP part):
- Every 200 ms, the server broadcasts a packet containing 2 ints: (frameX, sequence number) to all clients
- This allows the clients to determine whether they have received all commands up to frameX. If they have, they commence, if not, they halt.
In order to ensure things look smooth (feel like 60fps) on the client, I need to run the client ~200ms behind.
Possible Solutions
Option 1 - More UDP
I could optimise the hell out of my UDP packet (i.e. get it down to a handful of bits) and send it ~60 times per second. (i.e. twelve times as many UDP packets)
This might work, but I’m not sure it’s a good solution. It seems like a lot of UDP packets (though this is a gut feeling rather than based on any evidence or experience). This is where some guidance would be helpful. Is it normal for games to send packets at this sort of frequency?
Option 2 - Prediction & Roll back
This is a much more sophisticated solution.
Think of it this way: most of the time, it’s just fine for the client to assume that when it hasn’t received commands in the last ~ping ms, no commands were issued and it can commence its lockstep engine forward.
The problem is that if one TCP packet is delayed by 1 ms too much, the whole thing blows up because the client has made a wrong assumption and is now out of sync with the server and other clients.
This can be solved as follows:
- Keep snapshots of the full lockstep engine state every (let’s say 0.25 sec) for the past few seconds
- When the client realises it has made an incorrect assumption (i.e. it receives a command designated for a frame it has already proceeded past), it rolls back to a past state, then steps forward again up to real time executing all the commands on the correct frame
- ...and is back in sync again
- This should only happen by exception, as it will be costly to recover. So, dynamically adjust how far behind the client runs to ensure this condition is not being hit frequently
- Note this solution requires that I take a deep copy of the state very frequently. I would probably avoid using clone for performance reasons. I would probably achieve this without any garbage collection by cycling a number of cached copies, and by implementing my own deep copy for every game entity (i.e. it's a lot of work, and an on-going effort to maintain)
Note: this has a side-effect. On the occasions when the client gets it wrong and has to roll back, the player may briefly observe something that didn't happen. E.g. see a unit die, and then it suddenly is alive again.
Option 2 - You tell me
Any other techniques I’ve not thought of? Note - I need to stick with the lockstep technique because I have a lot of state / physics etc.
Additional information:
- I’m using Libgdx & Kryonet
- I’ve written my own Fixed Point library and physics engine on top of it to guarantee determinism on all machines
- By “lockstep engine” I'm referring to the deterministic world state of the game.
- I’m using the word “frame” to refer to an iteration of the lockstep engine. This doesn’t always correspond exactly to a render frame.