How can I create stable custom physics at lower frame rates?

I’m working on custom spring physics to simulate suspension for a car. The code is pretty straightforward, directly applying Newton’s 2nd law and Hooke’s law:

local RunService = game:GetService("RunService")

local Object = workspace.Body
local RestLength = 8 -- The length of the spring if no external forces were acting on it 
local DesiredLength = 4

RunService.Heartbeat:Connect(function(dt)
	local Weight = Object.Mass * workspace.Gravity * Vector3.new(0,1,0):Dot(Object.CFrame.UpVector)-- Effective G force
	local SpringConstant = Weight/(RestLength - DesiredLength)/4 -- Solve mg - kx = 0, where x is the desired compression. Divide by 4 to distribute between wheels
	local Dampening = SpringConstant * 0.3 --How much dampening force is applied proportional to the velocity of the spring

	for i, force in pairs(Object:GetChildren()) do
		if force:IsA("VectorForce") then
			local attachment = force.Attachment0
			local origin = attachment.WorldPosition
			local downVector = -attachment.CFrame.UpVector

			local params = RaycastParams.new() do
				params.FilterDescendantsInstances = {Object}
			end
			local result = workspace:Raycast(origin, downVector * 10, params)

			if result then
				--Calculate the length of the spring along with how much it is compressed from equilibrium
				local length = (result.Position - origin).Magnitude
				local x = length - RestLength

				-- F = -kx - bv
				local f = -SpringConstant * x - Dampening * (x - force.dx.Value)/dt - 0.5 * Dampening * (x - force.d2x.Value)/dt

				--Store the past length values to calculate instantaneous velocity of compression
				force.d2x.Value = force.dx.Value
				force.dx.Value = x

				force.Force = Vector3.new(0,f,0)
			else
				force.Force = Vector3.new()
			end
		end
	end
end)

The script is run locally (I give network ownership of the block to the client) and
works just fine when I run it at a standard 60 fps:

However, things get ugly when I cap my frame rate to even 30 fps. The spring forces seem to overcompensate, and a lot of flinging happens:

I’ve tried removing the “/dt” in this line in an effort to make calculations more stable independent of framerate:
local f = -SpringConstant * x - Dampening * (x - force.dx.Value)/dt - 0.5 * Dampening * (x - force.d2x.Value)/dt
This helps, to an extent. The motion is stable at 30fps once I remove the /dt but the same flinging happens at 20 fps.

Maybe I’m being too meticulous here, but I want the suspension to work properly for lower-end devices. I’m not too familiar with what the “average” device running Roblox is like, but I’d hate to completely ruin the experience for players that get 20-30 frames per second. Is there something fundamentally wrong with my code here or is this just an inevitable result of the physics engine? Any help is appreciated :slight_smile:

Hi, have you fixed this by any chance?

Similar problem here on my car scripts, any solutions found?

Use tick()

How it work: