How to use formulas for car suspension

Yes, something like that. Except i want the wheels to extend outward a bit from the chassis, like in the video the wheels are already pushed out from the chassis by the script and they’re a bit bouncy when they go over a curb or an obstacle.

Okay so there are several major issues with the understanding of the Mass-Spring-Damper system, as well as understanding of derivatives, mass, weight and their effect on the system.

Mass-Spring-Damper Equation

For a free mass-spring-damper system,

F = m(d²x/dt²) + c(dx/dt) + kx

Where m is your mass, c is the damping coefficient and k is your spring constant. Now Roblox actually has inertia built into it, so we can scrap the second derivative term and just apply force based on the damping and spring terms.

Using your terms in your code, your a is k, your b is c, and then for some reason you’ve got b/2 in place of m. Get rid of b/2 and get rid of the second derivative as described above.

You don’t need gravity as that part is already accounted for by the counter force Roblox applies, you’re only concerned with the force from the spring, which is the same here or on the moon or anywhere for the given extension and rates of change.

Defining Displacement Derivatives

Okay so firstly, you need the time difference. Without it there’s no way to get the derivatives properly, so use the arguments given by the stepped function.

Your derivative formulae were incorrect unfortunately - you don’t get the second derivative my taking away the extension from 2 time periods ago. You need time in there for starters, and secondly they are defined as:

dx/dt = change in x over period t, e.g. ( x2 - x1 ) / t

d²x/dt² = change in dx over period t, e.g. ( (dx/dt)2 - (dx/dt)1 ) / t

Though as mentioned above we don’t need any second derivative.

Coding Practices and Logical Errors

Each wheel has its own spring. You can’t use a variable outside the loop for the derivatives as you’re constantly replacing them with each wheel’s data. Your equations ended up where one wheel would have a proper input, and the others would be smaller as they were based off the first wheel’s changes.

Mass Calculations

There are four wheels, which have four springs acting in parallel. They each can be assumed to get 1/4 of the mass of the truck to worry about, if the mass is fairly evenly spread. Any GetMass() calls need to be divided by 4.

NetworkOwnership and weird Physics

Roblox physics is a fine art, and sometimes weird stuff happens if you want the Server to do it all. Instead, your driver should own the truck and should be the one doing all the suspension calculations. Pass the baton over to the driver using SetNetworkOwner on one of the parts, probably the chassis, when the driver is known, and put all the suspension and any driving scripts into local scripts and put them on the player.

The exact process of where you put this localscript, how you trigger it and tell it the correct vehicle to treat as yours, how you spawn the vehicle and when you change network ownership is beyond the scope of this post, but my advice is to anchor the chassis (and probably the wheels too) until you have a driver, then unanchor it and set the network owner in quick succession.

Putting it all together

After working with InternallyAmplified in DMs on the place, we finally arrived at a client-side script as below. Note that each wheel contains an ObjectValue called Thrust that points to a BodyThrust in the main chassis, with its Location property set to the wheel.

local d = 3.0 -- Free length CylindricalContraint position - if there were no car weight on it

local a = ( ( Chassis:GetMass() / 4 ) * workspace.Gravity ) / 1.0 -- Stiffness - ride height is determined by the denominator and the value of d
local b = 0.3 * 2 * ( a * Chassis:GetMass() / 4 ) ^ 0.5 -- Damping coefficient, the 0.3 is a damping factor, lower it for more bounciness, raise it for less bounciness

local extensions = {}
local derivatives = {}
game:GetService("RunService").Stepped:Connect(function(_, step)
	for i, Wheel in pairs(Car.Wheels:GetChildren()) do
		local x = d - Wheel.CylindricalConstraint.CurrentPosition --X is how much the suspension is contracted
        -- Initialise values
        if not extensions[Wheel] then
            extensions[Wheel] = x
        end
        if not derivatives[Wheel] then
            derivatives[Wheel] = 0
        end

        -- Calculate derivatives
        local dx = (x - extensions[Wheel]) / step
        derivatives[Wheel] = dx
		extensions[Wheel] = x

        -- Spring-Damper equation
		local f = (a*x + b*(dx))

		-- Apply the forces
 		Wheel.Thrust.Value.Force = Vector3.new(0, f, 0)
	end
end)

The four BodyThrusts in the chassis act as the springs pushing back on the chassis due to their compression. In this example, the truck rides with the wheels at 2 studs below the chassis attachment point, as d = 3.0 and I calculated the spring constant to rest at 1.0 stud less than that. These values are easily tweaked to effectively change the length of the spring, and change how much compression it has when the truck is just sat there level.

This results in a truck like this:

14 Likes

Thank you for your answer, i understand more on how springs work a bit more now, i’m going to try implement it right now, for the dx/dt values, does every wheel have their own dx/dt table?

EDIT: I’ve updated my script to what you’ve sent me:

local maxd = 8
local d = 6

local a = 2 --Stiffness, higher means higher height at suspension
local b = 1 --Rigidity higher means the suspensions will get less effected by slight changes and will balance out unlike Roblox's suspensions. This is the part that dampens the suspension.

local ST = {dx=0,d2x=0} --For storing the derivatives per suspension	

function GetDist(part)
	local dir = part.CFrame.upVector * 100
	
	local ray = Ray.new(part.Position, Vector3.new(0,-maxd,0))
	local hit, pos = workspace:FindPartOnRayWithIgnoreList(ray, {Car}, false, true)	
	
	return (part.Position - pos).Magnitude
end

game:GetService("RunService").Stepped:Connect(function()
		for i, Wheel in pairs(Car.Wheels:GetChildren()) do
		
			local x = d - GetDist(Wheel) --X is how much the suspension is contracted
	
			local f = (a*x + b*(ST.dx) + Chassis:GetMass()*(ST.d2x))
		
			ST.d2x = ST.dx --Store the past values
	 		ST.dx = x
		
			print(f)
		
	 		Chassis.BodyThrust.Force = Vector3.new(0,-f,0) --Apply the forces
			Wheel.BodyThrust.Force = Vector3.new(0,f,0)
		end
end)

It’s still not pushing the car up or down: https://gyazo.com/0ac6a3b1a6d81370554ba6547dd30abf
(Sorry if i’m missing anything)

I mentioned in my edit there were more fundamental problems running through the entire thing. Check my reply now

1 Like

Wow, that’s alot of information, thank you for taking the time to write it, i’ll try understand it all. :smiley:

I’ve used your code and this is what happens: https://gyazo.com/1ac7e73cdf84b59592bf3d94c8c8600c

Is this due to the spring constant and damping? If so, how can i know how i should tweak it so it doesn’t glitch out like that?

(Thank you again, i’m grateful for all the help!)

I think it’s probably a combination of things.

The spring constant and damping ratio certainly play a part, and you can find methods of selecting appropriate starting points for those values online if you want to, using natural frequency, how quickly you want it to die down and settle.

I think the other cause is that you probably don’t need to apply the force to both objects. Try swapping from BodyThrust to BodyForce (same thing, just no chance of accidental torque), and try only applying it to the Chassis. The wheel’s mass is likely negligible in comparison and I think you’ll experience better results that way. I think applying to both causes double accounting anyway.

1 Like

Is this what you mean by natural frequency?
image

And i replace the damping coefficient with this natural frequency?

Applying the force only to the chassis bugs out less but it still doesn’t push from the ground: https://gyazo.com/5d7e59c40688f69960c1360269922fe9

Here’s a place file of all the progress also if you want to check anything: SuspensionTest.rbxl (95.1 KB)

That is sort of it, but no you don’t replace it with that. I wouldn’t worry too much about natural frequency right now. Here’s a bit of a starting point to get some sensible numbers:

If you want the car to drop by 1 stud due to the weight of the chassis, then you can work out that at x = 1, you want f to equal the weight of the chassis.

a = ( Chassis:GetMass() * Workspace.Gravity ) / x where x = 1, as we want it to settle at 1 stud.

Calculating the ideal damping coefficient is trickier, as knowledge of damping ratio and its relationship to damping coefficient with natural frequency root(k/m) or root(a/m) in your variable naming. Once you’ve got your spring constant sorted, playing around with this value will help. A good starting point is the critical damping coefficient, b = 2*(a * m)^0.5, or c = (2 * root(km)) in standard terminology.

I’ll do the same calculations now on your test place to see what I get. I spent an entire year learning about it at university, but you don’t need the full ins and outs to get your car acting sensible for now.

2 Likes

I’m giving it a shot, and this is what i have come up with so far:

local a = ( Chassis:GetMass() * workspace.Gravity ) / 1 --Stiffness
local b = 2*(a * Chassis:GetMass())^0.5 --Dampening

It’s acting more like a spring but it’s still buggy, I’ll wait to see how you do it as i’m sure im messing something up

Okay I think there’s a few things happening here beyond spring theory. I think there’s issues with relying on raycasting to detect the chassis union and a few other bits. I’ll DM you to go over some of them as we’ll just fill this thread and I think the mass-spring-damper info I’ve put here is helpful for others who are struggling with that specific aspect.

Sure thing, i will mark your answer as solution for others also.

How could I implement friction into this? My car just slides around

EDIT: Seems to be some sort of bodymover problem, no idea how to fix it

EDIT 2: My thruster locations were misplaced and not symmetrical, fixed now

2 Likes