Raycast suspension chassis behaving inconsistently on different frame rates


my chassis utilizes raycast suspension, and everything works fine however it behaves differently and inconsistently on different frame rates; below 30 is practically unusable and upwards of 60 it drives too fast.

here is an example:

ive checked out other forum posts that face the same issue, however they were of little help

here is my code

-- in a while true loop, 
-- delta = game["Run Service"].Heartbeat:Wait()

for wheel_name, original_position in pairs(chassis_configurations.wheel_positions) do
	local chassis_cframe = chassis_primary.CFrame
	local ray_origin = chassis_cframe:ToWorldSpace(CFrame.new(original_position))
	local ray_direction = -chassis_cframe.UpVector * (chassis_configurations.suspension_max_length + chassis_configurations.wheel_radius)
	local ray_params = RaycastParams.new()
	ray_params.FilterDescendantsInstances = {chassis}
	local raycast = workspace:Raycast(ray_origin.Position, ray_direction, ray_params)

	if raycast then
		local raycast_distance = (ray_origin.Position - raycast.Position).Magnitude
		local spring_length = math.clamp(raycast_distance - chassis_configurations.wheel_radius, 0, chassis_configurations.suspension_max_length)
		local stiffness_force = chassis_configurations.stiffness * (chassis_configurations.suspension_max_length - spring_length)
		local damper_force = chassis_configurations.damper * ((spring_length_memory[wheel_name] - spring_length) / delta)
		local suspension_force_vector3 = chassis_cframe.UpVector * (stiffness_force + damper_force)
		local rotations_only_wheel_direction_cframe = CFrame.lookAt(Vector3.zero, chassis_cframe.LookVector, chassis_cframe.UpVector)
		if string.sub(wheel_name, 1, 1) == 'f' then
			rotations_only_wheel_direction_cframe = rotations_only_wheel_direction_cframe * CFrame.Angles(
				-math.rad(chassis_smooth_steer * chassis_configurations.steer_angle), 
		local local_velocity = rotations_only_wheel_direction_cframe:ToObjectSpace(CFrame.new(chassis_primary:GetVelocityAtPosition(raycast.Position)))

		local x_force = rotations_only_wheel_direction_cframe.RightVector * -local_velocity.X * chassis_configurations.wheel_friction
		local z_force = rotations_only_wheel_direction_cframe.LookVector * seat.ThrottleFloat * chassis_configurations.torque * (
			math.sign(-local_velocity.Z) == seat.Throttle and (
			1 - math.min(1, math.abs(local_velocity.z)/chassis_configurations.max_speed - chassis_module.chassis_cons)
			) or 1
		spring_length_memory[wheel_name] = spring_length
		chassis_primary:ApplyImpulseAtPosition(suspension_force_vector3 + x_force + z_force, ray_origin.Position)
		spring_length_memory[wheel_name] = chassis_configurations.suspension_max_length

id have to assume this is an issue associated with how i used delta but i am clueless nonetheless

One possible solution to improve the consistency of your raycast suspension is to use a fixed timestep instead of relying on the frame rate. Instead of using “delta = game[“Run Service”].Heartbeat:Wait()”, you can set a fixed timestep value that is independent of the actual frame rate and use it to calculate the simulation updates.

Here’s an example implementation using a fixed timestep of 1/60 seconds:

local fixed_timestep = 1/60
local accumulator = 0

while true do
    -- Get the elapsed time since last frame
    local delta = game["Run Service"].Heartbeat:Wait()
    -- Add the elapsed time to the accumulator
    accumulator = accumulator + delta
    -- Perform the simulation updates in fixed timestep increments
    while accumulator >= fixed_timestep do
        for wheel_name, original_position in pairs(chassis_configurations.wheel_positions) do
            -- Perform the raycast suspension calculations here
            -- using the fixed_timestep value instead of delta
        -- Subtract the fixed timestep from the accumulator
        accumulator = accumulator - fixed_timestep

By using a fixed timestep, the simulation updates will be consistent regardless of the actual frame rate, and the behavior of your raycast suspension should be more predictable and stable.

it certainly improved it especially around the 20-30 fps range.

a bigger issue however is the car starts to fling around uncontrollably on really high frame rates (e.g 300 courtesy of fps unlocker).

1 Like

additionally, 20 is where it really starts to spaz out

Another possible solution or mitigation is to use a Physics sub stepping algorithm and divide the physics calculation for a large time step (or frame) into multiple sub frames which reduces instabilities: