Why GetServerTimeNow is the GOAT of client-side simulation

Determinism is a term we can use to describe algorithms, where given the same input they give the same output. Meanwhile this definitions seems to be simple it allows us to do one really cool thing: if we can sync the input to the function on client and server, we can make the client calculate updates to things instead of the server.

An example of this is a moving part, consider this script

local part = workspace.Part

RunService.Heartbeat:Connect(function(dt)
    part:PivotTo(part:GetPivot() * CFrame.new(dt * 10, 0, 0))
end)

This being ran on the server will send a packet to the client every heartbeat, not really network efficient.

Moving the script to the client will have 2 problems

  1. Players will see the part in different positions, relative when they joined
  2. Even if they joined at the same time, the parts will slowly drive away from eachothers cause of small floating point errors

GetServerTimeNow

GetServerTimeNow is a method under workspace, which approximates the current time on server, which means from the last time it got a current time update from a server, it will approximate time it should be now, it slowly speeds up or slows down to account for innacuracies within the approximation.

Rewriting the algorithm to be deterministic

The deterministic version of the script will use 3 variables, startTime, currentTime, startPosition from which point on we will use the speed formula.

On server in the part we will make it keep a timestamp of when it was created

part:SetAttribute("StartTime", workspace:GetServerTimeNow())
part:SetAttribute("StartPosition", part:GetPivot())

and on client we will calculate the distance the part would have passed since that timestamp, every update treshould you would use (im gonna use RunService.RenderStepped)

local startTime = part:GetAttribute("StartTime")
local startPosition = part:GetAttribute("StartPosition")
RunService.RenderStepped:Connect(function()
    local currentTime = workspace:GetServerTimeNow()

    local distance = currentTime - startTime

    local newCFrame = startPosition * CFrame.new(distance * SPEED, 0, 0)
    part:PivotTo(newCFrame)
end)

Using this method causes all clients to be synced up even better than constantly sending the new position thru the server with less packets.

Cons

  1. If your algorithm uses randomness, and that randomness is supposed to be unpredictable to the client, then yeah you cant use this method (otherwise just send the seed)
  2. You have to convert your previous algorithm to a deterministic one which might be hard or impossible, consider other algorithms that achieve similiar things (Personal example: i was making a orb which looks like it wanders around, my original version would have the server choose the new position to go at random, for the derministic version i have used math.noise with argument being the difference between start time and time now, math.noise would decide the orbs distance from the startPoint)

Edit: Changed the title from “Using Determinism to Lower Network Strain” to “Why GetServerTimeNow is the GOAT of client-side simulation”

18 Likes

GetServerTimeNow() is good to calibrate the time difference between the client and the server, but that’s about it.
It’s very slow and resource-demanding; you’d better turn GetServerTimeNow() into os.clock offset difference.

Etc.:

local offset = workspace:GetServerTimeNow()-os.clock()

And calibrate with:

os.clock()+offset

From time to time, update the offset variable to keep it up to date.
Benchmarks:

image
(workspace:GetServerTimeNow() almost 20 times slower)

task.wait(5)


local t = os.clock()

for i=1,9999999 do
	workspace:GetServerTimeNow()
end

print("GetServerTimeNow()",os.clock()-t)
t = os.clock()

for i=1,9999999 do
	os.clock()
end

print("os.clock()",os.clock()-t)
1 Like

Calling workspace:GetServerTimeNow() is never realistically going to tank the performance of your code. If you ever need to call it 10000+ times a frame - hopefully you wont need that - then you could call it only once and pass the value to whatever needs it.

1 Like

That does not matter
Making offset saves up network and hence good
It all eventually snowballs
Neglegence on optimization and especially cheap to implement one is the root of all problems.

Saves up on network? You were just talking about performance earlier then now network?
That method doesn’t consume any bandwidth to use. Regardless of whether you use it or not, roblox will keep syncing it with server.

I’m not claiming Roblox is firing packets every time, but I also don’t buy the idea that it’s magically cached unless you call it. From what I’ve seen, the server runs it ~15x faster, so something in that chain clearly isn’t “free.”

And even ignoring that, the benchmark still shows a 25-30x gap where os.clock() wins every single time. So yeah: using an offset literally costs nothing and avoids the overhead. That’s just basic optimization, not rocket science.

My point wasn’t vague, you just assumed the best-case scenario for the API instead of looking at the numbers.

If you are that worried about :GetServerTimeNow(), I have this resource, which does basically what you are mentioning, with a simple TickModule:UTC() call. It sends an unreliable remote event every so often to sync the server’s and client’s clocks

The thing is that the worst case scenario is literally wildly unrealistic. Something very important to understand with optimization is that, yes you could optimize :GetServerTimeNow() with your own solution, but the gains you will get from it literally will not be perceivable

I think your math is wrong? idk how you got 20 times slower. I’m getting like 6.85

I’m not sure I quite understand the sentence, but here is my answer anyway, it has to be cached, unless, a call to :GetServerTimeNow() wouldn’t be able to return accurate time (and :GetServerTimeNow() has a guarantee that it wont ever decrease, which implies that at least 1 syncing operation was done before it is called. This is something my implementation doesn’t do)


As for the tutorial, this technique is one I really like. I’ve done a song playlist system (where songs play in random order, but are all guaranteed to play once per “cycle”. Required to get a fixed duration for a cycle, so clients can know, with basic math, in which cycle the system is).
I’ve also done a ski lift system that uses the same principle, for the position of the chairs. It does get very complicated when it comes to making the chairs slow down and speed up at the stations (as figuring out the equations requires calculus knowledge, to figure out how much time each section takes, and getting the position x(t) function from the speed v(t) function)

The swinging is non deterministic (unsurprisingly), but since each clients sees the same movement for the chairs, they will have similar swinging

3 Likes

But module you provided uses method when never using self
UNOPRIMIZED + NON INLINABLE
Plus you really better off copy pasting code into each script for better performance
I really hate that there is no header files in luau or macros

Do you really think it matters for performance? I use methods because it looks nicer. That’s how much I care

Are you trolling?

You are very cruel to people that run Roblox on a potato.
That’s a very not good person behavior; I would even say A PERFORMANCE MURDERING BEHAVIOR.

Obviously I’m not trolling; I’m just stating the facts.
Fact: Roblox devs cling to excuses instead of optimizing.

I lowkey plan to write my own preprocessor + minifier that adds the ability to have header files and macros.
And don’t forget “local _”
Less space = faster to compile and more engagement metrics™.

Im just good at following my goal: making world a more optimized place.

1 Like