Custom wait - the best solution to yielding!

I’m pretty sure both are linked to physics simulation? (Or it’s just Stepped cause Heartbeat still works when you’re in Edit mode)

@PysephDEV
How does this compare to using tick(), like:

oldTick = tick()

if (tick() - oldTick) > 0.01 then

OldTick = tick()

end

(Just some pseudo code, but you get what I mean, using a loop for example)

Stepped and Hearbeat are both fired during a frame - Stepped is fired pre-simulation, Heartbeat is fired after.

I’m sorry but I’ve no idea what you’re asking here.

1 Like

I see now that my post was awful. So what I meant:
instead of writing;
while true do
print(“test”
wait(1)
end

you can write:

heartbeat:Connect(function()
if tick()-oldTick > 1 then 
oldTick = tick()
end
end)

so you are basically checking if the time difference is more than 1, instead of waiting 1 second before re-running the “loop”.

AND THAT leads me to another question:
is the tick() method just as good as your module, for all things loops? And what is the difference between @Maximum_ADHD 's module and yours? Thanks.

I want to ask this question just to clarify, I scrolled through all of the posts and haven’t seen this addressed.

Do you recommend that this module be used in any game regardless of wait count or frequency? Is it a guarantee that your performance will be better? Or do the performance benefits of this module only apply in situations where there is a significant number of yields to actually clog the task scheduler.

The latter - there’s not really a reason to use this unless you rely on yields quite a bit. My game doesn’t really use wait itself all that much, but it does use a custom version of Debris:AddItem, which uses this module.
It doesn’t do any harm to use this module even if your game isn’t calling yields a lot, though.

It is not necessarily true. I ran some tests with your module and the regular “loop with Heartbeat:Wait()”. The result showed “loop with Heartbeat:Wait()” is slightly more accurate. My test code and outputs are provided below.

Test code:

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)

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

local function loopHeartbeat(seconds)
	local elapsed = 0
	local timeout = seconds or 0

	repeat
		elapsed = elapsed + RunService.Heartbeat:Wait()
	until elapsed > timeout

	return elapsed
end

wait(10) -- Wait 10 seconds for "Play Solo" to be stable

local function test(waitFunction, runCounts)
	local totalTime = 0

	runCounts = runCounts or 30

	for i = 1, runCounts do
		local waited2 = waitFunction(1)
		totalTime += waited2
		print(waited2)
	end

	local averageWaitTime = totalTime / runCounts
	print("Average wait time: ", averageWaitTime)

	return averageWaitTime
end

print("Pyseph's BetterWait")
local result1 = test(wait2)

print("Loop with Heartbeat:Wait()")
local result2 = test(loopHeartbeat)

local delta = result2 - result1

print("Final Results: ")
print("Pyseph's BetterWait average wait time " .. result1)
print("Loop with Heartbeat:Wait() average wait time " .. result2)
print(
	"More Accurate Function: "
		.. (delta < 0 and "Loop with Heartbeat:Wait()" or "Pyseph's BetterWait")
		.. " by "
		.. delta
)

Output:

  17:36:16.164  Pyseph's BetterWait - Playground:143
  17:36:17.180  1.0151325040024 - Playground:134
  17:36:18.196  1.0163139370015 - Playground:134
  17:36:19.197  1.0007079129973 - Playground:134
  17:36:20.214  1.0163980680009 - Playground:134
  17:36:21.229  1.0155252889999 - Playground:134
  17:36:22.230  1.0004381969993 - Playground:134
  17:36:23.246  1.0161530400001 - Playground:134
  17:36:24.264  1.0170518500017 - Playground:134
  17:36:25.280  1.0155368090018 - Playground:134
  17:36:26.281  1.000763101998 - Playground:134
  17:36:27.297  1.0158456979989 - Playground:134
  17:36:28.297  1.0000290450007 - Playground:134
  17:36:29.314  1.0163870389988 - Playground:134
  17:36:30.330  1.0162918210008 - Playground:134
  17:36:31.347  1.0161106450032 - Playground:134
  17:36:32.364  1.016458170001 - Playground:134
  17:36:33.380  1.016455691999 - Playground:134
  17:36:34.397  1.0159157490016 - Playground:134
  17:36:35.413  1.0164659180009 - Playground:134
  17:36:36.430  1.0164859329998 - Playground:134
  17:36:37.446  1.0157243880021 - Playground:134
  17:36:38.463  1.0169294400002 - Playground:134
  17:36:39.480  1.0161411480003 - Playground:134
  17:36:40.480  1.0000558619977 - Playground:134
  17:36:41.497  1.0163700119992 - Playground:134
  17:36:42.514  1.016410242999 - Playground:134
  17:36:43.530  1.0162811160008 - Playground:134
  17:36:44.547  1.0162930980005 - Playground:134
  17:36:45.563  1.016094224 - Playground:134
  17:36:46.580  1.0163273530015 - Playground:134
  17:36:46.580  Average wait time:  1.013569776767 - Playground:138
  17:36:46.580  Loop with Heartbeat:Wait() - Playground:146
  17:36:47.564  1.0000607641414 - Playground:134
  17:36:48.564  1.0002627000213 - Playground:134
  17:36:49.564  1.0000321837142 - Playground:134
  17:36:50.564  1.0001076254994 - Playground:134
  17:36:51.581  1.0168677251786 - Playground:134
  17:36:52.597  1.016533896327 - Playground:134
  17:36:53.613  1.0155636835843 - Playground:134
  17:36:54.614  1.0010616341606 - Playground:134
  17:36:55.631  1.0166026903316 - Playground:134
  17:36:56.646  1.0149364760146 - Playground:134
  17:36:57.647  1.0009081605822 - Playground:134
  17:36:58.663  1.0167570821941 - Playground:134
  17:36:59.664  1.0002399636433 - Playground:134
  17:37:00.664  1.0002204701304 - Playground:134
  17:37:01.680  1.0162733001634 - Playground:134
  17:37:02.696  1.0162025485188 - Playground:134
  17:37:03.713  1.0166019331664 - Playground:134
  17:37:04.730  1.0164914987981 - Playground:134
  17:37:05.732  1.0022880947217 - Playground:134
  17:37:06.746  1.014188173227 - Playground:134
  17:37:07.746  1.0003567114472 - Playground:134
  17:37:08.747  1.0000508250669 - Playground:134
  17:37:09.763  1.0163883510977 - Playground:134
  17:37:10.763  1.0000830888748 - Playground:134
  17:37:11.763  1.000117902644 - Playground:134
  17:37:12.763  1.0001743398607 - Playground:134
  17:37:13.779  1.0156670575961 - Playground:134
  17:37:14.780  1.0006323931739 - Playground:134
  17:37:15.797  1.0167331211269 - Playground:134
  17:37:16.813  1.0167141389102 - Playground:134
  17:37:16.813  Average wait time:  1.0083039511306 - Playground:138
  17:37:16.814  Final Results:  - Playground:151
  17:37:16.814  Pyseph's BetterWait average wait time 1.013569776767 - Playground:152
  17:37:16.814  Loop with Heartbeat:Wait() average wait time 1.0083039511306 - Playground:153
  17:37:16.814  More Accurate Function: Loop with Heartbeat:Wait() by -0.0052658256364035 - Playground:154

Edit: Swap “Heartbeat” with “Stepped” is more accurate.

local function loopStepped(seconds)
	local elapsed = 0
	local timeout = seconds or 0

	repeat
		local _, delta = RunService.Stepped:Wait()
		elapsed = elapsed + delta
	until elapsed > timeout

	return elapsed
end

Your benchmark is flawed…
First off, you do not type check properly for whether the input is a number or not, and also whether it is smaller than 0 or not. This already makes your function more performant (and not to mention that you use Heartbeat instead of Stepped to make the comparison more equal).

Switching to Stepped and adding the number check, you get this (forgot to rename Heartbeat:Wait() to Stepped:Wait()):
image

Now, yes, this does seem as if the latter is better, but I’m doubtful you are going to have one yield in your whole game. Utilizing tables is inevitably going to be a better option for updating several waits - I in fact don’t even recommend using this module if you’re going to barely use yields! You should only use this when you’re starting to worry about clogging your task scheduler, which is the real enemy here.

EDIT: I realized I came off as rude here… Definitely not the intention. TL;DR is that you shouldn’t use this module unless you use wait a lot.

1 Like

One question I have about this is that what are the differences between this and CloneTrooper1019’s Thread function?

Only differences are:
A) this version is more compact
B) slightly more performant (you shouldnt need to care about this though), and
C) publicly available on devforum.

1 Like

Ok, thanks I’ll think about using this in my future projects.

Hey, I think you might have already talked about this, but I need a bit of clarification.
Is it performant to use wait() with an integer? Ex: wait(1)
Although I rarely use wait() by itself, I do use wait(integer) and the delay function quite frequently.
I’m writing the “engine” for a game I’m making, and I’d like to know what’s most performant early on before I have to sift through piles of code.

Depends. If you call wait(1) 100 times across your game code, I.e have 100 active yields, then it’ll start to turn a bit sour. The task scheduler gets flooded whenever it has to yield and resume a lot of threads - which is especially easy to occur when you call wait(). wait(1) can still flood the task scheduler, but it’s more unlikely. So in the end, it depends.

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