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?
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
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")