When to yield a couroutine (or break the loop) for a loading screen effect? Am I using coroutines right, even?

Earlier today, while scripting my main menu, I wanted to add an aesthetic effect where a set of four dots would tween in order from left to right from visible to invisible. I got rid of it because I wasn’t sure whether I was using coroutines correctly or not, or if it would achieve my desired effect.

I don’t think seeing the object hierarchy is necessary, so I’ll provide you with an outline of the code I was using to attain this effect. Assume the variables I use already exist.

coroutine.wrap(function ()
    while true do
        if not Loading then coroutine.yield() break end

        for n in next, #Dots:GetChildren()
            local Tween = TweenService:Create(Dots["Dot"..n], TweenInfo, Goals)
            Tween:Play()
            Tween.Completed:Wait()
        end

        -- ^ Repeated to make them invisible below

    end
end)()

This code is successfully able to attain the fade in effect that I desire for my main menu. The issue lies in halting this coroutine. I want it to stop only after the dots have been made invisible. That also spawns other issues that I wish to determine:

  • Am I using coroutines right?
  • How do I make the script not do anything until a full run of this goes? I’ve been thinking of creating a “pass” upvalue where its set to false at the top of the loop and true at the bottom, with repeat wait() until Pass == true around the bottom after preloading has finished.
  • If I’m not doing this right, how can I achieve an effect where a loading aesthetic runs in parallel to some other form of live-updating code? I want the loading percentage to update real time while this aesthetic is running, but I don’t want the next screen to progress until preloading is done AND the dots are invisible.

If I’m not mistaken, coroutines are usually called and run in another environment, but I don’t think you’re misusing it.

Might be useful.

I am not certain whether you should want to use yield; if you want to resume it later the break would stop it, while simply substituting yield and break for return should set the coroutine state to dead as well. (the moment a coroutine returns the state is set to dead and execution stops)

I’d try doing this the other way around; if the part executing parallel to the routine sets ‘finish_loading’ or some other flag to true, the coroutine returns (after executing a full loop). You could wait till its state is dead to resume execution of the code below that;

– preload is done
finish_loading = true – causes coroutine to return after its current loop
while coroutine.status( that_loading_routine ) ~= “dead” do wait(.1) end
– continue about your business

Coroutines run within the same environment, they do run in a simulated thread. I assume that’s what you meant to say.

I’m not so sure I understand what you mean with the middle response. I don’t normally use coroutines except as alternatives to spawn.

From what I gather, you want to make sure a full execution of the loop is done AND the bottom code is done doing some other task. If the bottom code is ready to continue, the coroutine should stop executing after it’s done one full loop, correct?

To achieve that, the coroutine executes its loop and then checks for a variable that’s set externally. If it’s set to true, the routine returns and stops executing.

Below the coroutine is other code that at some point decides it’s done. When this happens, it tells the coroutine to stop executing after its current loop (by setting the variable to true), and then proceeds to wait for that loop to actually be done by checking the coroutine status. (“dead” means it has returned and stopped executing)

You don’t need coroutine.yield, just the break statement (which is never reached with the code as-is). You have no way to resume the coroutine, because you’re not using the return value from coroutine.wrap, which would be the coroutine reference you’d need to resume it. It appears you want this to only run once, so that’s fine.

1 Like

What I want to achieve is this workflow:

  • Two things are done in parallel; each dot out of a set of four is faded in and out, while the percentage bar updates each time an asset has finished back from PreloadAsync
  • When all assets are passed into PreloadAsync, then the code yields until all the dots have been faded out (or if the dots are already faded out, it just continues on)

I don’t really understand anything else beyond that.

local co = coroutine.create(function()
	while true do
	-- dot stuff in a loop
		if end_anim then
			return
		end
	end
end)

coroutine.resume( co )

while requestqueuesize > 1 do
	-- set bar process
	wait(.1)
end
end_anim = true
while coroutine.status( co ) ~= "dead" do 
	wait(.1) 
end -- wait till the coroutine is done with its current loop
-- continue doing things

When you use tweenservice there’s a property inside of tweeninfo you can change so it stops, and doesn’t loop. I’m almost certain on the wiki it’s set to -1 which is repetitive but if you set it to 0 or above it will stop after a certain number of times.

@TaaRt Would using break as opposed to return also kill the coroutine?

@Arcerion The TweenInfo associated with the Tween is a read-only property. Once you create a Tween, you can’t change the info property. You have to either cancel it out with an overlapping tween or call cancel on the Tween itself. I also can’t use repeat count because loading time is variable.

That’s to note I’m not using RequestQueueSize.

You can set coroutines as variables, and yield them;

local coroutineVar = coroutine.create(function()
-- stuff here
end)

coroutine.yield(coroutineVar)

and to run it you type coroutine.resume.

I’m confused as to how this is relevant to the question I posed. I’m aware this can happen. As well, you cannot explicitly call coroutine.yield like this - it has to be called within the coroutine itself.

Whenever the function is done executing its body the coroutine will enter a ‘dead’ state. Whether you use break or return in this case doesn’t matter, and is very much personal preference. You could have scenarios where there’s code below the loop, which you do not want executed (return would ensure immediate exit rather than just the current loop).

2 Likes

That’s not how that works. You can only yield a coroutine from within the coroutine itself. Calling coroutine.yield(coroutineVar) does something different entirely.

Take a look at the following example.

local coroutineVar

coroutineVar = coroutine.create(function ()
    coroutine.yield(coroutineVar)
end)

print(coroutine.resume(coroutineVar))

This will print out the coroutine object, since yield passes its parameters to resume. You could change the parameter to anything else, and it’d print that instead.

1 Like

That didn’t work. I even isolated the code and created a bare bones version of this.

local a = coroutine.wrap(function ()
	while true do
		wait(1)
	end
end)()

while not coroutine.status(a) == "dead" do wait() end

The following error was spat back out:

My bad, coroutine.wrap doesn’t behave the same as coroutine.create. The value returned by coroutine.create should be the thread (co), this requires you to run the coroutine by calling coroutine.resume(co) after creating it.

local co = coroutine.create(function ()
	while true do
		wait(1)
	end
end)
coroutine.resume(co)

while not coroutine.status(co) == "dead" do wait() end

Firstly, Lua is singled threaded. This means that anything you do will always be sequential, even if it appears to run in parallel. For this reason, coroutines are essentially psuedo-threads.

If I understand correctly, you want your code to yield until all the dots have faded out, and that’s totally achievable!

Here’s some code that will hopefully set you in the right direction.

local thread
local taskIsCompleted = false

local runTask = coroutine.wrap(function ()
    -- do something which takes time (fading the dots)
    if thread then
        coroutine.resume(thread)
    else
        taskIsCompleted = true
    end
end)

runTask()

-- do something else which takes time (preloading assets)

if not taskIsCompleted then
    thread = coroutine.running()
    coroutine.yield()
end
1 Like

I’m aware of that, but thanks for correcting me as it was inaccurate help I provided!

1 Like