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.
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.
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
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)
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.
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:
- Educating us in effective code design and construction
- Making this module which looks AMAZING!
- Making the code open source
So is ROBLOX task.wait() better or this module?
yea I am curious about this now
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