I’m working on a vehicle suspension system that works kind of like a hovercraft, with a BodyThrust at each wheel. Every frame, each thruster gets its altitude with a downward raycast and adjusts the upward thrust with the help of a PID loop. You can see a similar thing in the default roblox racing game template. This works very nicely as long as the client stays above about 20 fps:

However there’s some issues if players drop below about 20fps, and for some reason a lot of people think that’s an acceptable framerate to play at. The thrusters can’t update quickly enough to keep things smooth, and end up oscillating which makes the truck undrivable:

I’ve tried adjusting the PID values and tweening the thrust values to try and smooth things out but I can’t get around the limitations of having the altitude values not updating quickly enough. As far as I know, there’s no way to get a loop that updates quicker than the clients framerate. Any ideas?

It’d be pretty hacky but you could try clamping the allowed delta between the process variable and the return value whenever the delta time is above a certain threshold

local res = ProportionalTerm + IntergralTerm + DerivativeTerm
-- clamp if framerate is 30fps or lower
if dt >= .033 then
return processVariable + math.clamp(res - processVariable, -LOW_FPS_MAX_DELTA, LOW_FPS_MAX_DELTA)
else
return res
end

Increasing the integral bias seems like it should help there. Maybe dynamically by that delta time threshold. It’ll still feel kinda doggy and worn out but it’s going to slowly but surely move the process variable to the set point in those spaced out integration moments.

I put together a slightly different system that still uses raycasts at each wheel, but instead of a bodythrust at each wheel, I do some math to figure out what height and orientation the vehicle should have as a whole. Then I can have all the constraints/forces in the primary part without worrying too much about them overcorrecting. It seems to work pretty well so far, there’s just a bit more things you have to fake, like the body roll.

Use AlignPosition on each thruster [see pic for info on config].

Use sine curve for dampening.

With reference to the #3. Take the average of the past n thetas [including the current], and feed that into the sin method.

Eg:

-- CFG
local MAX_UPFORCE = 1000
local MAX_HEIGHT = 5
local MAX_THETAS = 10
-- Keep track of prev thetas
local Prev_Thetas = {}
do -- Do this every Heartbeat
-- Calculate thruster's current height by raycasting
current_height = ....
-- Calculate theta for current step
-- [Theta is what we give as the arg of math.sin]
local theta = 1 - current_height / MAX_HEIGHT
-- Append theta to Prev_Thetas table
table.insert(Prev_Thetas, 1, theta)
-- Take average of all thetas
-- This will be our new theta
theta = 0
for _, x in pairs(Prev_Thetas) do
theta += x
end
theta /= #Prev_Thetas
-- Only want to track past 10 ticks
table.remove(Prev_Thetas, 10)
-- Now we have the average of all thetas from past 10 ticks [including this one]
-- Feed that into sin and multiply to get upforce
local upforce = math.sin(theta) * MAX_UPFORCE
-- Set the MaxAxesForce.Y of the thruster's AlignPosition to upforce
-- Also need to set the Position of the thruster's AlignPosition to the target
-- height whilst respecting it's current orientation and position
end