Stabilizing Spring and Recoil with FPS Unlockers

Hello, developers!

I’m having an issue with using Spring modules for visual effects like sway, bobbing, and recoil in my game. The behavior of these effects changes based on the player’s FPS:

  • High FPS: Effects become excessively fast and intense.
  • Low FPS: Effects become slow and less noticeable.

What I’ve Tried

  1. Different Loop Methods: I’ve tried RenderStepped, Stepped and Heartbeat, but none of them kept the effects consistent across different FPS rates.
  2. Using clock Parameter (with os.clock): I set up the Spring to use os.clock in the time parameter to aim for a real-time update. However, the effects still vary with FPS.
  3. Adjusting Delta Time: I also tried multiplying deltaTime (dt) by 60 to adjust it based on the frame rate. I created an adjustedDT variable where adjustedDT = dt * 60 and passed this adjusted value into the Spring functions to try and standardize the effect. However, this didn’t solve the issue.
  4. Other Modules and Solutions: I’ve tested various Spring modules and looked through DevForum posts for solutions, but nothing has fully resolved the issue.
Modules Used

Module Spring Quenty
SpringModule.lua (1,3,KB)

Images

image

image

image

Videos

Question Does anyone have a solution to make Spring effects consistent, regardless of the player’s FPS? Any suggestions are appreciated!

The module being used was made by @Quenty

2 Likes

you need the client fps or rendering fps?

If is the client I think this return the fps:

workspace:GetRealPhysicsFPS()

if is the rendering fps I don’t know how to do it well sorry

Thanks for the response! I’m looking to control spring effects based on render FPS, not physics FPS. GetRealPhysicsFPS() doesn’t solve it in this case, but I appreciate the suggestion

1 Like

Use a while loop instead of RunService, which uses CPU/Engine time instead of GPU time.

You only want to use RunService when you WANT to rely on a dynamic clocktime depending on the client’s GPU.

If you want a fixed time, which depends on the CPU clock, which is always static unless the engine isn’t responding, you’re gonna wanna use a while loop, which is what they exist for.

1 Like

I think you can use the RenderStepped in RunService

You can do 1 / step to get the exact FPS from the RenderStepped

Hope I helped

I’ve already tried using a while loop instead of RunService, but the effect becomes broken and inconsistent. I’m still looking for a way to stabilize the spring effects regardless of FPS :melting_face:

1 Like

He doesn’t WANT to rely on gpu time, that’s the problem; please read before you reply.; Plus he already tried that.

1 Like

It seems like a problem with general optimization if the CPU clocktime is also running behind.
Try optimizing everything and cleaning up a bit.

The CPU will only be running behind if the engine itself is lagging, which is result of optimization issues.

Have you tried isolating your weapon (i assume it’s a weapon) system into a separate baseplate and using the while loop?

The baseplate is already empty for testing purposes, and the code is clean and optimized. However, the issue still persists. Ideally, I would need a way to standardize the delta time to simulate 60 FPS, regardless of the player’s actual FPS. This would keep the spring effects consistent, even if the player’s FPS is high or low. So far, I haven’t found a solution for this…

wanna send the code in question?

the issue with using delta time to simulate 60 fps is:

If the player’s fps is higher than 60, its fine; we can do some math for this.
However
If the player’s fps is LOWER, we can’t do anything because it wont be firing enough times.

This function (updateCheckouts) is responsible for updating the effects in real-time, applying the calculated values for sway, recoil, aim, and bobbing. The final adjustments are made in the last LerpBehavior function, where I pass the parameters of each effect. The LerpBehavior.Lerping function manages the specific interpolation and smoothing of these effects, ensuring each one transitions smoothly based on the calculated values.

Function Updates
function weaponStatus:updateCheckouts(deltaTime)
    self.deltaTime = deltaTime

    if self.managerFPS.viewmodel then
        if self.humanoid.WalkSpeed == self.states.running then
            self.variables.schemeAim = false
        else
            self.variables.schemeAim = true
        end

        local goalBobbing = Vector3.new(
            math.sin(tick() * -8 * 1) * 30,
            math.cos(tick() * 32 * 0.5) * 0.012,
            0
        )

        self.springs.viewmodelBobbing:SetTarget(goalBobbing)
        
        LerpBehavior.Lerping(weaponData)
    end
end

Function Lerping and New

image

Edited.

you COULD use a multiplier on your values based on how much time has passed since the last loop

local FPS_60_CLOCK = 1/60

function weaponStatus:updateCheckouts(TimeSinceLastLoop)
	local CounterLoopRate = TimeSinceLastLoop/FPS_60_CLOCK 	--// Create a multiplier based on the time since last loop

	if self.managerFPS.viewmodel then
		if self.humanoid.WalkSpeed == self.states.running then
			self.variables.schemeAim = false
		else
			self.variables.schemeAim = true
		end

		local goalBobbing = Vector3.new(
			(math.sin(tick() * -8 * 1) * 30) * CounterLoopRate,		 --// Multiply based on our result multiplier
			(math.cos(tick() * 32 * 0.5) * 0.012) * CounterLoopRate, 	--// Multiply based on our result multiplier
			0
		)

		self.springs.bobbings.viewmodel:shove(goalBobbing)
		self.springs.bobbings.viewmodel:update(deltaTime)

		LerpBehavior.Lerping(weaponData)
	end
end

local LastLoopClock = tick()

while task.wait() do 
	local CurrentTick = tick() 

	weaponStatus:updateCheckouts(CurrentTick-LastLoopClock)
	LastLoopClock = CurrentTick 
end 

Thanks for the suggestion, I’ll test this and see if it works.

this also might fire in time thats faster than 60fps but it’s going to multiply based on 60fps so it wont matter that much.

I tested with the module that requires shove and update, but the issue persists. I also tried Quenty’s module, which doesn’t rely on delta time, yet somehow the player’s FPS still affects the effects. I can’t understand why FPS influences the behavior, considering the module shouldn’t depend on delta time. Additionally, using loops like while only breaks the effects and doesn’t solve the issue. Thanks for the suggestion and support so far.

After three days of attempts, I realized that the Lerp function was causing FPS dependency in my effects, leading to inconsistent results. I fixed this by multiplying the interpolation speed by deltaTime * 60. This adjustment normalizes the speed, ensuring that the effect remains stable regardless of the frame rate. The reason behind this is that deltaTime represents the time between frames (pretty obvious), so multiplying it by 60 adjusts the interpolation to simulate a 60 FPS rate, resulting in a consistent experience.

local lerpSpeed = 0.2 * deltaTime * 60 -- \\ Normalize to 60 Fps
testCF = testCF:Lerp(CFrame.new(0, 0, springsFPS.vmTest.v.Z * .5) * CFrame.Angles(springsFPS.vmTest.v.Z / 50, 0, springsFPS.vmTest.v.X / 50), lerpSpeed )
Effect Result

Adendum: If you’re trying to normalize weapon recoil effects and ensure consistent force regardless of the player’s FPS, here’s a tip that worked for me. Multiply the recoil vector by deltaTime multiplied by 60, as shown in the example below:

function weaponStatus:updateRecoil(rc)
   self.springs.camRecoil:SetTarget(Vector3.new(rc / 35, 0, math.random(-rc, rc) / 10) * self.deltaTime * 60)

    task.delay(.015, function()
        self.springs.camRecoil:SetTarget(Vector3.zero)
    end)
end

Thank you all for the suggestions!