I’d love to see this working in a game. You should also have a look at Quenty’s clock synchronization module https://github.com/Quenty/NevermoreEngine/tree/version2/Modules/Shared/TimeSync which probably is “good enough” if you’re willing to relax the < 1 ms requirement. It’s based on IEEE 1588 Introduction to IEEE 1588 | NIST. The synchronization is performed similarly to yours, except a remote call is made around once a second (configurable IIRC).
I have a suggestion: Instead of using remote invocation calls why not just use a number value in the workspace/replicated storage?
thank you for this, os.time wasn’t syncing at all and i was so annoyed. this fixed the problem instantly. thank you for saving my time
What are you using it for? Just curious.
More bandwith probably. Hypothetically, if you were to rely on the instance to replicate, data about where this instance is, do i have a reference to it? etc. must be processed both ways internally. Whereas a remote is a more explicit method of communication, where you’d imagine less ‘tag’ data is sent along
I use the module in my virtual piano game specifically due to all three reasons you list. The basic framework involves notes being fired as remotes containing their information (i.e. press/release, volume, etc.) from a client to the server, which then fires the info to other clients for playing/visualization. Depending on the volume and speed notes are played at, however, notes intended to be played as rolled chords/arpeggios/grace notes/etc. can get weirdly pooled in the remote scheduling, which turns what should be discrete notes into a group that appears to be played at the same time; I term this behavior “note snapping” (imagine a bus stop, etc.).
To get around this (for the most part), I timestamp notes on the client they were played and delay playing/visualizing them on receiving clients using a pseudo-scheduler tied to RenderStepped to replicate how they were played by the sender as closely as possible. The accuracy and precision of the timestamps provided by the module are basically perfect for achieving this, so I’m really grateful that it exists. I was at the end of my rope trying to get the implementation to work since tick() differed across system time zones and both os.time() and TimeSyncManager lacked the accuracy and/or precision to be used in RenderStepped scheduling.
That’s awesome. Have you tested this with multiple devices in the same room, like your pc and a phone on different accounts? You’re going to get the best results in simulated servers in studio, but do check its performance in live servers.
Yeah, I’ve been running the implementation since around October last year but decided to post here now since you seemed to be looking for implementation examples. As far as I can tell it works fairly well, and I introduce around 500ms of delay by default since worst-case ping scenarios seem to be ~250ms round trips on average.
Is there any open source practical use of the module? I want to use this in my game but I’m confused on how to implement it
i am making an obby racing game and i wanted the “3, 2, 1” countdown to sync so it doesnt say “go” like .5 seconds after you actually go.
its really easy to implement, just replace tick() with clock:GetTime() or whatever
BTW, you can get away with just timestamping with tick() for your use case. It sounds like it’s not really the exact timing of the notes that matters, but the timing of notes relative to other notes. So, you can just figure out how many fractions of a second one note should play after another, and relay that information.
Of course, my sync tech works too, but since it uses some pretty hacky stuff, if Roblox decides to change their networking behavior unannounced, it could suddenly break one day with no warning. So please be wary of that.
How would I sync a Vector3 or CFrame value like what was done in the demo?
Could you explain how this works? Recently I’ve been trying to make a lag compensation system, but the ping calculations not being perfect (Because packets are not sent as the call is made) keep offsetting my predictions.
The green block is created on the client, while the red one is created on the server. Red is supposed to estimate where the position of the white block is on the client, green is where it actually is. This does a decent job, considering this block was moving at 60 studs per second, but it’s still not perfect, and I think that’s because of the delay between a packet being called and it being sent. I’d need to always know how long the previous network update’s packet replication ping from server to client.
[UPDATE]: I have rewritten this module to version 2.0. This new version should be much much more stable, relies on less hacky code to accomplish practically the same result, and will maintain the same precision level even during extremely high latency situations. If you have used the original version of this code in your games, please swap it out with this new one. Updated code is found on the github, as well as the uncopylocked demo place.
Try using the new version of this sync clock. I rewrote and took a smarter, simpler approach to predicting/circumventing unpredictability in the packet buffer.
Very useful module, I’m using it right now in my effects system to properly queue up effects to be processed. I recommend
I like this method a lot, previously I was using Quenty’s at 1 second updates but I will probably switch to yours since it’s more continuous.
I think there is a typo on line 106 where you should FireClient
not FireAllClients
and on line 52 I think total makes more sense to be math.min(count,50)
.
ReplicationPressure
is also a really cool concept, is this your substitute for UDP? I’m curious to learn more use cases (physics? non-physics?) and how you determine your constants eg module.Threshold?
Slightly off topic, but I saw in another thread that you structure your code to run mostly in heartbeat—would it be possible to share more about this?
Good catch. It should definitely be FireClient, I will update the github and uncopylocked place in a bit with the correction. Line 52 is correct.
Replication pressure I will explain in a moment
Replication pressure is basically measuring how many things are being replicated at that moment. I use game:DescendantAdded and keep a tally on the number of things being added every frame.
Why is this important? It’s important because when many things are being replicated at once, all of your remotes will get held up while the replication is occurring. So, if you parent a massive map from ServerStorage to Workspace, all of your remotes that were fired during that period will have to wait for however long it takes to replicate that map, which could be 10 seconds, a minute, etc.
So basically I track “Replication Pressure” and if the pressure is really high, I stop updating the offsets temporarily.
For your question about running code on heartbeat, I just picked heartbeat because I like heartbeat. I could’ve picked stepped and have essentially the same functionality. Picking renderstepped, however, is bad because then you cannot have your code execute in parallel to the rendering code.