Custom wait - the best solution to yielding!

This is even better performance and more accurate than CloneTrooper’s FastWait. It was so much smoother when I applied it to my game. The game seems to pick up a higher bandwidth and ping than usual after the switch though.

1 Like

Unsure as to why the game would pick up a higher bandwidth & ping, as this has nothing related with networking. Are you sure you’ve benchmarked the data correctly?

I replaced all the wait() and wait(x) with this wait module on all serversided script, and it increased average ping by about 60-80%. So I switch back on the serversided ones and keep only the ones in localscripts, and the ping goes back to normal.

This module is very useful in making VFX. With the wait() and wait(x) being way more precise, low-end device can render them with way less delays on their client.

While I’m happy you find this module useful, you shouldn’t use waits everywhere. Use events and Event:Wait(), as it’s much better.

Example:
TweenService:Create(…):Play()
wait(x)
do stuff

Better solution:
local Tween = TweenService:Create(…)
Tween:Play()
Tween.Completed:Wait()
do stuff

And also use events like RunService.RenderStepped etc

(I have changed everything from Stepped to Heartbeat because the debugging process was easier, so I’ll be the same as using Stepped, in this test, this doesn’t matter.)

The problem with your module waiting longer than it probably could, is that it checks the time since CustomWait was called, not since the last frame.

This means that this module is fine for uses where what frame it is doesn’t matter, but the time needs to be accurate.

Basically, your solution can wait around 2 frames more than usual wait solutions.

I’m gonna try to explain this in the best way possible, but feel free to ask about it.


Your solution counts the amount of seconds SINCE the CustomWait was called, not since the last frame,

why is this a problem?

Well, if you’re working with tweens, or anything UI really, you’re using += deltaTime on some form of loop, or RenderStepped connection, then you check if it surpassed the time and resume the thread.

DeltaTime is not the time since you first called wait, but actually the time since the last frame was fired.

Let’s say you’re new and don’t know how to use Tween.Completed:Wait, well you’re gonna use this wait solution, in this case I’m gonna just be using a LoopWait because it’s easier and has the same effect.

I have a counter for frames, each new frame is +1 to this counter, let’s see then, how much longer does it take for this solution to believe the time has passed, compared to normal LoopWait,

It took 2 frames more :/

Basically this means that this breaks consistency with normal wait functions,

It’s really hard to explain why this is a problem, but if you think about the Tween Completed situation, think about how someone would like that to be fired at the exact same frame, as that would be an UI, but it might take up to 2 frames more for that to happen, which isn’t ideal.

And yes, I know the Tween Completed thing is literally already something that’s being done the wrong way, but it’s not specifically for this case, but for many others, where you NEED consistency with what frame that is being resumed at, and this little library doesn’t follow that expected result.

Most wait usage that I personally use is probably gonna be UIs too, so this could be a problem for multiple people.

If someone wants to follow the behaviour from before, you can have an extra argument to decide that.

My solution is to store when the last frame was fired, and use that instead of getting os.clock every time wait is called, which means that it basically does the same as += deltaTime,

Here’s some code, an example of the mod I did, and also the script I used to test it.

A solution I would have preferred is to just use += deltaTime on each item on the array, which I believe could be better. I’m more so discussing this problem right now rather than giving you a solution because it’s like 1 AM but I will help if I can soon.

Edit: Seems like this mod can also break slightly and wait one extra frame, haven’t it do 2 though, so that’s good, I’m gonna check if I did anything wrong, but it’s possible this is something that’s normal given my solution.

Concept fixed library:

--\\ I changed it back to Stepped here, but the testing script expects a function which uses Heartbeat

local RunService = game:GetService("RunService")
local o_clock = os.clock
local c_yield = coroutine.yield
local c_running = coroutine.running
local c_resume = coroutine.resume

local lastFrame = o_clock()

local Yields = {}


RunService.Stepped:Connect(function()
	local Clock = o_clock()
	for Idx, data in next, Yields do
		local Spent = Clock - data[1]
		if Spent >= data[2] then
			Yields[Idx] = nil
			c_resume(data[3], Spent, Clock)
		end
	end
end)

RunService.Stepped:Connect(function()
	lastFrame = o_clock()
	
	--\\ Connections are fired from last connected to first connected.
end)


return function(Time)
	Time = (type(Time) ~= 'number' or Time < 0) and 0 or Time
	table.insert(Yields, {lastFrame, Time, c_running()})
	return c_yield()
end

Code used to test it:

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

local function StandardWait(n)
	n = typeof(n) == 'number' and n or 0
	
	local spent = 0
	repeat
		spent += Heartbeat:Wait()
	until spent >= n
	return spent
end

local BeforeWait = require(game.ReplicatedStorage.Before)
local AfterWait = require(game.ReplicatedStorage.After)

task.wait(2)

Heartbeat:Connect(function()
	frameCount += 1
	
	--\\ Remember? Connections are fired from last to first,
	-- So we need this to update before anything else!
end)

local hasFinished = false

coroutine.wrap(function()
	StandardWait(5)
	print("LoopWait ended at frame", frameCount)
end)()

BeforeWait(5)
print("CustomWait ended at frame", frameCount)

Another problem that CAN cause this same delaying of one frame or two, is the fact that this library uses coroutine.yield so it might need to wait one frame before actually resuming, that would require you to use a BindableEvent to do that kind of stuff, and it’s just not worth it but yeah.

You could do a solution on which you just use one Bindable, or reuse a bunch, I don’t care really about that, but yeah that’s just something it’s good to remember if anyone didn’t know that.

I didn’t make a pull request because I kind of wanted to see people’s actual usage and how that kind of things affects or doesn’t affect them.

1 Like

Comparing this to UI here is not applicable as any UI-related changes should be set during RenderStepped - this module uses Stepped. This module is not meant to be extremely accurate down to 2 frames - it is meant to be accurate in terms of how many yields you can execute in bulk without losing accuracy. By taking into account for deltaTime, you are introducing:

  • GETTABLE to get the table data
  • ADD to perform addition on that value
  • SETTABLE to update the index’s value

And this would happen for every yield you want to update. In terms
of staying onto the path of what this module is intended for, it is not ideal. There is no one solution for everything - if you feel this module does not suit your needs, feel free to modify it.

That’s not necessarily true, I myself wanted to look into other options.

For example, I was updating a lastFrame value on the fork I had above, but really it would be better to have a value which increases every frame using DeltaTime, example:

local RunService = game:GetService("RunService")
local Yields = table.create(2)
local TimePassed = 0

local t_insert = table.insert
local os_clock = os.clock
local c_running = coroutine.running
local c_resume = coroutine.resume
local c_yield = coroutine.yield


RunService.Stepped:Connect(function(_, deltaTime)
	TimePassed += deltaTime
	
    local Clock = os_clock()

    for index, data in pairs(Yields) do
		local spent = TimePassed - data[1]
		
		if spent >= data[2] then
			
			Yields[index] = nil
			
            c_resume(
                data[3],
                spent,
                Clock
            )
        end
    end
end)

return function(n)
    n = typeof(n) == 'number' and n or 0
	
    t_insert(Yields, {
        TimePassed,
        n,
        c_running()
    })

    return c_yield()
end

There are many approaches, this is the best one I can think of.

So it’s not a question of performance or speed, or even complexity, it’s more so a question of do you want the behaviour to be the same as you have right now, which is fine for server usage, but can cause someone to stop using it on the client, I’ll leave that up to you to decide.

Edit: That sounded a little bit forcing, that’s not what I’m going for, it’s your decision if you want this behaviour for YOUR custom wait, I don’t want you to feel pressured

1 Like

Would I have to replace all my waits if I want to utilize this module?

You can just set a local variable called wait and require this module for your scripts and you should be fine.

It’s not that useful if you’re not calling wait a lot, but if yes, then yeah this will definitely help you in terms of performance (and accuracy)

1 Like

Alrighty, one more thing, trying to call it and then do a print after just hangs forever, here is my code

local ACWait = require(game.ReplicatedStorage.Modules.Wait) ACWait(4) print("hi")

The module:

local o_clock = os.clock
local c_yield = coroutine.yield
local c_running = coroutine.running
local c_resume = coroutine.resume

local Yields = {}
game:GetService('RunService').Stepped:Connect(function()
	local Clock = o_clock()
	for Idx, data in next, Yields do
		local Spent = Clock - data[1]
		if Spent >= data[2] then
			Yields[Idx] = nil
			c_resume(data[3], Spent, Clock)
		end
	end
end)

return function(Time)
	Time = (type(Time) ~= 'number' or Time < 0) and 0 or Time
	table.insert(Yields, {o_clock(), Time, c_running()})
	return c_yield()
end

Just a question, is this inside wait call inside another module?
coroutine.yield can break stuff with modules so, you might have to do Heartbeat / Stepped:Wait() before running this

The line of code is in the command bar

Uuh. RunService events shouldn’t fire on command bar…

You can use it just fine inside normal scripts.

I believe you can use task.wait instead of wait on the command bar though.

1 Like

Hi, I have a procedural map generation system that although is blazing fast, has a woping 12 waits in the main script to keep it from exhaust timeout (don’t remember the exact name) Although I was aware this was not ideal, I was not aware it was that bad. so I want to thanks you for 3 things:

  1. Educating us in effective code design and construction
  2. Making this module which looks AMAZING!
  3. Making the code open source
1 Like

So is ROBLOX task.wait() better or this module?

1 Like

yea I am curious about this now

@D0RYU @nakata1609

1 Like

What I mean is which one is more accurate

They should be similar, this should give you a more “accurate” yield time, but they should take basically the same amount of time.

If you’re messing with things the user might see like UI things or something, then go with … well neither I think.

What this function is made for is really being friendly to the thread scheduler, but apparently that’s fixed? so…

If you want something for UI use or something, things that depend on framerate, something the user sees, then you might wanna look into my little fork in my github

If you want to use something that depends on frame rate you should be using BindToRenderStepped, as pointed out by me above. RenderStepped waits for each callback to finish anyway, so it’s not as if a custom task scheduler for RenderStepped will change anything.