VectorForce Suspension Unstable

  1. What do you want to achieve? Keep it simple and clear!
    I am using vectorforce and raycast for my cars suspension

  2. What is the issue?
    The issue is when the the players FPS drops below 30 the suspension becomes unstable and starts bouncing and twitching.

STABLE FPS

UNSTABLE FPS (you can see at end of the video the car becomes stable as fps increases)

heres a snippit of how the suspension runs this works perfectly fine when the player has a stable 50+ FPS

local Force = 10000 * Settings.Suspension
local Damping = Force / Settings.Bounce

local PartHeight = (Position - RefCFrame.Position).Magnitude
local CurrentHeight , TargetHeight = Attachment0.Position.Y , -math.min(PartHeight , Settings.Height) + (Wheel.Size.Y * 0.5)
local PartDamping = RefCFrame:ToObjectSpace(CFrame.new(Wheel.Velocity + RefCFrame.Position)).p * Damping
local ForceMax, ForceMin = Force, -1 * Force

-- Calculate delta (difference) in height
local DeltaHeight = Settings.Height - PartHeight
local UpForce = DeltaHeight^2 * (Force / Settings.Height^2) - PartDamping.Y

-- Clamp UpForce to ForceMax and ForceMin
UpForce = math.clamp(UpForce , ForceMin/4 , ForceMax/4)


VectorForce.Force = Vector3.new(
	0, 
	UpForce, 
	0
)
1 Like

Hello,

The problem you have is that VectorForce runs at a different rate than the FPS. As a result, when you have low frame rates you’ll over shoot your goal position.

This is a problem which is kind of difficult to solve with scripted physics, but the best method is to try to predicate where it’ll be in the future and adjust your force so that the velocity of the object doesn’t overshoot your goal before you get a chance to interfere with it again.

This post might also be of use to you. It seems they settled on some type of physics sub step solution.

EDIT: Luanoid apparently suffers from similar issues, it’s not of use. The above post does go over it better though.

I’ve tried doing this exact thing before. The lazy solution (which I did) is just to cap the maximum force the suspension could apply so that the car does not break because of the forces.

However, with low fps, the suspension will always be very different than the smooth high fps suspension when you use forces alone. If you want similar behavior you will need to find the spring height with time instead of finding the spring force with time. Unfortunately, this is quite complex to do when damping is involved as it requires calculus to integrate the forces to get a position. This is essentially what you are doing when you have high fps, but with low fps, the gaps become too large to provide values to accurately model your suspension (your suspension will overshoot).

The Equation

Luckily, damped springs are a common topic since all real-world springs have some damping to them, and there are already pre-derived equations for you to use. If you are up for the read, I found a textbook page on this that explains the equation: 15.6: Damped Oscillations - Physics LibreTexts

Essentially, they describe how to get to the equation (not really that important for implementing it), and also how to use it (more important). The two important equations for finding the height of your springs (the difference from your resting position) can be found by these two equations:
image
image
Note that you are just plugging second equation into the first equation.

To give a quick summary of the variables here: A0 is the initial height difference from your suspension’s resting position, b is the damping coefficient, k is the spring coefficient (the spring stiffness), t is time, that e is just math.exp(), m is mass, that weird sign that looks like a circle with a slash essentially just shifts the equation to a later or earlier time (for our case it can be ignored). In total, the equation graphs like this with the height difference graphed on the vertical axis and time on the horizontal axis (I already plugged in the second equation into the first equation here):


^ Damped spring!!!

Edge Cases

The textbook also explains what happens when the damping is too low or too high.
image
Basically if b^2 > 4mk, the damping is too high. This is the most important state to take into consideration because: 1) you probably want your suspension to reach the height you target it to and 2) when the damping is too high, the height becomes a complex number which is hard to deal with. Instead, I suggest clamping this value down to 4mk so that this does not happen.

Implementation

With all of this we can make a function that takes in time, the spring stiffness and damping coefficients, the mass of the car, and the initial height of the spring to get the current spring height.

local function getSpringHeight(deltaTime, stiffness, damping, mass, initialSpringHeight)
	-- Sets stiffness back to calculable values when b^2 > 4mk
	if damping ^ 2 > 4 * mass * stiffness then
		stiffness = damping ^ 2 / (4 * mass)
	end
	local bOver2m = damping / (2 * mass)
	local w = math.sqrt(stiffness / mass - bOver2m^2)
	return initialSpringHeight * math.exp(-bOver2m * deltaTime) * math.cos(w * deltaTime)
end

Of course, these parameters can be minimized since most of these values do not change on every call of the function, but that is up to you for implementation. Basically what I would do is keep initialSpringHeight the same until the wheel collides with the floor again, then reset deltaTime back. The mass can be approximated by 1/4 the mass of the car. Overall, it would look something like this:

local mass = carMass / 4
local totalDeltaTime = 0
local initialHeight
local function onHitGround()
	totalDeltaTime = 0
	initialHeight = -- Get height from the raycast that detected ground hit
end
local function heartbeat(deltaTime)
	totalDeltaTime += deltaTime
	local height = getSpringHeight(totalDeltaTime, stiffness, damping, mass, initialHeight)
	-- Add car reorientation code here based on spring heights
end

Overall, I did not test this, but the implementation to solve this issue is there if you decide not to go with the cheap route of clamping max force like I did. I probably will try to implement this myself in the future since this was a problem plaguing me for the longest time. Any questions are always welcome.

3 Likes

Its not about the height its the UpForce itself and the thing is clamping Upforce still does not seem to fix the boucning issues and overall the makes the suspesnion act worse. heres my implementation not sure if i did it correctly but the suspesnion works but the bouncing issue still exists

local Force = 10000 * Settings.Suspension
		local Damping = Force / Settings.Bounce

		local PartHeight = (Position - RefCFrame.Position).Magnitude
		local CurrentHeight , TargetHeight = Attachment0.Position.Y , -math.min(PartHeight , Settings.Height) + (Wheel.Size.Y * 0.5)
		local PartDamping = RefCFrame:ToObjectSpace(CFrame.new(Wheel.Velocity + RefCFrame.Position)).p * Damping
		local ForceMax, ForceMin = Force, -1 * Force
		Attachment0.Position = Vector3.new(Attachment0.Position.X , TargetHeight, Attachment0.Position.Z)

		-- Calculate delta (difference) in height
		TotalDelta += Delta
		local assemblyMass = Engine.AssemblyMass/4
		local DeltaHeight = Settings.Height - PartHeight
		-- Sets stiffness back to calculable values when b^2 > 4mk
		if Settings.Bounce ^ 2 > 4 * assemblyMass * Settings.Suspension then
			Settings.Suspension = Settings.Bounce ^ 2 / (4 * assemblyMass)
		end
		local bOver2m = Settings.Bounce / (2 * assemblyMass)
		local w = math.sqrt(Settings.Suspension / assemblyMass - bOver2m^2)
		local finalheight = DeltaHeight * math.exp(-bOver2m * Delta) * math.cos(w * Delta)

		local UpForce = finalheight^2 * (Force / Settings.Height^2) - PartDamping.Y --/ DeltaFactor

		-- Clamp UpForce to ForceMax and ForceMin
		--UpForce = math.clamp(UpForce , ForceMin/4 , ForceMax/4)


		VectorForce.Force = Vector3.new(
			0, 
			UpForce, 
			0
		)

Ive tried substeping but have no idea how it actually works heres my try but it does not lift the chassis at all

-- Attachment0 is each of the "Suspension"
		local Velocity = Engine.Velocity + Engine.RotVelocity:Cross(Attachment0.WorldCFrame.Position - Engine.Position)
		
		LinearAcceleration = (Engine.AssemblyLinearVelocity - PastLinearVelocity)
		PastLinearVelocity = Engine.AssemblyLinearVelocity

		local assemblyMass = Engine.AssemblyMass*4

		--Calculate spring constant, for given hipheight against gravity, from equilibrium
		local weight = assemblyMass*workspace.Gravity
		local springFreeLengthRatio = 1.5
		local suspensionDesiredLength = Settings.Height
		local ratio = suspensionDesiredLength*(springFreeLengthRatio-1)
		local calculatedStiffnessValue = weight/ratio

		--Calculating damping coefficient from ratio
		local totalStiffness = calculatedStiffnessValue 
		local dampingRatio = 0.5
		local dampingCoefficient = (dampingRatio * (2*math.sqrt(totalStiffness*assemblyMass)))

		--Since we have 4 wheels
		--local totalStiffness = calculatedStiffnessValue * 4
		local totalDampening = dampingCoefficient
		--local dampingRatio = totalDampening/(2*math.sqrt(totalStiffness*assemblyMass))
		--warn("Damping Ratio", dampingRatio) --Should be 0.2 - 0.6
		local totalStiffness = calculatedStiffnessValue 

		--local alpha = (1/dt)*math.sqrt(assemblyMass/(totalStiffness))
		--warn("Alpha: ", math.round(alpha*100)/100)
		local MaxSpringLength = suspensionDesiredLength*springFreeLengthRatio

		local substepDivisions = 2
				
		local chassisMass = Engine.AssemblyMass

		local simulatedSteppedFrameChassisData = {
			CurrentCFrame = Attachment0.WorldCFrame;--from primary part
			AssemblyVelocity = Velocity;
		}
		
		
		
		local raycast_distance = (Attachment0.WorldCFrame.Position - Position).Magnitude
		local spring_length = math.clamp(raycast_distance, 0, MaxSpringLength)

		local spring_displacement = MaxSpringLength - spring_length

		local springDirection = Engine.CFrame.UpVector

		local springForceMagnitude = spring_displacement*calculatedStiffnessValue

		local dampening = - Velocity.Y*dampingCoefficient --Problem damping not good 60 fps, need roblox 240 Hz or physics substepping, solution use bodyvelocity

		local springForceVector = (dampening + springForceMagnitude)*springDirection

		local oldnetTranslationForce = Vector3.zero

		oldnetTranslationForce += springForceVector
	
		local netSpringForce = oldnetTranslationForce

		local netForce = netSpringForce - chassisMass*workspace.Gravity*Vector3.yAxis

		local linearAcceleration = netForce/chassisMass


		--Perform Semi-Implicit Euler integrator in half the time step.
		local simulationTimeStep = Delta/substepDivisions

		local averageSpringForce = netSpringForce

		for i = 1, substepDivisions - 1 do
			simulatedSteppedFrameChassisData.AssemblyVelocity += linearAcceleration*simulationTimeStep

			simulatedSteppedFrameChassisData.CurrentCFrame += predictDisplacement(linearAcceleration, simulatedSteppedFrameChassisData.AssemblyVelocity, simulationTimeStep)
			--Recalculate spring force
			local NewHit, NewPosition , NewNormal = ChassisModule:RayCast(RefCFrame.Position , RefCFrame:VectorToWorldSpace(Vector3.new(0 , -1 , 0)) * Settings.Height , Model)
			local raycast_distance = (Attachment0.WorldCFrame.Position - NewPosition).Magnitude
			local spring_length = math.clamp(raycast_distance, 0, MaxSpringLength)
			local spring_displacement = MaxSpringLength - spring_length
			local springDirection = Engine.CFrame.UpVector
			local springForceMagnitude = spring_displacement*calculatedStiffnessValue

			local dampening = - Velocity.Y*dampingCoefficient --Problem damping not good 60 fps, need roblox 240 Hz or physics substepping, solution use bodyvelocity

			local springForceVector = (dampening + springForceMagnitude)*springDirection

			local netTranslationForce = Vector3.zero

			netTranslationForce += springForceVector
			--average force
			local difference = netTranslationForce - netSpringForce
			--print(momentDifference.Magnitude)
			averageSpringForce += -difference/substepDivisions
		end

		
		VectorForce.Force = Vector3.new(
			0, 
			averageSpringForce, 
			0
		)

Are you sing render stepped, while wait() or heartbeat?

I am currenlty using Hearbeat as renderstepped is gonna cause more issues since it runs before physics

Did you tried while wait()?
Are your script which changing vector force is on server or client? Idk how it really could work but if car is owned by client but vector force is changed through server then it blablabla.

This has nothing todo with the network ownership which is set to the client and is running on the client. This is a physics problem.

If it’s more unstable at lower fps’, why not just sync it with deltaTime? Renderstepped already has that parameter passed. Also why don’t you try using heartbeat instead

Thats not how it works unfortunately you cannot simply sync it with delta time as its a force applied and the suspension force needs to reach its limit to be “fullly suspended in the air” and multiplying with delta time willl only lower or raise the force applied

You can always add another multiplication factor alongside with deltaTime if it isn’t enough to your likings.

I still don’t understand exactly wha you wanna do. But try use while wait(.1). Don’t change any suspension values. Only change Vector force which you calculating as I guess by the car speed.

if you dont understand its better not to reply as its just adds to unnecessary replies :slight_smile:

Maybe first you explain what u want achive because we don’t have half of day to understanding your script.

The first 2 replies understood in nicely hence they provided with actual help and i am just waiting on them to see if i can get more information :slight_smile:

But its time taking. It would be easier if you put model or if you don’t want just explaing what vector Force do in your car. It is attached directly to the wheel? or over the suspension? What is the function?

you should never use task.wait() or wait() on the client as the actual time that those two are gonna wait for will fluctuate based by the client’s framerate

2 Likes

I was talking about setting the height of the suspension manually (without a VectorForce). This is because no matter what you do, you will always overshoot using a force you do not know how long will be applied for (frame times could take seconds at worst). I think maybe an easy way of doing this would be to average the front and back suspension heights and compare those averages to get the front-back tilt and do the same for the left and right for the left-right tilt. You could then use an AlignOrientation or something similar to match this tilt. For the height of the car above the floor, you could probably just average all the suspension heights + the amount of height of the ground the suspension is on (set the car’s y position to this value with some sort of constraint or even just hard coding it). Of course, there would be issues when the suspension is not on the ground, but that can be detected with raycasts and the aligning and the custom position of the car could be turned off.

Also to add on to the other reply about substepping, substepping will also not work with VectorForce as you need the simulation to play out to update your force values. If you had values that were under full control (not force constraints) this can be an option though.

1 Like

i need to use a vectorforce for my use case and having to set it manually and using align orientation will mess up with the cars traction and its ability to steer correclty so i think i have to try substepping based on what ive seen it takes the average instead of “prediecting” the future but so far no luck with it. heres the code and the car just behaves weird sinking and raising over the height.

        local PartHeight = (Position - Attachment0.WorldCFrame.Position).Magnitude
		local CurrentHeight , TargetHeight = Attachment0.Position.Y , -math.min(PartHeight , Settings.Height + (Wheel.Size.Y * 0.5)) 
		
		local Mass = Engine.AssemblyMass
		local Weight = Mass*workspace.Gravity
	
		t = 0.05
		local upwardForce = calculateForces(TargetHeight, CurrentHeight, calculateAttachmentLocalVelocity(Attachment0).Y, t, Mass, Settings.Height, Delta)
		
		local function physicsSubstep(substepUpwardForce, step, substepYVelocity, iterateHeight)
			local netForce = substepUpwardForce  - Weight

			local predictedVelocity = predictVelocity(netForce, Mass, substepYVelocity, step)
			local predictedDisplacement = predictDisplacement(netForce, Mass, substepYVelocity, step)
			
			local _, NewPosition , _ = ChassisModule:RayCast(Attachment0.WorldCFrame.Position , Attachment0.WorldCFrame:VectorToWorldSpace(Vector3.new(0 , -1 , 0)) * (Settings.Height + Wheel.Size.Y * 0.5), Model)
			local NewPartHeight = (NewPosition - Attachment0.WorldCFrame.Position).Magnitude
			local NewCurrentHeight , NewTargetHeight = Attachment0.Position.Y , -math.min(PartHeight , Settings.Height + (Wheel.Size.Y * 0.5)) 
			
			local newUpwardForce = calculateForces(NewTargetHeight, iterateHeight+predictedDisplacement, predictedVelocity, t, Mass,  (Settings.Height + Wheel.Size.Y * 0.5), Delta)
			local averageVelocity = (substepYVelocity+predictedVelocity) * 0.5
			local averageForce = (newUpwardForce+ substepUpwardForce)*0.5
			local averageHeight = iterateHeight+predictedDisplacement*0.5

			return averageForce, averageVelocity, averageHeight
		end

		--Perform physics substep
		
		local iterateForce = upwardForce
		local iterateHeight = CurrentHeight
		local n = 3 --Split a frame into multipe "Substep" frames
		local iterateYVelocity = calculateAttachmentLocalVelocity(Attachment0).Y
		local step = Delta/n
		for _ = 1, n-1 do
			iterateForce, iterateYVelocity, iterateHeight = physicsSubstep(iterateForce, step, iterateYVelocity, iterateHeight)
		end
		Attachment0.Position = Vector3.new(Attachment0.Position.X , TargetHeight, Attachment0.Position.Z)
		VectorForce.Force = Vector3.new(
			0, 
			iterateForce/4, -- 4 wheels
			0
		)