Introduction

In lockstep multiplayer games, all clients must start executing the simulation at the same exact time. Once the server has connected with the expected number of clients, it will broadcast a “start executing the simulation at time t” message to all the clients. Each client may receive this message at a different time, but ultimately they will all start the simulation at the same exact time.

This sounds a bit easier than it is (thought it ain’t that hard).

Naive Solution

The immediate solution that may have popped into your head is to use some universal measure of time (agnostic of timezone). That way, all clients, irrespective of their local time, could start the simulation simultaneously. However, the problem with this is that the clock on each client may be slightly out of sync (by milliseconds) due to clock drift.

Clock Drift

Timers in hardware usually have an oscillating crystal that is used to keep track of how much time has passed. The crystal oscillates at some known frequency (say 32.768 kHz for a typical crystal), and the number of oscillations is counted to measure passage of time. The problem is, that the frequency of oscillation is not perfect (i.e. not exactly 32.768 kHz), there is some margin of error. This means that two timers over time will drift apart and begin showing different times. How fast they drift depends on the margin of error in their crystals.

Time Syncing

If this is true, why is it your phone and computer time don’t keep on drifting apart until they show different minutes? Because both devices periodically synchronize their clocks with a more accurate time source, such as an NTP (Network Time Protocol) server. The more frequently two devices sync with a common time source, the less drift they experience.

Time Syncing is not Good Enough for Multiplayer Games

In multiplayer games, you are dealing with extremely tight/sensitive timing constraints. If you’re lockstep simulating at 60 ticks per second, that means you only have about 1/60 or 16 milliseconds per tick. With common PC timer crystals, two computers can drift about 20 ms in 10 minutes (if they don’t sync with a common time source during that time).

So, we have to find a way to deal with, to “cancel out”, this clock drift. In other words, we have to find a way to measure the clock drift between two devices (so that we can compensate for it).

Computing Client Offset

The difference between two device’s timers is called an offset. For our two devices, let’s consider a server and a client.

You send a message from the client to the server, which sends a message back to the client. We’ll record time at each point.

\[\text{client } (t_0) \longrightarrow \text{server } (t_1) \\ \text{client } (t_3) \longleftarrow \text{server } (t_2)\]
  • \(t_0\) is the time when the client sends the message (in client time)
  • \(t_1\) is the time when the server receives the message (in server time)
  • \(t_2\) is the time when the server sends the response (in server time)
  • \(t_3\) is the time when the client receives the response (in client time)

Assume \(\theta\) is the offset between the server and the client, meaning (all equivalent):

\[t_s - t_c = \theta\] \[t_s = t_c + \theta\] \[t_c = t_s - \theta\]

Where:

  • \(t_c\) is the time on the client
  • \(t_s\) is the time on the server
  • \(\theta\) is the offset between the server and the client

Server’s receive time in client frame is:

\[t_1 - \theta\]

Thus uplink time (time for packet to go from client to server) is:

\[(t_1 - \theta) - t_0\]

Server’s send time in client frame is:

\[t_2 - \theta\]

Thus downlink time is:

\[t_3 - (t_2 - \theta)\]

If we assume that the the network is “symmetric” (uplink time is equal to downlink time):

\[(t_1 - \theta) - t_0 = t_3 - (t_2 - \theta)\]

Let’s simplify:

\[t_1 - \theta - t_0 = t_3 - t_2 + \theta\]

Rearrange to solve for theta:

\[t_1 - t_0 - t_3 + t_2 = 2\theta\]

Thus:

\[\theta = \frac{(t_1 - t_0) - (t_2 - t_3)}{2}\]

Conclusion

To find offset (theta) between the client and the server, we send RTT packets (Round Trip Time packets) and use the timestamps to compute the offset. We keep doing this for several packets and take the median (to avoid outliers).