Unreal Engine - A non-destructive and better synced network clock
In this brief but necessary post we explore a non-destructive approach to a better network synced clock for Unreal Engine.
Introduction
Time is very relevant in multiplayer, as it can provide a clear insight of when an event happened. It not only helps to provide a more accurate view of our stateful systems across the network (when was this object interacted with?), but it is also very significant when it comes to action validation (ie: rewinding).
The main difficulty in this topic is to achieve a Client clock that matches the Server clock as much as possible. With such clock we can calculate the time it takes for an event to reach the server and vice-versa (also known as round-trip time (RTT)).
Unreal tries to solve this problem providing the GetServerWorldTimeSeconds()
function, however, many users have found this clock to be inaccurate for their use cases, while others find its functionality fine.
The new clock
Replacing the old network clock has already been explored, but, as mentioned above, it is not ideal for everyone.
For that reason, this post provides a non-destructive implementation you can add to your projects while leaving the vanilla clock intact.
How does it work?
The new clock employs PostNetInit
in order to create a constant timer that updates the client clock at a constant rate (ie: every second) calling RequestWorldTime_Internal
. This function sends a server RPC forwarding the client time, which then pongs back to the client alongside the server time to calculate the ServerWorldTimeDelta
in ClientUpdateWorldTime
employing a NTP formula to adjust the client’s local clock appropriately.
Implementation
There are many approaches to implement this clock, in this article I provide two:
- Based on shortest round-trip-time: It reduces the error by an order of magnitude versus the vanilla clock in hazardous conditions. In this case, the client clock will be based on statistical outliers, as it uses the shortest round-trip time to adjust clocks. This will make the final result less accurate with the benefit of a very simplistic O(1) implementation.
- Based on a circular buffer discarding outliers: It reduces the average error versus the shortest round-trip-time approach. To do this, it employs a circular buffer to hold the latest n round-trip-times and computes an average over them discarding possible outliers to compute the “fairest RTT”. This algorithm has a higher complexity but provides a more accurate end-result.
Based on shortest round-trip-time
First, we inherit APlayerController
and add the following in the header file:
Then, we implement our clock in the cpp
of our APlayerController
:
Based on a circular buffer discarding outliers
First, we inherit APlayerController
and add the following in the header file:
Then, we implement our clock in the cpp
of our APlayerController
:
In this method there are a couple of magic numbers that have been selected empirically considering the trade-off between performance and accuracy. In this case, our circular buffer has 10 entries and we discard the biggest and smallest numbers (20%) from it (treating them as outliers) to compute the RTT average. I encourage the reader to tweak these magic numbers and the clock update frequency until you get your desired results!
And that’s it, with this now you have a more accurate and non-destructive synced network clock.
QOL functions
As an extra, I recommend adding these two functions to your Blueprint static function library:
Using the new synced clock static functions is as easy as follows:
Results
After the first complete sync-up, the new network clocks provide a more accurate view of the server time than the native one, reducing drastically the deviation on high ping scenarios. The following example log was recorded on the Third Person Template with the following Network Emulation settings:
- Emulation Target: Everyone
- Network Emulation Profile: Bad
Results:
Experiment # | Server time | Vanilla network clock | Vanilla network clock error | Shortest round-trip-time clock | Shortest round-trip-time clock error |
---|---|---|---|---|---|
1 | 8.607961 | 8.257961 | 0.3500 | 8.569201 | 0.0388 |
2 | 14.769416 | 14.371296 | 0.3981 | 14.774051 | 0.0046 |
3 | 6.552305 | 6.215758 | 0.3365 | 6.513541 | 0.0388 |
4 | 4.971255 | 4.673519 | 0.2977 | 4.93249 | 0.0388 |
5 | 5.075128 | 4.772511 | 0.3026 | 5.036363 | 0.0388 |
Experiment # | Server time | Vanilla network clock | Vanilla network clock error | Circular buffer clock | Circular buffer clock error |
---|---|---|---|---|---|
1 | 19.079962 | 18.424429 | 0.6555 | 19.077688 | 0.0023 |
2 | 32.913425 | 32.289417 | 0.6240 | 32.925301 | 0.0119 |
3 | 10.613771 | 10.11378 | 0.5000 | 10.637333 | 0.0236 |
4 | 14.840554 | 14.153416 | 0.6871 | 14.856544 | 0.0160 |
5 | 18.089819 | 17.513674 | 0.5761 | 18.087681 | 0.0021 |
As seen above, both new network clocks report values closer to the expected value (Server time).
The problem with synced network clocks (in general)
No matter which clock you end up using, remember that you cannot trust the clock value in a late joining situation. This is because we cannot ensure that the clock has synced right when we joined. However, we can paliate this situation with a NetworkEventSubsystem
(by Jambax), in which we would halt our OnRep actions until the clock syncs.
Conclusion
Finally, a non-intrusive synced network clock!
As I always say, be sure to make your own experiments, and don’t blindly trust randoms on the internet (me)!
I would like to thank Zlo#1654
, Adriel#5737
and Laura#4664
from Slackers again for all the help they gave me to make this post. As well all the people who support my articles, you are amazing.
By the way, the best way to track when I post articles is on Twitter, so don’t be afraid to follow me! DM’s are open!
Enjoy, vori.