How to make a reliable wait system - WEwait() [Deprecated]

Deprecated

WEwait() has been deprecated, and should not be used for new work. It is currently impossible to create a reliable wait() function.

Hello! As you might all know, wait() is un-reliable. Today, I will show you how to make it reliable with a function I created! I call it WEwait()!


1. Create the NumberValue

Add a NumberValue to the script that you want to use WEwait for.
Why can’t I just use `Instance.new(“NumberValue”)?
It will make the code take longer to process. We want it to be instant.

2. Makind the function

First you will want to start with

local function Wait(waitTime)

end

And you will want to pass the argument/parameter waitTime.

3. Creating the Wait function

Now that we have the function, it is super simple. All you got to do is add the line we will be using. It is using TweenService, since that was programmed a lot better than :wait().

local waitTween = game:GetService("TweenService"):Create(script.Value, TweenInfo.new(waitTime, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut), {Value = 1})

4. Playing and catching when the tween was played

Now, all you got to do is add the 2 following lines:

waitTween:Play()
waitTween.Completed:Wait()

This will play the waiting tween, and detect when it is completed.

5. Returning

Now, all you got to do is return the value, so you can use it exactly like wait()!

return

6. Using in your code

Now that your function was created, you can simply replace wait() with capital Wait() if you already have come code using wait().

If you are creating new code, simply use Wait() and pass the amount of time you want to wait for.


I just want the function.

local function Wait(waitTime)
	local waitTween = game:GetService("TweenService"):Create(script.Value, TweenInfo.new(waitTime, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut), {Value = 1})
	waitTween:Play()
	waitTween.Completed:Wait()
	return
end

And you would use Wait() and not wait().
JUST MAKE SURE YOU HAVE A NUMBERVALUE INSIDE OF THE SCRIPT YOU WANT TO USE THIS IN!


Is it possible to just use wait() instead of Wait()?

Yes, it is! All you got to do is rename the function to wait() instead of Wait()! However, I don’t recommend this, because Roblox can just release an update to wait() to make it always use the un-reliable wait(). Global Wait() is already deprecated, so you should have no problems.


Examples:

  • I use this method inside of Bloxy Kart, inside of the stopwatch on Time Trials mode
  • Making code faster

I hope this helps you! If you have any questions, feel free to reply!
Hope this helps you!

3 Likes

This doesn’t make code fast but actually makes it slower(not by much). I’d rather use wait() instead of this methods, because you’re trying to set a Value which is really hacky(not to mention that you’re also using the entire TweenService code in the process).

2 Likes

Well it really depends on what you are using it for:

That’s because wait() runs at 30 fps, and any other thing (such as signals and events), run instantaneously or at 60 fps:

local RS = game:GetService("RunService")

RS.Heartbeat:Connect(function(DT)
  -- Put the DT time on the textbox
end

This example here is basically wait() but at the speed of the engine(around 60 fps).

local RS = game:GetService("RunService")

local function Wait(Time)
    local T1 = os.clock()
    repeat RS.Heartbeat:Wait()
    until os.clock() - T1 >= Time
end
3 Likes

Two issues with this:

  1. This isn’t really true, you could just create a NumberValue outside of your function a single time then use it for all calls which would qualify as “instant”.

  2. This does not work with coroutines that overlap their Wait(). I have tested this in studio:

local function Wait(waitTime)
	local waitTween = game:GetService("TweenService"):Create(script.Value, TweenInfo.new(waitTime, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut), {Value = waitTime})
	waitTween:Play()
	waitTween.Completed:Wait()
	return
end

local newThr = coroutine.wrap(function()
	local now = os.time()
	print("Coroutine calling wait")
	Wait(5)
	warn("Courtine elapsed time:", os.time() - now)
end)()

local now = os.time()
print("Normal thread calling wait")
Wait(1)
warn("Norm thread elapsed time:", os.time() - now)

This results in output:

  14:17:38.411  Coroutine calling wait  -  Server - Script:10
  14:17:38.411  Normal thread calling wait  -  Server - Script:16
  14:17:39.700  Courtine elapsed time: 1  -  Server - Script:12
  14:17:39.700  Norm thread elapsed time: 1  -  Server - Script:18

Personally, I think it’s a better idea to use Heartbeat for waiting if you really want precision/faster delays than regular wait(). This is pretty hacky IMO.

5 Likes

This method is not optimized at all. Everytime you call that Wait function you are calling the :Play method of TweenService which runs on it’s own thread. Threads aren’t meant to be spammed and can waste resources if used intensively.

And for that reason, the worst case scenario would be running this function in a loop which is bound to happen eventually for people using this module.

while true do
	Wait(0.2) -- Creates a new thread every 0.2 seconds
	-- Blah
end

I recommend using this implementation of a custom wait by @PysephDEV because of how performant it is, utilizing coroutine yielding instead of creating a tween object, and a priority queue datastructure implemented as a binary heap for max efficiency. Extracting the min node in the heap is O(1) time complexity. 0_0

3 Likes

Sorry about that, but I had to switch to just a default array because I wasn’t experienced enough to properly implement a binary-heap, lol…

1 Like

lol. I’m not very experienced with binary heaps either so I made a linked list implementation around an hour ago, and benchmarked it with the standard / common approach to making a custom wait. The differences in accuracy were very miniscule.

→ List Implementation

local RunService = game:GetService('RunService')
local Head = nil

local THREAD, YIELD, CLOCK, NEXT = 1, 2, 3, 4
local function Insert(Thread, Yield, Clock) --> Void
	local Node = {
		[THREAD] = Thread,
		[YIELD] = Yield, 
		[CLOCK] = Clock,
	}
	if not Head then
		Head = Node
	else
		local Temp = Head
		while Temp[NEXT] do
			if Temp[NEXT][CLOCK] < Clock then 
				break 
			end
			Temp = Temp[NEXT]
		end

		Node[NEXT] = Temp[NEXT]
		Temp[NEXT] = Node
	end
	return nil
end
local function ExtractMin() --> Void
	if not Head then return end
	
	if os.clock() >= Head[CLOCK] then
		local Difference = os.clock() - Head[CLOCK]
		coroutine.resume(Head[THREAD], Difference + Head[YIELD])
		Head = Head[NEXT]
	end
	return nil
end
RunService.Heartbeat:Connect(ExtractMin)
return function(Yield) --> TimeYielded
	Yield = Yield or 0
	local Thread = coroutine.running()

	Insert(Thread, Yield, os.clock() + Yield)
	return coroutine.yield()
end

→ Benchmark (Linked List vs Standard)

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Heartbeat = game:GetService('RunService').Heartbeat
local Yield = require(ReplicatedStorage.Wait)

local function Standard(T) --> TimeYielded
	T = T or 0	
	local PollT = os.clock() + T
	while os.clock() < PollT do
		Heartbeat:Wait()
	end
	return ( os.clock() - PollT ) + T
end
while true do
	local A = Standard(5)
	local B = Yield(5)
	
	print(( A < B and 'Poll' ) or 'List') -- Outputs which method was more accurate. 
end

Sorting the elements via a priority queue gave a very negligible speed boost which causes my benchmark to have inconsistent results (since both methods rely on heartbeat). My version is faster around half the time. Therefore, it isn’t worth using datastructures rather than the standard dynamic array when making a custom wait.

There are also a few bugs that could occur when using datastructures such as script exhaustion time which means a datastructure implementation isn’t very scalable with large amounts of input.
ex.

while Temp[NEXT] do
	Temp = Temp[NEXT]
end

Too many instructions will occur for luau’s VM to keep up with since the loop doesn’t yield. If I decided to yield in my loop via heartbeat, that would defeat the purpose of the module and lead to an even more inaccurate yield.

Therefore, the Standard wait is fully bug proof and should be used instead.

To be honest, I am tired of people posting “better” wait modules over and over again. And they all do not work well. This is included. It is a waste of resources, and not what tweens were meant for. Also, it is inaccurate and unreliable.

In my opinion, the best alternative to a new “wait” function is this:

local RunService = game:GetService(“RunService”)
local HB = RunService.Heartbeat

local function wait(time)
    local start = os.clock()
    
    while os.clock() - start < time do
        HB:Wait()
    end

    return os.clock() - start
end

What this above method uses is essentially the code behind the original wait function (likely), except it runs at 60 FPS unlike the 30 FPS that the original wait function uses. This is further explained by @VegetationBush here.

Or of course by using a custom task scheduler, like used in this module, although it is still vulnerable to numerous things such as script timeout.

1 Like

FACTS bro. I see a lot of devs make an over-complicated and not performant method to making custom spawn, custom delay or wait. Some methods are actually worse than the original issue.

I don’t really recommend that due to reasons mentioned in my most recent post in this thread. The custom scheduler is still prone to script exhaustion time and therefore isn’t very scalable via large amount of inputs.

1 Like

Nope.

TweenService is just based at RenderStepped / Stepped; You’re just using TweenService as a ‘bridge’ which uses more memory and it’s just inefficient.

Custom wait, for me is the best out there. It handles every wait() in a single Stepped event, which does not clog up the task scheduler easily. If you use it, I would change it to Heartbeat instead which takes .1 seconds anyways.

I would love to see a waitForCondition() or wait(function() return 2+2 == 3 end) type of thing.

Just use the wait by Pyseph and like have something like this lol:

local wait = require(game.ReplicatedFirst:WaitForChild("wait"))

local function spawn(func, ...)
    return coroutine.wrap(func)(...)
end

local function delay(time, func, ...)
    coroutine.wrap(function()
        wait(time)
        func(...)
    end)()
end

After getting feedback, I have decided that I will re-write this when I have time. Thank you, WE

Update 5/16/2021

I decided to deprecate this method of waiting because it is currently impossible to make a better wait(). I even tried other modules/methods, and they performed close/exactly the same as WEwait(). Hope you understand, and have a great day!

1 Like

If only the solution was that simple. When utilizing coroutines to handle pseudo threading, the stack trace is destroyed. This leads to the overcomplicated solutions some devs come up with.

but coroutine.wrap doesn’t do that though??


Yes, it won’t stop the script or anything, but it will print it out.

WHY ARE PEOPLE MAKING COMPLEX SOLUTIONS WHAT?

image

Spawn doesn’t ??? Spawn like behaves the same though?

That is a huge an issue and ruins the point of the error function. Error and Warn are meant to function differently. Error stops the current running thread, while warn doesn’t.

The problem seems to be with specifically get the trace, not print it out per say, for that you can just use coroutine.wrap anyways.

In that case I guess spawn() returns a traceback? Anyways, I don’t see the difference between like just using coroutine.wrap and spawn() except for spawn being like horrible. That’s fine though, let’s not spam too much;

Spawn is “horrible” if not used sparingly / wisley as that’s the common rule of thumb when creating threads, tbh I have absolutely no idea what evarea did to get spawn to “yield for 15 seconds” :skull:

Yes, it does.

1 Like

1 Like

that’s pretty cool. I’m trembling tho at how expensive that function is uWu. That’s triple the amount of threads: coroutine.wrap() + coroutine.resume(coroutine.create()) + pcall making it worse than spawn.

1 Like