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