Script Not Yielding After Calling :Wait() on BindableEvent

For a timer-based module I was working on, I tried to wait for a coroutine to stop. However this required me to make sure the coroutine stayed alive until the timer is up.

I tried using a while true do end loop akin to something like

while true do 
	task.wait()

	if Connection.Connected == false then
		coroutine.yield("TimerFinished")
	end
end

However I ended up with countless warning that said
task.wait should not be called on a thread that is already 'waiting' in the task library

Next I tried using a BindableEvent and waiting for it to fire. However for some reason the script isn’t waiting for the BindableEvent to fire and instead just continues normal.

This is the code if you’re wondering

local module = {}

local RunService = game:GetService("RunService")

export type NewTimerValues = {
	-- required
	Length:number,
	EndFunction:any,
	
	-- depends
	RemoteEvent:RemoteEvent?, -- if no function for Function then make sure to put a remoteevent for this so it can fire
	
	-- not required
	Function:any?
}

-- makes a new coroutine which has a timer
-- pass a table for timervalues
function module.newTimer(TimerValues:NewTimerValues)
	local CurrentTime = 0

	local TimerCoroutine:thread
	
	TimerCoroutine = coroutine.create(function()
		local Connection:RBXScriptConnection
		local BindableEvent = Instance.new("BindableEvent")
		
		Connection = RunService.Heartbeat:Connect(function(DeltaTime)			
			CurrentTime += DeltaTime
			if CurrentTime > TimerValues.Length then
				Connection:Disconnect()
				TimerValues.EndFunction()
				BindableEvent:Fire()
				print("bindableevent fired")

				return
			end

			if TimerValues.Function then
				TimerValues.Function()
			else
				if TimerValues.RemoteEvent then
					TimerValues.RemoteEvent:FireAllClients(math.round(TimerValues.Length - CurrentTime))
				else
					warn("WHERE IS THE REMOTEEVENT??")
				end
			end
		end)
		
		-- returns "TimerFinished" using coroutine.resume
		BindableEvent.Event:Wait()
		print("yielded")
		coroutine.yield("TimerFinished")
	end)
	
	return TimerCoroutine
end

-- starts timer
-- equivalent to coroutine.resume(TimerCoroutine)
function module:StartTimer(TimerCoroutine:thread)
	coroutine.resume(TimerCoroutine,"TimerFinished")
end

-- waits for timer to finish
function module:WaitForTimer(TimerCoroutine:thread)
	local Success, Result 
	
	repeat
		task.wait()
		Success, Result = coroutine.resume(TimerCoroutine)
		print(Success,Result)
	until Success == false or Result == "TimerFinished"

	if Success == false then
		error("Timer encountered an error. Error message: "..Result)
		return
	elseif Result == "TimerFinished" then
		return true
	else
		return false
	end
end

return module

Could anybody tell me why the script isn’t waiting for the BindableEvent to fire?

Can you show an intended example usage of this module?

The issue is with your WaitForTimer function. The thread you created in the newTimer function is yielding on the bindable event and an additional coroutine.yield call. WaitForTimer is consistently attempting to resume the yielding thread each frame, which by the second frame it will have resumed the yield caused by RBXScriptSignal:Wait and coroutine.yield.

Your WaitForTimer function is redundant. Return BindableEvent.Event for the caller to yield on.

I developed something like this a while back, with absolute disregard to overengineering. I developed the system with no serious intent, so upon re-reading the code a while back, I believe I found a bug. I do not remember what it was, so take the source code with a grain of salt

So, I believe that the problem is that your calling task.wait() within a coroutine, that already is searching for the Bindable Event, this would cause unnecessary delays and confuse your script.

You could fix this by restructuring your WaitForTimer function, to not confuse your script. For future reference, you shouldn’t add in a task.wait() inside of a coroutine, instead use a BindableEvent.

This should be your script:

local module = {}

local RunService = game:GetService("RunService")

export type NewTimerValues = {
	-- required
	Length:number,
	EndFunction:any,
	
	-- depends
	RemoteEvent:RemoteEvent?, -- if no function for Function then make sure to put a remoteevent for this so it can fire
	
	-- not required
	Function:any?
}

-- makes a new coroutine which has a timer
-- pass a table for timervalues
function module.newTimer(TimerValues:NewTimerValues)
	local CurrentTime = 0

	local TimerCoroutine:thread
	
	TimerCoroutine = coroutine.create(function()
		local Connection:RBXScriptConnection
		local BindableEvent = Instance.new("BindableEvent")
		
		Connection = RunService.Heartbeat:Connect(function(DeltaTime)			
			CurrentTime += DeltaTime
			if CurrentTime > TimerValues.Length then
				Connection:Disconnect()
				TimerValues.EndFunction()
				BindableEvent:Fire()
				print("bindableevent fired")

				return
			end

			if TimerValues.Function then
				TimerValues.Function()
			else
				if TimerValues.RemoteEvent then
					TimerValues.RemoteEvent:FireAllClients(math.round(TimerValues.Length - CurrentTime))
				else
					warn("WHERE IS THE REMOTEEVENT??")
				end
			end
		end)
		
		-- waits for the BindableEvent to fire before yielding
		BindableEvent.Event:Wait()
		print("yielded")
		coroutine.yield("TimerFinished")
	end)
	
	return TimerCoroutine
end

-- starts timer
-- equivalent to coroutine.resume(TimerCoroutine)
function module:StartTimer(TimerCoroutine:thread)
	coroutine.resume(TimerCoroutine,"TimerFinished")
end

-- waits for timer to finish
function module:WaitForTimer(TimerCoroutine:thread)
	local Success, Result 
	
	-- Instead of task.wait, we wait until the coroutine yields
	Success, Result = coroutine.resume(TimerCoroutine)
	print(Success, Result)
	
	-- Wait until the timer finishes or encounters an error
	if Success == false then
		error("Timer encountered an error. Error message: "..Result)
		return
	elseif Result == "TimerFinished" then
		return true
	else
		return false
	end
end

return module

Let me know if your have any further questions! :smiley:

The purpose of the WaitForTimer function is to yield the caller thread until the timer is complete. This will bypass the RBXScriptSignal:Wait call and leave the thread on coroutine.yield, which is assumably now never resumed. This would leave the thread in a forever suspended state. The timer is still a thread, which means it’s continuing its work with no disruption to the caller thread of WaitForTimer

I’d probably set up a timer something more like this, where the events are provided by the Timer and you can just call :Wait() directly on the bindable event

local Timer = {}
Timer.__index = Timer

export type ClassType = typeof(setmetatable(
	{} :: {
		lengthSeconds: number,
		onTick: RBXScriptSignal,
		onFinished: RBXScriptSignal,

		_onTickBindable: BindableEvent,
		_onFinishedBindable: BindableEvent,
		_isStarted: boolean,
	},
	Timer
	))

function Timer.new(lengthSeconds: number): ClassType
	local onTickBindable = Instance.new("BindableEvent")
	local onFinishedBindable = Instance.new("BindableEvent")

	local timer = {
		lengthSeconds = lengthSeconds,
		onTick = onTickBindable.Event,
		onFinished = onFinishedBindable.Event,

		_onTickBindable = onTickBindable,
		_onFinishedBindable = onFinishedBindable,
		_isStarted = false,
	}

	setmetatable(timer, Timer)

	return timer
end

function Timer.start(self: ClassType)
	assert(not self._isStarted, "Timer is already started")
	self._isStarted = true

	local startTimeMillis = DateTime.now().UnixTimestampMillis
	local endTimeMillis = startTimeMillis + self.lengthSeconds * 1000
	local elapsedTimeSeconds = 0

	task.spawn(function()
		repeat
			local isDestroyed = self._onFinishedBindable == nil
			if isDestroyed then
				break
			end

			local nowMillis = DateTime.now().UnixTimestampMillis
			local elapsedTimeMillis = nowMillis - startTimeMillis
			local millisRemaining = endTimeMillis - nowMillis

			local nowElapsedTimeSeconds = elapsedTimeMillis // 1000
			if nowElapsedTimeSeconds ~= elapsedTimeSeconds then
				local secondsRemaining = 1 + millisRemaining // 1000
				self._onTickBindable:Fire(secondsRemaining)
				elapsedTimeSeconds = nowElapsedTimeSeconds
			end

			if millisRemaining > 0 then
				local millisUntilNextTick = millisRemaining % 1000
				millisUntilNextTick = if millisUntilNextTick == 0 then 1000 else millisUntilNextTick
				task.wait(millisUntilNextTick / 1000)
			end
		until millisRemaining <= 0

		if self._onFinishedBindable then
			self._onFinishedBindable:Fire()
		end
	end)
end

function Timer.destroy(self: ClassType)
	self._onTickBindable:Destroy()
	self._onFinishedBindable:Destroy()
end

return Timer

So you could use it like this

local Timer = require(script.Parent.Timer)

local timer = Timer.new(10)
timer.onTick:Connect(function(...)
	print("Tick", ...)
end)
timer.onFinished:Once(function()
	print("Finished!")
end)

timer:start()
timer.onFinished:Wait()
timer:destroy()
print("Continuing with the script")

And the output would be

00:00:06.077  Tick 9
00:00:07.077  Tick 8
00:00:08.077  Tick 7
00:00:09.077  Tick 6
00:00:10.077  Tick 5
00:00:11.076  Tick 4
00:00:12.077  Tick 3
00:00:13.076  Tick 2
00:00:14.076  Tick 1
00:00:15.076  Tick 0
00:00:15.076  Continuing with the script
00:00:15.076  Finished!

Basically, yeah. That’s how I approached mine

Thanks! I understand now, I made it work by returning a table with the thread and a BindableEvent for the script to wait on.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.