BufferWait - Call now, yield later

Yes, this way it can be used in server scripts and local scripts [as opposed to .RenderStepped]
As far as I am aware there is absolutely no way to actually wait for a shorter period of time as Roblox’s scripts are bound to the frame rate

Heartbeat wouldn’t ever wait 0.00001 seconds. Heartbeat fires once a frame, which is a minimum of 1/60th of a second or ~0.017 seconds. This title is a complete misrepresentation of what the module actually does.

13 Likes

Also if you find yourself needing to wait that short you probably have a code smell somewhere.

4 Likes

Case of use: When RunService isn’t fast enough, thanks bro.

1 Like

This script does a lot of unnecessary work, besides the fact that the title is inaccurate. RunService frame-bound events are already machine dependent, so you can just make use of the frame delta timing directly in the wait function instead of setting it as a global in your module.

local Heartbeat = game:GetService("RunService").Heartbeat

return function(seconds)
    local aggregate = 0
    while seconds > aggregate do
        aggregate += Heartbeat:Wait()
    end
end

Not sure if what I’m doing is the same as what you’re doing on at least a technical level, but I shortened 29 lines of code to 8 here. At the very least, I don’t understand why all that work is being done in the original module for something that’s mainly intended to wait less than ~0.03 seconds.

4 Likes

This is not the same as the script I made. Try running this code with the script you made:

module = require(module)
for i = 1, 10000 do
module(0.00001)
end
This should wait for about 10000 * 0.00001 seconds (or 0.1 seconds) however with your module it would wait 10000 * 0.02 seconds (or 200 seconds).

Your module doesn’t return the function btw so it will error out with “Module code must return exactly one value”

image

I don’t think this works very consistently. I’m not getting good results for some durrations.

local bw = require(script.BufferWait)

local function test(amount)
	local t = tick()
	
	bw(amount)
	
	local r = tick() - t
	
	print(amount, "took", r, "delta", amount - r)
end

test(2) --> 2 took 3.2986514568329 delta -1.2986514568329
test(1) --> 1 took 0.97598147392273 delta 0.024018526077271
test(1/10) --> 0.1 took 0.1029257774353 delta -0.0029257774353027
test(1/20) --> 0.05 took 0.049432992935181 delta 0.00056700706481934
test(1/60) --> 0.016666666666667 took 4.7683715820313e-07 delta 0.016666189829508
test(1/100) --> 0.01 took 0.017052888870239 delta -0.0070528888702393
test(1/200) --> 0.005 took 4.7683715820313e-07 delta 0.0049995231628418
test(1/300) --> 0.0033333333333333 took 2.3841857910156e-07 delta 0.0033330949147542
2 Likes

Oh, I see. So the idea is to wait an inconsistent amount to work towards an ideal average time of the argument? That makes more sense functionality-wise, but I’m still not sure I understand the usecase here.

This is a very very smart hack.
Let’s go through it line by line.

This line seems very weird but,

	for i = PhysicsFPS, Time, PhysicsFPS do --// A better visualization will be to plug in numbers directly so we see what it's for.
		Time_Yielded_In_The_For_Loop_Kind_Of += Heartbeat:Wait()
	end

So in the real world, we get

	for i = .0166, 0.01, .0166 do --// or something similar.

Either way, it’s clear that this loop is actually in place to yield more than a frame. So when inputs are something like 0.1,0.2 it will yield at least a somewhat similar time.

Because of the horrible lack of comments, this is rather mystifying BUT I am pretty sure this just add how many time have been “wasted” in the previous loop (even if it didn’t yield) to the “buffer”. Over time, the buffer accumulates enough time.

	local seconds_add = Time - Time_Yielded_In_The_For_Loop_Kind_Of
	if seconds_add > 0 then
		CurrentBuffer += seconds_add
	end

Again this seems mystifying so we put more numbers in.

	local seconds_add = 0.01 - 0 --// Or something like 0.1 - 0.08, but it's also possible for this to be HIGHER than 0 for cases where the for loop did yield more than it should.

So, a check is in place to actually yield whenever that buffer has actually waited enough time.

	if CurrentBuffer > PhysicsFPS then
		CurrentBuffer = 0 --// Clears the buffer.
		Heartbeat:Wait() --// Yield for the minimum time possible.
	end

So tl;dr, it just yields every once in a while. My thought (possibly wrong) on the “inaccurate” time that this seems to have yielded is more or less likely due to the fact that os.clock is not std::chrono::high_resolution_clock or, even that is too inaccurate.

Very smart work. I salute you.

3 Likes
-- fast wait
local RunService = game:GetService("RunService")

return function(num)
    num = num or 0
    local n = 0
    while not (n >= num) do
        n += RunService.Heartbeat:Wait()
    end
    return
end

I recommend this instead. It uses only one .Heartbeat and that’s it. It’s just better.

So you wrapped RunService.Heartbeat:wait() in a module and said it waits 0.0000000000000001 seconds and posted it here? It’s genius isn’t it?

2 Likes

If you wanna “yield” for a very small amount of time the only way to do it is to spam a command when it’s close to ending until it ends. But that clogs up everything and is not recommended.

But it is literally just a wrap around the Heartbeat from the RunService. This will actually be slower than heartbeat because of the fact that it is wrapped!

Ah, so it’s a lie? I’m a bit confused.

Yeah I was being sarcastic. Its a complete lie.

1 Like