Avoiding wait() and why

Threads yielded with wait(…) have a chance to resume every other frame, so if you use wait() or wait(0) or wait(n) where n <= 1/30, your thread will resume at most FPS/2 times, or 30Hz if you aren’t dropping frames. So to clarify, it is not a fixed amount of time, everything is tied to frame rate.

22 Likes

Thanks, that’s consistent with what I’ve read. Maybe I’m getting ahead of myself. With other systems I’ve toyed around with, it might have been desirable to clamp an update at 30 ms (which it sounds like wait() would do) for certain things and use a delta in update functions to account for times when even that slower rate wasn’t sufficient. Maybe none of that is relevant to Roblox (I haven’t got around to game Ai yet, and I have no plans to script any physics). So no wait ever? Interesting.

3 Likes

So if I don’t care about micro organization, wait() can still be replaced with Stepped or Heartbeat to be more accurate?

4 Likes

Great post. This pretty much sums it up for me. Event-based programming is far superior when available, and will keep your code running like a finely oiled machine.


If you ever need a continuously-running loop, or even have to do polling for whatever reason, Heartbeat is what I prefer. I use this for polling Gamepad inputs usually, since the event-based inputs for Gamepad are buggy.

21 Likes

So, I’m not finding the post, but I read that using Heartbeat:Wait() (or Stepped, etc) as a loop won’t yield in the way that a wait() loop would. Besides potentially running twice as fast as a wait() loop, are there any practical considerations to note in the event that a slowish script can’t keep up with the Heartbeat? Will it just ignore events that it isn’t ready to handle? [Nevermind, I found the post, and my recollection of it was wrong. A connection to Heartbeat works like you’d expect.]

Maybe I should just run some tests to answer my own goofy questions :grinning:
This thread has help my understanding of a number of things. Wish I could like it more than once. Thanks Kampfkarren and all.

1 Like

If your script can’t keep up with Heartbeat, you’ve got other issues to address. Regardless, I would recommend Connecting to the event, not using :Wait() in a loop:

heartbeat:Connect(function(dt) ... end)

Somewhere in some other thread (Edit: this thread), it was mentioned that this won’t actually spawn a bunch of new threads every heartbeat. It’s smart enough to reuse the same thread. And it’s guaranteed to run every frame.

9 Likes

This is generally what coroutines are used for (coroutine.running, coroutine.yield, coroutine.resume) but they’re a bit finnicky as I mentioned.

What would you recommend instead, it’s at the very least a better solution than wait().

3 Likes

Yes but you’ll still have architectural problems: please don’t just mass replace wait() with Heartbeat:Wait(), it misses the entire point of the post.

3 Likes

The most obvious solution is to have the callback function simply execute the code you need to execute when your flag would have become true (or call another function that does it). Your example is not concrete enough to make it clear why you thing a flag and polling mechanism is required in addition to registering a callback function.

3 Likes

For that small example, yes of course. However I’m referring to situations where that would not be possible (to where an example would be larger and possibly less easy to understand).

5 Likes

Good advice in general. Only instance where I poll is when waiting for my services to load and be exposed (happens only in one script)

2 Likes

What exactly does “waiting for my services to load” mean? Are you using another anti pattern like _G where you can’t yield until it’s added? If not, why can’t your services expose an event that they fire when they load?

2 Likes

I do use _G, though I admit it probably isn’t the best thing to use for my services.
Basically, all my services expose their methods via _G.Services., but I need to make sure _G.Services exists before I can reference it from another script; Hence, I wait() on it.

It’d probably be better to swap to a ModuleScript, but not really on my priority list to do so atm.

Also, an event would require me to still wait for it (albeit I could theoretically swap to a BindableEvent and use roblox’s WaitForChild instead), and it just adds uneeded complexity for one script that polls for atmost 1 second upon player join.

4 Likes

Time to go back into my scripts and fix them all.

11 Likes

So I found a place where I’m polling, and it isn’t immediately obvious to me which way to go with this. With the risk of embarrassing myself , here goes:

I’m using a state machine to manage “scenes” and transitions. This code is part of a module that holds state for a menu screen whose background is provided by a camera flying along paths through the world.

	local maxpaths = 5	-- return after maxpaths paths
	local pointsTable, duration = GetCamPath() -- load initial values
	
	repeat
		local waitstart = tick()
		
		-- call fn that binds cam code to render step (non blocking)
		RenderCam(pointsTable, duration)
		
		-- do stuff while waiting
   		local waitduration = duration
 		pointsTable, duration = GetCamPath()
		maxpaths = maxpaths - 1
		
		-- stall until cam anim ends so next path doesn't stomp on current
		-- break immediately if "flying" has changed to false
		while wait() do
			if tick() - waitstart >= waitduration or not _M.flying then
				break
			end
		end		
	until (not _M.flying or maxpaths == 0)

Each camera path animation is handled by a function bound to the render step with RunService:BindToRenderStep. That binding step just sets things up and returns. The render step code isn’t blocking, so I added the wait loop to create a pause so that one path doesn’t immediately stomp on the previous one. There is a “flying” flag that is set by an event that is triggered when a button is pressed in the gui and it’s time to transition to another screen/scene. There is also a maxpaths counter that breaks the loop if the user isn’t making a selection, but I may not keep that.

Looking at the advice above, it looks like I want to have another event inside the bound RenderStep code to get rid of the “while wait()” loop. I’m getting confused about how to structure the repeat loop that cycles through each camera path if it’s tied to those events. Chaining things together without a loop seems overly complicated. Would I need to disconnect an event that’s set up inside the bound renderstep function or would that go away cleanly when I unbind the function? This bit of code may need to be reimagined from the ground up, but everything I’m coming up with seems much more complicated than what I have. Is there a case to be made for using wait() in order to avoid complexity?

I ran into two other places in my code where I could directly apply the advice above, and I feel my code is better for it. All this seems like something to aspire to and valuable info for amateurs, though some of the “never use wait” sentiment seems excessive for anyone but the top tier Roblox programmers… who probably already know this stuff.

2 Likes

You can do something like:

local yield = Instance.new("BindableEvent")

delay(duration, function()
    yield:Fire()
end)

SomeEventYouFireWhenTheyStopFlying:Connect(function()
    yield:Fire()
end)

yield.Event:Wait()
9 Likes

Wow, that was a quick reply. Thanks! I’ll experiment with that.

2 Likes

Should spawn also be avoided? It is effectively the same as creating a new coroutine and putting wait() at the start of it.

3 Likes

Theoretically, yes–You should prefer coroutine.wrap or the fast spawn idiom (spawning a BindableEvent with a connection then firing it). There’s no guarantee your spawn will run, even. That being said, I use spawn anyway when timing doesn’t matter and I’ve never had issues with it.

2 Likes

I have no idea if this needs a seperate post but while I was messing around with while loops (for a new project) I experienced that a while wait(0.001) is not accurate (like really not). I tested it by comparing the miliseconds (0.001ms = 1s) * 1000 to a 1 second timer.