Outdated {view new post linked below}

There’s better ways of handling fire rate for low fps players.

Queuing and then firing all of their shots on the next frame is bad practice since it gives them an unfair advantage - a lot of guns become shotguns at that point.

It’s better to use tick and delta time imo.

But how does that handle having a weapon that shoots around 40 times a second run the same for people with 60 FPS and 20 FPS?

Well @nooneisback is on the right track and all. The Accuwait system basically queues up the firing until the next frame automatically because my fire function is recursive and only yields for the Accuwait.

@TheRings0fSaturn Made a point about this potentially causing problems on low framerates, as it’s possible for multiple rounds to be dumped in a single frame. However, it’s a moot point, since the firerate for weapons in FPS games varies anywhere from 60 RPM (1 shot per second) and 1350 RPM (22 shots per second). On the high end of firerates, in order for multiple rounds to be fired in a single frame, the user would have to be running slower than 25 FPS. Even then, you’d only be seeing 2 rounds fired sub-frame. So it being a “shotgun” isn’t really all the much of a possibility.

Just for fun we can calculate when that’d start being a problem. Lets assume players would never be lower than 15 FPS (Because why would you play the game at that point). In order to have a decent shotgun effect, you’d need 8 or more rounds fired in a single frame. So their weapon’s firerate would need to be at least 7200 RPM.

Finally I’d like to wrap up why the Accuwait system seems better for me than using normal waits or RunService.Stepped:wait(). It’s an elegant solution that allows me to use the replace tool to replace any usage of wait() with _G.AccuWait() and it doesn’t negatively affect functionality. It however, instantly opens up the possibility of sub-frame operations, something not possible with RunService.Stepped:wait() and not even close to possible with the normal wait(). Alot of my repeating functions utilize recursion, and then an event yield. So I didn’t have to make any additional changes/modifications anywhere, it just worked instantly without issue.

1 Like

I’m going to put this here, I’ve really been trying to avoid advertising any of my projects in this thread, but I feel like it exemplifies both the issue, and how the AccuWait function solved it nicely.

This is Fray 1, it uses version 4.2 of my Apposition Engine. The Fire function is recursive much like version 4.9, but it still uses the default wait() function just about everywhere.
https://www.roblox.com/games/1963760996/Fray-ALPHA

In Fray 1, you’ll notice a few things. The weapons don’t feel as responsive, the firerates and animations seem almost locked to the framerate. The slower the game runs, the slower everything is.

This is Fray 2.0 (to be renamed). It runs on version 4.9 of my engine, among lots of improvements, it uses the AccuWait() system for everything including Animations and the fire() function. You can definitely notice a huge difference, despite the stats on most of the weapons being the same as Fray 1.
https://www.roblox.com/games/3449026753/Fray-2-Dev

2 Likes

This is not a good technique. A much better method to run code on a fixed time step is to accumulate the frame time, and only run the code when enough time has been accumulated, like so:

-- fixed time step, in seconds
local RATE = 1/60

local Heartbeat = game:GetService("RunService").Heartbeat
local accumulator = 0
local lastFrameTime = Heartbeat:Wait()

while true do
	accumulator = accumulator + lastFrameTime
	while accumulator >= RATE do
		-- code runs here !
		accumulator = accumulator - RATE
	end
	lastFrameTime = Heartbeat:Wait()
end

I highly recommend against AccuWait. It’s incredibly unnecessary, quite strange (absolutely a hack, regardless of your opinion), and makes a number of questionable assumptions.

2 Likes

There are much better methods of making accurate physics engines than by using a higher event rate.

For example, for hit detection, rather than relying on touched, fire a ray between previous and current positions every frame.

I don’t see how this is that useful.

Your solution can only handle waits equal to or over the frametime. So has the same problem as the default wait service.

1 Like

I kind of just skimmed over the thread here, but I don’t see the point of all this overhead versus just using wait…? I don’t see the point in trying to run something faster than wait or a frame either, which running something more than once per frame is questionable in itself.

This code tells me that you have some code smells where you’re using waits where not required or not optimising where required, resulting in your processes hogging the task scheduler and deferring wait. Wait will always pass the allotted time though once it has, it also needs to find an open slot in the task scheduler which is the cause for additional wait time to n.

You’re leading yourself into a trap and banking on this method’s magical reliability, to which it has none. I’ve seen instances of FastSpawn, which is this but for spawn()/coroutines, take up to a whole 5-6 frames to execute in games before.

3 Likes

Did you try setting RATE to something lower than 1/60?

My example code does essentially the same thing as AccuWait (runs code a certain amount of times per frame), but without all the extra overhead and weirdness. Please understand these things: AccuWait does not guarantee that these intervals are evenly spaced, and indeed when we’re talking about sub-frame time intervals it does not matter one bit.

3 Likes

I think the primary issues of this thread are firstly, I titled it incorrectly, and the initial post focused on the wrong points of the system. Secondly, we all have our own ways of dealing with problems, and we all have had different experiences with Roblox in the past, so we all have different ideas on what systems are reliable or not.

From my perspective, I’ve had major issues in the past with the wait() service getting completely bogged down and having horribly long duration inconsistencies. Granted, the last time I had a seriously game-breaking issue with that was a few years back. However, the default wait() service is currently causing my recursive functions and loops to run slower on lower-frame-rates. I’ve also had a problem with the TweenService. Because running a tween, then using a wait() as long as the tween’s duration is to determine when it should be done almost always leaves a little hang-time. Sometimes as long as 0.14 seconds, which is really noticeable to the player. I had been compensating for this by making the wait, roughly 0.05 seconds shorter than how long the tween will operate for.

My entire engine, all 8000 some-odd lines, had functions and loops using the default wait() service. I didn’t want to go through and optimize each function individually, or rewrite entire blocks to compensate for an inaccurate wait. Instead, I made AccuWait, a simple short system that I can throw in anywhere, and use the replace tool to replace all the default waits. Most of my waits were less than a second, so I didn’t get the chance to test how it would handle long durations.

In my particular case, the benefits of the AccuWait system were instantaneously evident. All my loops and functions started working as intended even on low-framerates. The AccuWait function seemed to fix the hangtime on tweens as well. Most importantly, in comparison to your examples, is that without rewriting my functions, they could now run up to 10 times per frame. In the example of fast firing weapons on lower framerates, people have said; “just write the fire function so it can send out multiple rounds in a single frame”. Except it’s speed was relying simply on the wait(), so I don’t have to rewrite it, using AccuWait, it already does that.

That’s why from my point of view, it was the best solution, and did exactly what I needed if not more.

4 Likes

We recommend against this.

wait() is currently problematic for a few reasons (30Hz+throttling+lack of timing guarantees). This may change, but this is where we are right now.

That is why we recommend the periodic events under RunService instead for cases that require granular timings–RenderStepped, Stepped, and Heartbeat. If those events aren’t granular enough for your use case, run your code multiple times within one of those events. This is effectively what the code in this thread does anyway, just in a very roundabout way.

“AccuWait” puts a few resume points into RenderPriority. This offers no additional benefit over running your code multiple times in RenderStepped, and has negative performance implications due to the cost of resuming a thread from the task scheduler.

There are no technical justifications for this approach.

Accurate Yielding Down to Ten-Thousandths of a Second

This is not possible with the engine. If someone tries to sell you a system that claims to do that, they are not being honest.

18 Likes

Sorry to bring back this thread, but I stumbled across it again.

After reading everyone’s feedback, the general consensus is what subcritical said:

I decided to try to actually implement and test that method, and see what the difference from the default wait() would be.

Here's my function:
local RS = game:GetService("RunService")
local Bindable = Instance.new("BindableEvent")

local function AccuWait(WaitTime)	
	local StartTime = tick()
	local Waited = 0
	
	local Completed = Bindable:Clone()
	
	local SteppedConnection
	SteppedConnection = RS.Stepped:Connect(function()
		for i=1,5 do
			Waited = tick()-StartTime
			if Waited>=WaitTime then
				Completed:Fire(Waited)
				SteppedConnection:Disconnect()
				Completed:Destroy()
				break
			end
		end
	end)
	
	return Completed.Event:Wait()
end

I think the function followed the concepts taught in this thread. If I misunderstood something, please correct me!

Tests:

Test 1:
wait(2) -- Give the game a moment to start before we run the test


local pResults = {
	a = {};
	w = {};
}

for i=1,100 do
	
	local g = i/100
	local w = wait(g)
	local a = AccuWait(g)
	
	local wP = math.abs(g-w)/g
	local aP = math.abs(g-a)/g
	
	print("Wait Test Goal: "..g.."\n  Reg: "..w.." (Off by "..wP.."%)\n  Accu: "..a.." (Off by "..aP.."%)")
	
	pResults.a[#pResults.a+1] = aP
	pResults.w[#pResults.w+1] = wP
	
end

local aT, aP = 0,0
for i=1, #pResults.a do
	aT = aT+pResults.a[i]
end
aP = aT/#pResults.a

local wT, wP = 0,0
for i=1, #pResults.w do
	wT = wT+pResults.w[i]
end
wP = wT/#pResults.w

print("\n-----------------\nWAIT TEST RESULTS\n-----------------\n  Reg: "..wP.."%\n  Accu: "..aP.."%")
Test 2:
wait(2) -- Give the game a moment to start before we run the test


local pResults = {
	a = {};
	w = {};
}

for i=1,1000 do
	wait(0.1)
	spawn(function() -- Run in multiple threads or this test takes longer than my attention span
		local g = (i/1000) * 0.5
		local w = wait(g)
		local a = AccuWait(g)
		
		local wP = math.abs(g-w)/g
		local aP = math.abs(g-a)/g
		
		print("Wait Test Goal: "..g.."\n  Reg: "..w.." (Off by "..wP.."%)\n  Accu: "..a.." (Off by "..aP.."%)")
		
		pResults.a[#pResults.a+1] = aP
		pResults.w[#pResults.w+1] = wP
	end)
end

wait(3) -- Let results come in

local aT, aP = 0,0
for i=1, #pResults.a do
	aT = aT+pResults.a[i]
end
aP = aT/#pResults.a

local wT, wP = 0,0
for i=1, #pResults.w do
	wT = wT+pResults.w[i]
end
wP = wT/#pResults.w

print("\n-----------------\nWAIT TEST RESULTS\n-----------------\n  Reg: "..wP.."%\n  Accu: "..aP.."%")

Results:

Test 1:

Default wait averaged being off by 0.062004177663708%
Accurate wait averaged being off by 0.038123261452323%

Test 2:

Default wait averaged being off by 0.32967473346624%
Accurate wait averaged being off by 0.16510519174399%

Correct me if I’m spazzing on this, but isn’t that around twice as accurate?

1 Like

wait has a minimum of about 1/30th of a second. You didn’t add a minimum, so when it tried to yield for 1/1000th of a second, it would yield for far less than wait because it didn’t have that minimum.

Test
wait()
local RS = game:GetService("RunService")
local Bindable = Instance.new("BindableEvent")
local function AccuWait(WaitTime)	
	local StartTime = tick()
	local Waited = 0
	local Completed = Bindable:Clone()
	local SteppedConnection
	SteppedConnection = RS.Stepped:Connect(function()
		for i=1,5 do
			Waited = tick()-StartTime
			if Waited>=WaitTime then
				Completed:Fire(Waited)
				SteppedConnection:Disconnect()
				Completed:Destroy()
				break
			end
		end
	end)
	return Completed.Event:Wait()
end
local pResults = {
	a = {},
	w = {}
}
for i=500,1000 do
	coroutine.wrap(function()
		local g = (i/1000) * 0.5
		local w = wait(g)
		local a = AccuWait(g)
		local wP = math.abs(g-w)/g
		local aP = math.abs(g-a)/g
		print("Wait Test Goal: "..g.."\n  Reg: "..w.." (Off by "..wP.."%)\n  Accu: "..a.." (Off by "..aP.."%)")
		pResults.a[#pResults.a+1] = aP
		pResults.w[#pResults.w+1] = wP
	end)()
	RS.Heartbeat:Wait()
end
wait(3)
local aT, aP = 0,0
for i=1, #pResults.a do
	aT = aT+pResults.a[i]
end
aP = aT/#pResults.a
local wT, wP = 0,0
for i=1, #pResults.w do
	wT = wT+pResults.w[i]
end
wP = wT/#pResults.w
print("\n-----------------\nWAIT TEST RESULTS\n-----------------\n  Reg: "..wP.."%\n  Accu: "..aP.."%")

In this, the differences in outcome are much smaller.
image
Your function only appears to be more accurate when yielding for small amounts of time.

Its only worth checking once, the difference in time between each iteration is micro seconds.

I don’t see the purpose of using bindable events, considering you can use Wait on the RunService events.

local RunService = game:GetService"RunService"
local Heartbeat = RunService.Heartbeat
local function AccuWait(Time)
    local start = tick()
    while tick()-start < Time do
        Heartbeat:Wait()
    end
    return tick()-start
end
1 Like

This was a way to work with the iteration method within a connection. If you’re only checking once per Heartbeat, then your way is better.

I did that method for an “AccuDelay” function:

local function AccuDelay(WaitTime,Function)
	spawn(function()		
		local StartTime = tick()
		while true do
			if tick()-StartTime>=WaitTime then
				break
			end
			RS.Stepped:Wait()
		end
		
		Function()
	end)
end

This is actually useful for what I am trying to do in my game! Thanks!

Actually as the kind folks above mentioned. This really isn’t a great method. The code is long, it doesn’t always work as intended, and it’s only real benefit is accurate waits for lower framerate users.

In which case, this code is better to use:

local RS, RSTime, framerate = game:GetSerive('RunService'), 0.0167, 60
RS.Heartbeat:connect(function(step)
	RSTime = step
	local f = math.ceil(1/step)
	framerate = (framerate+f)/2
end)

function RealWait(Duration)
	if Duration == nil or Duration < RSTime then Duration = RSTime end
	local i = 0
	for j = 0,Duration,0.015 do
		i = i+RSTime
		RS.Heartbeat:wait()
		if i+RSTime > Duration+(RSTime/2) then break end
	end
end

Note that code has two variables you can reference anytime to get the framerate of the user, and the time it takes them to render a frame.

Yes, but it can serve to a very specific case to what I am doing in my game. The flaws that they are saying do not apply to what I am doing, I just need a function that fires as often and as fast as possible.

So this concept is smart but this will not give you great results for waiting and there’s a much more performant way to do this exact thing. Your code can be simplified down to this:

local RunService = game:GetService("RunService")
local event = Instance.new("BindableEvent")
RunService.Heartbeat:Connect(function()
    for i=1, 10 do
        event:Fire()
    end
end)

-- Some other code to handle waiting

This isn’t necessarily bad and will achieve >60hz however you’ll be getting 10 iterations at a time 60 times a second. This is how Roblox achieves 240hz physics on a 60hz frame loop.

However, this really can’t be used to reduce wait times. Each iteration will never be evenly spaced. This will achieve better results but still won’t be evenly spaced meaning short waiting isn’t feasible:

-- Better accuwait basis
if steps%10 == 0 then
    RunService.Heartbeat:Wait()
end
steps = steps + 1 -- Increment steps

The above will cause code to yield for ~1/60th of a second every 10 steps so on average 1/600 seconds. This still doesn’t give you faster waiting however it will be much more performant and can work in any situation. This is what I use for most of my resource intensive loops including physics and terrain generation.

1 Like

Hey guys… been a long time.

In that time I’ve created a new system… that ACTUALLY works

I have also created a new system that can ACTUALLY yield down to thousandths of a second (but it’s very hacky). Also I improved upon your code, but I was afraid to post it here because it had been a year since any activity, so I’m glad you have brought this back to life.