Avoiding wait() and why

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.

wait has a minimum of about 1/30th of a second, but inherently wait isn’t accurate.

If you need accuracy with wait you should probably use the deltaTime returned by wait.
image
It yielded for about 0.21 seconds even though I input 0.2.

For the best accuracy, you should probably use an accumulator and possibly run the code multiple times every frame.

local Rate = 0.001 -- 1000 times a second
local Accumulated = 0
local RunService = game:GetService"RunService"
RunService.Heartbeat:Connect(function(deltaTime)
	Accumulated = Accumulated + deltaTime
	while Accumulated >= Rate do
		Accumulated = Accumulated - Rate
		--block
	end
end)
6 Likes

The only way to get almost perfectly accurate wait times that are higher than 0.03 is to wait for a bit less than you want and add a busy wait to get a precise delay. Though this is somewhat intensive so I would avoid at all costs unless it’s indispensable.

local precisewait = function(timetowait)
	local starttime = tick()
	if timetowait>=0.04 then
		wait(timetowait-0.01)
	end
	while tick()-starttime<timetowait do end
end

I got results that were off by only 0.001 of a second max, but usually they are around 0.0001 or even less.

1 Like

I advise against ever doing this. There probably won’t ever be an instance where you’ll need to yield down to less than a frame, so using Heartbeat and adding the delta time together is the preferred method for ‘accurately’ yielding. That’s accurate down to a single frame, so it’s probably as accurate as you need.

local Heartbeat = game:GetService("RunService").Heartbeat
local function accurateWait(n)
  local elapsed = 0
  while elapsed < n do
    elapsed = elapsed+Heartbeat:Wait()
  end
  return elapsed
end
13 Likes

That is why I said that it gets a nearly perfect result, but the CPU time it consumes makes it a thing to void at all costs.

1 Like

Can you give some examples of real functions to give me a better idea of how to use this?

1 Like

Actual applications of these are harder to both write and understand, I’m not sure what’s wrong with the actual examples.

1 Like

Javascript has promises and async methods. C# has Task<>. From what I know, C++ could utilise the same functionality using CLI.

Yes, we don’t have yet a native form for Lua (that is allowed in the sandbox at least), but using BindableEvents have been tested to be an efficient workaround.

1 Like

The always fantastic LPGhatguy has created GitHub - LPGhatguy/roblox-lua-promise: (NO LONGER MAINTAINED) An implementation of promises akin to JavaScript's Promises/A+ if you want JavaScript-style promises. Using this in a few places, and love it.

1 Like

If you’re planning to use larger values (such as 10 seconds), it should run fine; it wouldn’t hurt for a countdown to fluctuate by 0.1-1%.

1 Like

I would’ve expected someone to have made them a thing by now; they’ve even stolen Jasmine (a testing framework) and stuck it into the CoreScripts.

1 Like