How to creating a thread that loops infinitely and can be halt/cancelled immediately?

What is this for?
An intro screen that has GUI elements running their animation in the background.

Sample code:

local self = {}
--Define GUI Objects and Tweens.

function self:UIAnim_Run()
  --Use method to create a running thread for this infinite loop.
  while true do
    MyUIObject.Position = PositionBlahBlah --Reset the GUI's properties.
    MyTween1:Play()
    MyTween1.Completed:Wait()
    MyTween2:Play()
    MyTween2.Completed:Wait()
    wait(3) --Cooldown before playing the sequence again.
  end
end

function self:UIAnim_Stop()
  --Cancel the thread that's running created by self:UIAnim_Run()
end

function self:Activate()
   --Insert intro sequence
   self:UIAnim_Run() --Make things idle in the background
end

function self:Deactivate()
   self:UIAnim_Stop()
   MyTween1:Cancel()
   MyTween2:Cancel()
end

return self

--vvv Somewhere else outside the modulescript vvv
modulescript:Activate()
--Some times later
modulescript:Deactivate()

Method tried: Using coroutine and a boolean to break the while do as soon as possible but when the script is currently in its wait state everything goes wrong:

local running = false

coroutine.wrap(function()
	running = true
	while running do
		print("> This is running.")
		wait(3)
		print("This shouldn't print after being stopped.")
	end
	print("This has to print immediately after stopping.")
end)()

wait(7)
print("Stopping!")
running = false

Using while loops to do asynchronous tasks is a bad idea. You should be creating a separate thread for actions like this, preferably using some sort of timer system.

Using this small module with this module, you can set up actions to occur every frame, like this:

local RunService = game:GetService("RunService")
local FlashEvery = require(...FlashEvery)
local CurveUtil = require(...CurveUtil)

local everySecond = FlashEvery:new(1)

RunService.Heartbeat:Connect(function(dt)
   local dtScale = CurveUtil:DeltaTimeToTimescale(dt)
   everySecond:update(dtScale)
   
   if everySecond:do_flash() then
       -- do what you want here
   end
end)

By doing this, you avoid the bad code mess that typically stems as a result of wrapping synchronous actions in a new thread. Knit provides an excellent all-in-one package for handling stuff like this with the Timer module.

Here is a usage example:

local timer = Timer.new(2)
timer.Tick:Connect(function()
    -- code
end)

timer:Start()

-- When we want to, we can just disconnect the callbacks we bound to the timer:

timer.Tick:DisconnectAll()

Doing things asynchronously leads to much fewer bugs, more maintainability, and more clean code.

2 Likes