Why does yielding this thread prevent a new thread from being ran?

I’m making a timer that counts down when a RemoteEvent is fired, but it creates a timer that overlaps with a timer that’s already running. My first idea was using coroutines and yielding any timer that is still happening.

Unfortunately, for some reason that stops me from replacing the timer thread variable with a new thread and running it.

local function updateTime(seconds: number)
	if timerThread ~= nil then
		print("There's already a timer thread, yielding")
		coroutine.yield(timerThread)
		timerThread = nil
	end
	
	timerThread = coroutine.wrap(function(Time)
		print("Starting new timer thread")
		for second = seconds, 0, -1 do
			local minutes = second/60%60
			local seconds = second%60
			timeText.Text = string.format("%02i:%02i", minutes, seconds)
			switchTick:Play()
			task.wait(1)
		end
	end)
	
	timerThread(seconds)
end

I tried using RunService and disconnecting old connections, but that still made timers overlap with one another. I tried using booleans and toggled it to stop old connections, but that also completely stopped the timer from even working, so I’m out of options. I’d appreciate any help with this

1 Like
coroutine.yield(timerThread)

This isn’t valid, first of all coroutine.wrap() returns a function not a thread and second of all the coroutine.yield() function expects 0 arguments, it simply yields the execution of the current coroutine.

local Thread
local function CreateThread()
	if coroutine.running() == Thread then coroutine.yield() end

	Thread = coroutine.create(function() CreateThread() end)
	coroutine.resume(Thread)
end
CreateThread()
1 Like

Does “overlaps with timer that’s already running” just mean that you want to be able to cancel the existing timer and replace it with a new one, but you get mult timers trying to change the timeText.Text and play the animation/sound/or whatever?

Anyway, I can’t remember ever trying to make a countdown timer in Lua that was more than a simple wait(1) with some text formatting. I played around a bit with this idea that you are welcome to use/experiment with. Maybe it will spark some ideas. Doubtful it is the right way to go about this as-is, and it’s a bit slap-dash, so apologies for that. It uses Heartbeat for the timer (in it’s own coroutine) and another coroutine with an update function. It can be paused and resumed or reset to a new countdown.

local RunService = game:GetService("RunService")
local ONE_SECOND = 1

local function useAsCallback(t)
		local minutes = math.floor(t/60)
		local seconds = t%60
		print(minutes, ":", seconds)
		--timeText.Text = string.format("%02i:%02i", minutes, seconds)
		--switchTick:Play()
end

-- Timer Class
-----------------------------------
local Timer = {}
Timer.__index = Timer

function Timer.new(seconds)
	local self = {
		_accumulator = 0,
		secondsInitial = seconds,
		secondsRemaining = seconds,
		running = false,
		heartBeat = nil,
		heartBeatco = nil,
		update = nil,
		updateCallbackFn = nil,
	}
	setmetatable(self, Timer)
	
	return self
end

function Timer:Cleanup()
	self:Pause()
	self._accumulator = 0
	self:destroyHeartBeatTimer()
	self:destroyUpdateCoroutine()
end

function Timer:Pause()
	if self.running then
		self.running = false
	end
end

function Timer:Reset(t)
	self:Pause()
	if t and type(t) == "number" then
		self.secondsInitial = t
	end
	self.secondsRemaining = self.secondsInitial
	self:Start()
end

function Timer:Start()
	if self.running then
		return
	end
	if self.secondsRemaining <= 0 then
		self.secondsRemaining = self.secondsInitial
	end
	if self.heartBeatco == nil then
		self:createHeartBeatTimer()
	end
	self.running = true
	coroutine.resume(self.update, self)
end

function Timer:attachCallbackFn(fn)
	self.updateCallbackFn = fn
end

function Timer:createUpdateCoroutine()
	self:Pause()
	self:destroyUpdateCoroutine()
	
	self.update = coroutine.create(function()
		while true do
			if self.updateCallbackFn then
				self.updateCallbackFn(self.secondsRemaining)
			end
			if self.secondsRemaining <= 0 then
				self.running = false
				task.defer(self:Cleanup())
			end
			coroutine.yield()
		end
	end)
end

function Timer:destroyUpdateCoroutine()
	if self.update then
		self:Pause()
		coroutine.close(self.update)
	end
end

function Timer:createHeartBeatTimer()
	self:Cleanup()
	self:createUpdateCoroutine()
	
	self.heartBeatco = coroutine.create(function() 
		self.heartBeat = RunService.Heartbeat:Connect(function(step)
			if self.running == true then
				self._accumulator += step
				while self._accumulator >= ONE_SECOND and self.secondsRemaining > 0 do
					self._accumulator -= ONE_SECOND
					self.secondsRemaining -= ONE_SECOND
					coroutine.resume(self.update, self)
				end
			end
		end)
	end)
	coroutine.resume(self.heartBeatco)
end

function Timer:destroyHeartBeatTimer()
	self:Pause()
	if self.heartBeat then
		self.heartBeat:Disconnect()
		self.heartBeat = nil
	end
	if self.heartBeatco then
		coroutine.close(self.heartBeatco)
		self.heartBeatco = nil
	end
end
Test
-- test
wait(4)
print("create timer with t=15 seconds")
local T = Timer.new(15)
T:attachCallbackFn(useAsCallback)
wait(2)
print("start countdown")
T:Start()
wait(5)
print("pause")
T:Pause()
wait(5)
print("resume")
T:Start()
wait(2)
print("Start while already started--shouldn't break")
T:Start()
wait(5)
print("reset with t=15")
T:Reset(15)
wait(10)
print("reset again with t=10")
T:Reset(10)
wait(12)
print("start again with existing values")
T:Start()
wait(10)
print("destroy the Timer object")
T:Cleanup()
T = nil
print("end")
1 Like

This is probably a result of how I coded it, but it still overlaps with a timer instead of replacing the existing timer.

local function updateTime(seconds: number)
	if coroutine.running() == timerThread then
		print("Yielding")
		coroutine.yield()
	end
	
	timerThread = coroutine.create(function()
		updateTime()
	end)
	coroutine.resume(timerThread)
	
	print("Starting new timer thread")
	for second = seconds, 0, -1 do
		local minutes = second/60%60
		local seconds = second%60
		timeText.Text = string.format("%02i:%02i", minutes, seconds)
		switchTick:Play()
		task.wait(1)
	end
end

Here’s a video I should have put in the first place of the problem:

This was the one that fully worked, thank you so much!
Code for anyone having similar problems:

-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Timer = require(ReplicatedStorage:WaitForChild("Timer")) -- Timer module

-- Timer variables
local newTimer = Timer.new(3600)

-- Functions
local function callbackThread(t)
	local minutes = math.floor(t/60)
	local seconds = t%60
	timeText.Text = minutes..":"..string.format("%02i", seconds)
	switchTick:Play()
end

local function updateTime(seconds: number)
	newTimer:attachCallbackFn(callbackThread)
	newTimer:Start()
	newTimer:Reset(seconds)
end

Once again, thank you, I can finally stop stressing over this small problem.

1 Like