Avoiding wait() and why

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 https://github.com/LPGhatguy/roblox-lua-promise 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

I always used:

repeat wait() until players.LocalPlayer.Character

I never knew

local character = player.Character or player.CharacterAdded:Wait()

was faster/even existed so will be using this from now on! Great tutorial!

4 Likes

Dang it, sorry for the necro-bump!

Thank you so much for explaining this. wait() is generally snake oil when it comes to performance and such.

The worse thing is, wait(5) can make the script wait at least 10 seconds if the client is not performing well in terms of FPS. So that’s why I’d prefer anything but wait().

Though we’ve embarrasingly used wait() in our lives, it isn’t like that will be removed any later on. It’s a good start for newcomers, though. They’ll eventually learn to use :Wait().

1 Like