# Help with Achieving a Loop with Precise Frequency

Hello all!

I am programming an electrical system that players may use to create complex and precise contraptions and mechanisms. It is very important to have reliable and precise behavior due to how complex some players will make their contraptions.

And naturally, I have been having issues with making a `While` loop that yields precisely. I am aware `wait()` is incredibly unreliable and so I have been using `task.wait()`, which is a significant improvement, but still far too unacceptable, even with some code to try to account for inaccuracies (somewhere below).

For instance, say the `while` loop is set to run at 50 hertz (50 times a second), I get around 62% of the desired result when not using correction code (around 30-31 hertz) and I get around 86% of the desired result when correcting (around 43 hertz).

Two things to note:
1.) Ideal behavior is to be no more than 3 hertz away from the target frequency (so, for a target hertz of 50, anything between 47-53 is not bad). Anything more than 5 hertz away is unacceptable.

2.) My testing has been limited mostly to around 50 hertz, but other frequencies will be implemented and I would really like to avoid hacky solutions that only work for one or two hardcoded frequencies. Also, it seems higher frequencies are even more unreliable. Testing at 150 hertz averages around ~63 hertz! Thatâ€™s not even half of the target result!

I am really stumped on this, perhaps I should do some RunService shenanigans with os.clock()?

Here is the code:

``````                --This is for the circuit searching/powering loop. Uses bC.hertz()

function bC.Start()
if not bC.canPower() then
local timesFPS = 0

--I would GREATLY like to avoid doing something like this hacky "constant" (it is not even a constant!)
local hackyConstant = 0.81967213114754098360655737704918 -- a very hacky, temporary multiplier. This does not work for values far other than 50.
local targetWait = (1 / bC.hertz()) * hackyConstant

local correctedWait = targetWait
local lastWait = targetWait

while bC.canPower() do
if bC.watts() > 0 then
--This is the main code to check for a valid circuit and then power it!
end

--TODO add correction function to account for the time needed to run the real, actual code above!

--This block attempts to correct for any spikes or drops due to Heartbeat.
timesFPS = timesFPS + 1

if lastWait ~= targetWait then
correctedWait =  targetWait + (targetWait - lastWait)
end

end
end)

bC.setPower(true)

--Checking to make sure fires mostly correctly.
bC.Stop()
print("Fired "..timesFPS.." times in "..debugTime.." seconds.")
print("TargetWait is "..1 / bC.hertz())
end
end
``````

Its not really possible to have accurate times regardless of what method you use, waits() and runservice will be different due to several factors like game FPS, and just simple floating issues. Instead you should use the time difference between each cycle(DeltaTime) to determine what happens, for example if the deltatime is greater than 2 cycles then you just scale whatever operation your doing by twice

2 Likes

Something like that could work, I suppose, but my concern is for user builds that require relatively precise timings. Sometimes having your electric components fire twice (or maybe more!) and the general unreliability of having it fluctuate that much is a concern for me.

Thatâ€™s why I was attempting to make correction code (this block):

``````                               if lastWait ~= targetWait then
correctedWait =  targetWait + (targetWait - lastWait)
end

``````

Basically, the idea was that if the last cycle was too slow or too fast, try adjusting the current cycle to be faster or slower to try to compensate and roughly get to a semi-reliable frequency

That honestly probably the best you could do, you could try using os.clock() but not sure if os.clock() has the precision needed for your usecase, or you maybe could try event based programming and rely on signals?

I donâ€™t intend on using massive frequencies like say, gigahertz (1 billion times a second). Realistically, it is unlikely that the game will use frequencies higher than maybe 500 hertz at the highest end. 50-60 hertz would be by far the most common and as long as I can get 90% of the target frequency reliably, I think it would work fine for my use case. If I substantially lower the `task.wait()` time, I can sometimes get closer to the target, but it gets so unreliable and unpredictable.

Just keep in mind that its common for games to have a delta time of like 1 second long before it executes the next frame due to like lag and stuff. So its just better to code for that event rather then trying to create a accurate frequency

Seems like you need to rely on achieving a rate of 50 Hz.

I would also suggest using delta time like @Thedagz mentioned but specifically an accumulator to guarantee a code runs at a desired rate.

Similarly for time step for achieving reliable behavior over time for problems such as physics or maybe your case though Iâ€™m not entirely sure.

3 Likes

I would suggest using both Heartbeat and Render Stepped if possible to double the rate you can check. You can use os.clock or tick to check how much time has elapsed since the time between Heartbeat and RenderStepped isnâ€™t going to be even.

1 Like

Ah, yes, I see. So, that is what you guys meant with DeltaTime. That probably can reasonably meet my use case. If not, I will probably have to settle for inaccuracies and try to make a more broad timing/clock cycling functionality. Naturally, I expect some performance sacrificed for precision, but is there any significant performance impacts I should be aware of when doing this?

Players will be able to access the timing mechanisms and players are unpredictable. I need to know roughly how much I have to limit their ability to use these mechanics to avoid abuse

assuming the stuff run at 50Hz, 0.02 seconds, each iteration would have at most 0.02 seconds of being late in doing what it needs to do

This would be ideal, yes, but the problem is that I canâ€™t get it consistently at a target frequency (like 50 hertz). For instance, the `task.wait(0.02)` (aka 1 / 50) can yield all over the place. Sometimes it yields at the desired 0.02. Sometimes it yields at 0.04. Sometimes 0.05. I even got a few times around 0.1 (this would be closer to a measly 10 hertz!)

The problem is the unreliability and consistently reaching around the target point.

I will try the DeltaTime idea tomorrow, but I am leaving this unsolved until I get around to trying it.

Future viewers, feel free to suggest other ideas as well.

I donâ€™t know about task.wait(), but I believe wait() could not go any faster than 0.03 seconds. (I think wait() was equivalent to wait(0.03).) I would check to see if task.wait() has a similar 0.03 second minimum.

I believe `task.wait()` does not have such a minimum as it is the same as `RunService.Heartbeat:Wait()`, meaning if no number is passed to it, it should execute on the next Heartbeat.

The problem is each Heartbeat varies greatly in time elapsed, albeit nowhere near as bad as normal `wait()`

You can get perfect 50 hz average by keeping a running count of the progressed time (which should be accurate over longer times) like so:

``````--Testing code
local dts = {}
local t_prev
function test_timing()
local t = tick()
if t_prev then
local dt = t - t_prev
table.insert(dts, dt)
end
t_prev = t

if #dts >= 100 then
local avg = 0
for _, dt in ipairs(dts) do
avg += dt
end
avg = avg / #dts
print(1 / avg) --Should be pretty close to 50
dts = {}
t_prev = nil
end
end

function update(dt)
local t0 = tick()

local t = tick()
if t - t0 > dt then
warn("Update is too slow to run at " .. dt .. " hz.")
end
end

local dt_cumulative = 0
RunS.Heartbeat:Connect(function(dt)
dt_cumulative += dt

while dt_cumulative >= freq do --Don't run all the way to 0, it's nicer to just have constant timesteps. Try it if you want tho.
dt_cumulative -= freq
test_timing()
update(freq)
end
end)
``````
2 Likes

The solution presented by @ThanksRoBama, @dthecoolest, and @Thedagz seems to be working quite well so far from the tests I have done. Frequency is often 98% within the target range, which is very good so far.

For those viewing this, the code I implemented is as follows:

The main code:

``````				function bC.Start()
if not bC.canPower() then
local targetWait = (1 / bC.hertz())
local timesFPS = 0

bC.setPower(true)

if bC.watts() > 0 then
--This is the main code to check for a valid circuit and then power it!
end

--TODO add correction function to account for the time needed to run the real, actual code above!

timesFPS = timesFPS + 1
end

if not bC.canPower() then
return true
else
return false
end
end

--Checking to make sure fires mostly correctly.
bC.Stop()
print("Fired "..timesFPS.." times in "..debugTime.." seconds.")
print("TargetWait is "..1 / bC.hertz())
end
end
``````

The â€śUtilityâ€ť ModuleScript (â€śutilâ€ť references this module)

``````function utilities.DeltaLoop.Start(payload,condition,frequency)
local deltaTimeTotal = 0

utilities.DeltaLoop.CurrentLoop = game:GetService("RunService").Heartbeat:Connect(function(dt)
deltaTimeTotal += dt

if condition() then
utilities.DeltaLoop.CurrentLoop:Disconnect()
utilities.DeltaLoop.CurrentLoop = ""
return
end

while deltaTimeTotal >= frequency do
deltaTimeTotal -= frequency
end
end)
end
``````

If youâ€™d like to point out flaws in the above code blocks, feel free to as well.

Again, thanks to everyone who answered/commented!

3 Likes