Spawn/Coroutine/Wait and Smoothing Out Code

Hello, All,

I come today with another entry to the glorious saga of “Spawn vs. Coroutine” that can be found by typing exactly that phrase into the search bar; however, I come forward with a problem that is perhaps opposite to those posed in all of the other threads: how do I smooth out my code?

My project has a great deal of interface tiles that need to be initialized at the beginning of the game (at the time of writing, it is about 3,200 tiles). All of these tiles have text marquees on them that needs to be properly sized using TextService and a number of user-input-focused events hooked up to them–including resizing the text if the container is resized. I’ve done a few iterations of code to get the execution time down to a satisfactory level, but I still fear the impact on the user.

Up until this point, the code supporting the tiles was lightweight enough that I could just iterate through them, but I’m at the point now where Studio aborts execution of the code if it is all on one thread. If I run the initializer for each tile in a coroutine, I get the fastest results, but there is a huge framerate impact. Spawn seems to keep Studio/the client on the load screen until the code executes, which seems like a better solution, but I don’t understand spawn well enough to know if I could run into problems using it.

I’ve also tried running the code from the primary thread with RunService.Heartbeat:Wait()'s thrown in between each tile, and while the framerate of that implementation is the highest of the lot, that un-nuanced, broad-stroked response results in some of the tiles taking nearly a minute to pop in. I do recognize that there is likely a middle ground on this approach, but I need help seeing it.

TL;DR - What is a good way to spread out my code’s execution so that it doesn’t tank the user’s framerate but still executes in a fairly unnoticeable timeframe?

I’ve answered this in a another thread:

The gist is to yield conditionally depending on how much time has passed.

local Budget = 1/60 -- seconds

local expireTime = 0

-- Call at start of process.
function ResetTimer()
	expireTime = os.clock() + Budget

-- Call where appropriate, such as at the top of loops.
function MaybeYield()
	if os.clock() >= expireTime then
		wait() -- insert preferred yielding method

Choosing a budget depends on how much you already have going on in your game. A budget of 1/60 basically consumes the entire frame. Something like 1/60 * 0.25 would spend 25% of the frame doing your work, leaving the remaining time for the rest of the game. You could also set the budget dynamically, but that would be more complicated.

Also worth noting that the implementations of wait, spawn, and delay were recently changed to have more sensible throttling.