Instance.Event:Wait(n) with n being time till timeout function

Have you ever wanted to do something like this?

local hit = part.Touched:Wait(5) -- With 5 being the time until timeout (like :WaitForChild(5))

if hit then
 -- Something touched part within the 5 seconds!
else
 -- Nothing touched part
end

Well now you can!

(but not with that simple syntax :grin:)

local function WaitUntil(event, timeout)
	local timedOut = Instance.new("BindableEvent") -- This is used to return whether or not the event listening has timed out (while also yielding it with a timedOut.Event:Wait())
	local result = nil -- This will be a tuple of what arguments the callback function receives. For ex. local hit = part.Touched:Wait() with "result" being "hit" 

	local function fireTimedOut(bool) -- "bool" here represents whether or not we timed out
		timedOut:Fire(bool) -- As soon as this happens, the script will return: timedOut (as "bool") and "result" (tuple of passed args to callback function)
		timedOut:Destroy() -- Cleanup
	end

	local eventConn; eventConn = event:Connect(function(...)
		eventConn:Disconnect() -- Disconnect the event immediately so we don't do anything if it fires again (we are only listening for 1 happening because of :Wait())
		result = {...} -- Set the result to the passed args from the event firing
		fireTimedOut(false) -- We are now ready to return: false, unpack(result)
	end)

	task.delay(timeout, function()
		if eventConn.Connected then -- If the event is still connected, that means it didn't fire within the "timeout" duration; therefore, we timed out
			eventConn:Disconnect() -- Same reason as above
			fireTimedOut(true) -- Same reason as above, but now we're returning timedOut as true
		end
	end)

	return timedOut.Event:Wait(), unpack(result or {}) -- Keep in mind here that it's important the timedOut.Event:Wait() is first or else the result tuple will always be nil
end

-- Example usage
local part = Instance.new("Part")
part.Position += Vector3.new(0, 5, 0)
part.Anchored = true
part.Parent = workspace

local s = os.clock()
local timedOut, hit = WaitUntil(part.Touched, 5)

if not timedOut then
	print("part.Touched:Wait() returned", hit) -- This is basically what has happened
else
	print("part.Touched:Wait() timed out after", os.clock() - s, "seconds") -- ^^^
end

If you have any questions or concerns please let me know by replying to this post, thanks!

4 Likes

I highly recommend stop using BindableEvents for manual resumption of threads, you can check out FastSignal’s :Wait implementation, it uses task.spawn, coroutine.running, and coroutine.yield to achieve that, it is faster, and also uses way less memory, not having to create an instance, and not having to create a table for arguments.

Which by the way, if you were to stay with BindableEvents, these args arent being handled in the best way, as having a nil argument can make any arguments after that disappear.

You can look into how table.pack and table.unpack works by seeing Nevermore’s Signal implementation.

-- Quick rewrite:

-- Should not *break* with deferred events
local function WaitUntil(
    event: RBXScriptSignal | any,
    timeOut: number
): (boolean, ...any)

    local thread = coroutine.running()

    local connection = nil
    connection = event:Connect(function(...)
        if connection == nil then
            return
        end

        connection:Disconnect()
        connection = nil

        task.spawn(thread, false, ...)
    end)

    task.delay(timeOut, function()
        if connection == nil then
            return
        end

        connection:Disconnect()
        connection = nil

        task.spawn(thread, true)
    end)

    return coroutine.yield()
end
2 Likes

Wow, thank you. I was trying to understand how to do it with coroutines before using bindables, but I never did end up understanding what coroutine.yeield() returns along with coroutine.running().

1 Like