Weapon accuracy is Framerate dependant

I would be pretty surprised if someone actually reply to this

I have been stuck on solving this problem for nearly 2 weeks for now and I really couldn’t come up with a consistent solution,

The problem:

Suppose there is a number accuracy, it represents the weapon’s accuracy, the degree that the bullets can be spread on. The larger it is, the more inaccurate the weapon is. 0 means no spread, aka perfect accuracy.

When the weapon is fired, it will add a certain amount to accuracy, then it waits for a short amount of time, and slowly returns back to 0.

Procedural code:

-- fires weapon
Weapon.Accuracy += 1
wait(resetTime)
local con  
con = RunService.Stepped:Connect(function(_, dt)
       if Weapon.Accuracy <= 0 then
           con:Disconnect()
           return
       end
       -- Currently I have set the reduce rate to 3 `accuracy` per second.
       Weapon.Accuracy -= math.max(3 * dt, 0)
end)

However, there are 2 problems:

First problem: wait(t) is frame-rate dependant and it runs on 30 tick only. In my case, most of the time t will be a quite precise number with decimals (e.g: 0.11) and wait(t) waits at least 1/30 ~= 0.033.

For example, resetTime is 0.11. I would like the game to wait 0.11 seconds before starting to reduce accuracy back to 0.
However, if the game runs on a 60 FPS basis, it will have to wait 0.11 / (1/60) = 6.6 frames, which is impossible as the game doesn’t run in between frames. So I have made it to wait for 7 frames, then compensate back the missing 0.4 frames to accuracy.

local elapsed = 0
local timeToWait = 0.116
local Compensation = 0

con = RunService.Stepped:Connect(function(_, dt)
	elapsed += dt
	if elapsed >= timeToWait then
		if Weapon.Accuracy <= 0 then
			con:Disconnect()
			return
		end
		
		if Compensation then
			Compensation = elapsed - timeToWait
		end
		
		Weapon.Accuracy -= math.max(3 * (dt + (Compensation or 0)), 0)
		Compensation = nil -- only compensate once
	end
end)

Second problem:

Since I have multiplied delta time (3 * dt), the accuracy will be reduced in a synchronized rate in all framerates. This is how it works on 60 FPS vs 120 FPS:

Red line represents frames, green text represents accuracy.

However, there is one thing that is not synchronized:

Suppose the weapon was fired again during the accuracy reduction process. In 120 FPS, the player gets more frames to fire the weapon. If it was fired on a non-60 FPS frame, the Accuracy will become slightly larger, comparing to 60 FPS.

As shown from the sketch below, the weapon accuracy will become 1.875 on 120 FPS, while it should be 1.85. This might not seem to be a huge difference but if the weapon was being fired rapidly or the user is on even higher FPS, the difference is very noticeable.

I have thought of few solutions, and to be compatible to my framework, such as other parts of the code are going to use the accuracy value, I have decided to update accuracy on a 60 tick rate.

Here is what I have got so far, basically I have make it not to update whenever it’s not on 60 frames, as well as still updating on 60 tick even if user is below 60 FPS.

However, it still does not work quite precisely on different framerates, the values are still different and varies in different frame-rate.

Here’s my entire prototype that includes a yield function to simulate weapon fire pausing:

while wait(3) do
	local RunService = game:GetService("RunService")
	local Weapon = {Accuracy = 0}

	local elapsed = 0
	local timeToWait = 0.017

	local Compensation = 0

	local elapsed_reduceStarted = 0
	local delta60Timer = 0

	local fireTag = tick()
	local tag = fireTag

	local function roundNumber(num, dec)
		return tonumber(string.format("%." .. (dec or 0) .. "f", num))
	end

	local function yield(duration)
		local frame = 0
		local frameToWait
		local elapsed = 0
		
		repeat
			local dt = RunService.RenderStepped:Wait()
			elapsed += dt
			frame += 1
			frameToWait = roundNumber(duration / (elapsed / frame))

		until frame >= frameToWait
			or elapsed >= duration 

		warn("yield frame waited", frame)
		warn("yield elapsed", elapsed)
		return elapsed - duration
	end

	Weapon.Accuracy += 1 -- fire weapon

	con = RunService.Stepped:Connect(function(_, dt)
		elapsed += dt
		if elapsed >= timeToWait then
			
			if Weapon.Accuracy <= 0 then
				con:Disconnect()
				return
			end
			
			-- overlayed, stop reducing
			if fireTag ~= tag then
				warn("Overlayed", Weapon.Accuracy)
				con:Disconnect()
				return
			end
			
			if Compensation and elapsed - timeToWait > 0.001 then -- does not have to be too precise or else it ruins the number integrity
				Compensation = elapsed - timeToWait
				print("Added compensation", Compensation)
			end
			
			elapsed_reduceStarted += dt
			
			if elapsed_reduceStarted > delta60Timer then
				delta60Timer += (1/60)
				-- frame passed during the process
				-- e.g: we still want the code to be updated on a 60hz rate even if the user is on 30hz
				-- if the user is on 30hz, we update twice as 60/30 = 2
				
				local framesDropped = elapsed_reduceStarted / delta60Timer
				Weapon.Accuracy -= (3 * (1/60)) * 
					(framesDropped >= 2 and math.floor(framesDropped) or 1) -- update twice if lower than 30 FPS
					+ (3 * (Compensation or 0))
				
				Compensation = nil -- only compensate once
			else
				warn("do not update")
			end
		end
		print(Weapon.Accuracy)
	end)

	yield(0.117) -- simulate fire-rate, suppose the weapon fires every 0.116s
	fireTag = tick() -- fire again
end

A place file for those who are interested to solve, same as the code above.
accuracy-prototype.rbxl (30.5 KB)

3 Likes

Sorry, don’t have a solution.

This might be of interest: Fix Your Timestep! | Gaffer On Games