Accurate task Library

These functions are for people who are looking for a more accurate task library. It includes delay, spawn and wait functions. In order to set it up, just copy this code and put in a ModuleScript.

local frame = task.wait() -- Counts the last frame delay
local task = {
	wait = function(n)
		local time_ = os.clock() -- Records the time when the function was called
		n = tonumber(n) or 0 -- If it's not a number, it will count it as a zero (not recommended, less performance)
		
		if os.clock() - time_ < n then -- If n has passed, then it doesn't yield.
			if n > frame then -- Verifies if n is more than the last frame delay.
				while os.clock() - time_ < n - frame do
					frame = task.wait() -- Waits for the next frame until the time passed is close to n
				end
			end
			
			while os.clock() - time_ < n do
				if false then end -- Repeats this function that doesn't "yield" until n has passed.
			end
		end
		
		return os.clock() - time_ -- Returns the time that has actually elapsed.
	end,
	spawn = function(func, ...)
		local event = Instance.new("BindableEvent") -- Creates a BindableEvent
		local param = {...} -- Puts the parameters in a table
		if type(func) ~= "function" then
			error("attempt to call a " .. typeof(func) .. " value")
		end
		
		event.Event:Connect(function() -- Connects the function with the BindableEvent
			func(unpack(param)) -- Calls the function with the parameters
			event:Destroy() -- Destroys the evet
		end)
		event:Fire() -- Fires the event
	end
}
task.delay = function(n, func, ...)
	local time_ = os.clock() -- Records the time when the function was called
	local event = Instance.new("BindableEvent") -- Creates a BindableEvent
	local param = {...} -- Puts the parameters in a table
	n = tonumber(n) or 0 -- If it's not a number, it will count it as a zero (not recommended, less performance)
	if type(func) ~= "function" then
		error("attempt to call a " .. typeof(func) .. " value")
	end
	
	event.Event:Connect(function() -- Connects the function with the BindableEvent
		if os.clock() - time_ < n then -- If n has passed, then it doesn't yield.
			task.wait(n - os.clock() + time_) -- Waits n seconds.
		end
		
		func(unpack(param)) -- Calls the function with the parameters
		event:Destroy() -- Destroys the event
	end)
	event:Fire() -- Fires the event
end

return task -- Returns the library

Comparisons:

wait function:

Seconds
accTask.wait
0 0.2657 0.0003 1e-6 7e-7
1 1.0123 1.0177 1+3e-6 1+1e-6
2 2.0144 2.00017 2.00010 2+1e-6
3 3.0151 3.0174 3+2e-6 3+1e-6
4 4.0142 4.0169 4+1.5e-6 4+1.2e-6
5 5.0135 5.0002 5+1.6e-6 5+9e-7

delay function:

Seconds
accTask.delay
0 0.2442 0.2440 1.4e-5
1 1.0058 1.0060 1+3e-6
2 2.0043 2.0046 2+4e-6
3 3.0036 3.0040 3+5e-6
4 4.0137 4.0140 4+3e-6
5 5.0081 5.0085 5+5e-6

spawn function:

accTask.spawn
0.2751 1.62e-5 1.94e-5

Results may vary.


Feel free to comment if you have any questions/advice.

3 Likes

Just out of curiosity, is it even possible to make the engine wait that amount of time? I’ll imagine that even most basic code executions take a similar amount of time or even more than that.

Yes! I tried with this:

local time_ = os.clock()
print(os.clock() - time_)

And it returns 1.0000076144934e-07 seconds (basically 0.00000010000076144934 seconds)

That is not waiting though. Unless you call actual wait(), task.wait() or coroutine.yield() functions, you would achieve nothing but simply stall the entire engine for that duration.

I don’t see any practical applications where waiting less than a duration of the frame is necessary, since pretty much everything else in the engine happens once* in a single frame, which means even if you trigger something precisely at the exact moment you want to, you would see the result only after it renders.

* Physics run 4 times, one after another.

P.S.: There was a similar post a couple of months ago, and a member of Roblox Staff has explained why waiting more than 60 times per second is unnecessary:

2 Likes

I only call non-yieldable code when I want to wait an amount of seconds less than the duration of the previous frame. That’s why I use task.wait() before that.

		if os.clock() - time_ < n then -- If n has passed, then it doesn't yield.
			if n > frame then -- Verifies if n is more than the last frame delay.
				while os.clock() - time_ < n - frame do
					frame = task.wait() -- Waits for the next frame until the time passed is close to n
				end
			end
			
			while os.clock() - time_ < n do
				if false then end -- Repeats this function that doesn't "yield" until n has passed.
			end
		end

Yes, I can see that. However, I am more concerned about the practical applications of your library. Can you provide any example of where it would be useful to run something after precisely N seconds instead of just N +- delta_time seconds?

I saw this topic so I thought if I could make it faster:

The only thing that would make it a tiny bit faster would be to set “clock” to a local variable, though I have no idea if it will make much of a difference.

https://www.lua.org/gems/sample.pdf (pg. 3)

local clock = os.clock

Good luck! :smiley:

I prefer to use os.clock(). Thank you for the advice though.

Not likely

Thanks for the preformance/optimization information :mag: :grinning_face_with_smiling_eyes:

These sorts of optimisations don’t apply to luau as roblox has a system called imports which localises all if not most of roblox’s standard libraries like os and the math library

thanks for linking my post.

could you stress test these? and check how much they lag the frame rates?

i recommend you doing it with 50-100ms loops.