Spring module not independent of framerate

Hello, DevForum.
I’m having an issue with a spring module I’m using for procedurally animated effects in my FPS game.
You see, whenever I run the game in 60FPS, the recoil is essentially 1x. When I run it in 120FPS, it’s doubled. This is extremely troublesome, especially for people who leave their FPS entirely uncapped. I accidentally joined my game where I had 1000fps, and the recoil made it unplayable.

I’ve tried many things, including passing in the ‘dt’ into the function, however it doesn’t seem to do anything.

Here is the spring module:

-- Constants

local ITERATIONS	= 8

-- Module

local SPRING	= {}

-- Functions 

function SPRING.new(self, mass, force, damping, speed)

	local spring	= {
		Target		= Vector3.new();
		Position	= Vector3.new();
		Velocity	= Vector3.new();

		Mass		= mass or 5;
		Force		= force or 50;
		Damping		= damping or 4;
		Speed		= speed  or 4;
	}

	function spring.getstats(self)
		return self.Mass, self.Force, self.Damping, self.Speed
	end

	function spring.changestats(self, mass, force, damping, speed)
		self.Mass = mass or self.Mass
		self.Force = force or self.Force
		self.Damping = damping or self.Damping
		self.Speed = speed or self.Speed
	end

	function spring.shove(self, force)
		local x, y, z	= force.X, force.Y, force.Z
		if x ~= x or x == math.huge or x == -math.huge then
			x	= 0
		end
		if y ~= y or y == math.huge or y == -math.huge then
			y	= 0
		end
		if z ~= z or z == math.huge or z == -math.huge then
			z	= 0
		end
		self.Velocity	= self.Velocity + Vector3.new(x, y, z)
	end

	function spring.update(self, dt)
		local scaledDeltaTime = dt * self.Speed / ITERATIONS
		for i = 1, ITERATIONS do
			local iterationForce = self.Target - self.Position
			local acceleration	= (iterationForce * self.Force) / self.Mass

			acceleration		= acceleration - self.Velocity * self.Damping

			self.Velocity	= self.Velocity + acceleration * scaledDeltaTime
			self.Position	= self.Position + self.Velocity * scaledDeltaTime
		end

		return self.Position
	end

	return spring
end

-- Return

return SPRING

And here is a snippet of the code responsible for using the spring module to update the recoiled viewmodel:

local cameraRecoil = self.CameraRecoilSpring:update(dt)
local viewmodelRecoil = self.ViewmodelRecoilSpring:update(dt)
self.Camera.CFrame *= CFrame.Angles(cameraRecoil.X, cameraRecoil.Y, cameraRecoil.Z)
self.Viewmodel.HumanoidRootPart.CFrame *= CFrame.Angles(viewmodelRecoil.X, viewmodelRecoil.Y, viewmodelRecoil.Z)

The above code is run in a ‘BindToRenderStep’ event where it gets the dt variable from.

1 Like

First, a side note, the title of this thread might be confusing for some. You’re saying that the spring module is dependent on frametime, but your problem is saying that it isn’t.

Your issue can stem from this part here:

Acceleration does not take the frametime into account and it will vary based on the framerate.


I have used spring modules before, but they all use a different method of interpolation different from yours, so I can’t really help much other than to give that suggestion. Here’s the spring module that I personally use and you can try it out first and see if it is any better:

local euler: number = math.exp(1)
local pi2: number = math.pi * 2

type class = {
	new: <T>(freq: number, pos: T) -> Spring<T>,
}
type attributes<T> = {
	f: number,
	p: T,
	v: T,	
}
export type Spring<T> = attributes<T> & {
	Update: (self: Spring<T>, dt: number, goal: T, speed: number) -> T,
	Reset: (self: Spring<T>, pos: T) -> (),
}

local Spring = {}
Spring.__index = Spring

function Spring.new<T>(freq: number, pos: T): Spring<T>
	local self: attributes<T> = {
		f = freq * pi2,
		p = pos,
		v = pos::any * 0,
	}
	setmetatable(self, Spring)
	return self::any
end

function Spring.Update<T>(self: Spring<T>, dt: number, goal: T, speed: number): T
	local f: number = self.f * speed
	local p0: T = self.p
	local v0: T = self.v

	local offset: T = goal::any - p0
	local decay: number = euler^(-f * dt)

	local fdt = f * dt
	local p1: T = goal + (v0::any*dt - offset * (fdt+1))*decay
	local v1: T = (fdt*(offset::any*f - v0) + v0) * decay

	self.p = p1
	self.v = v1

	return p1
end

function Spring.Reset<T>(self: Spring<T>, pos: T)
	self.p = pos::any
	self.v = pos::any * 0
end

return Spring::class

And an example of using it:

local Spring = require(script:WaitForChild('Spring'))

local boing = Spring.new(2, Vector3.zero) --frequency is 2, initial value is (0, 0, 0)
boing:Reset(Vector3.one) --override the spring and directly change its value to (1, 1, 1)

local goalPosition: Vector3 = Vector3.new(123, 456, 678)
local cnc: RBXScriptConnection; cnc = game:GetService('RunService').Heartbeat:Connect(function(dt)
	local currentValue = boing:Update(dt, goalPosition, 0.1) --interpolate the spring with the speed set to 0.1
	print(currentValue)
	
	--automatically end the connection when the spring is close enough to the goal
	if currentValue:FuzzyEq(goalPosition, 1e-4) then
		cnc:Disconnect()
		cnc = nil::any
		return
	end
end)

I stated in my title that my spring module is not independent of framerate, my title isn’t wrong. Also I don’t want to replace the entire chunk of code.

Independent is the opposite of dependent. Therefore, by saying not independent, you imply that it is dependent. The spring module being dependent on frametime is what you are trying to achieve, but you worded it in your title as if that’s the issue that you are trying to avoid.

This is a module. They’re both classes that simulates a spring. Heck, even the method calls are similar. Nonetheless, it’s up to you whether or not you want to try a potential solution :neutral_face:

1 Like

I don’t want springs to be dependant on frame rate. I want them to specifically act in a manner as such that frame-rate doesn’t affect their behaviour. Thus, I want them to be independent of frame-rate.

There is confusion on both sides clearly lol

You must know what the framerate is in order to adjust the math behind the spring, so the behavior of the spring depends on the framerate. But yes, in the bigger picture, that means making the spring appear independent of the framerate. Basically, dependent on the frameTIME, but independent from the frameRATE. I think we both got a stroke trying to understand this

If you still have trouble fixing the module with my initial suggestion then I’m afraid you have no choice but to swap it

1 Like