VectorForce Suspension Unstable

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
		)

Unfortunately, I doubt substepping will work with VectorForce. This is because you are still applying a constant force in the end. Imagine a lag spike of 5 seconds with this kind of force; this is why predicting the future will always cause overshoot (there is no guarentee for when the next update is). However, maybe applying impulses instead of a constant force may work using BasePart:ApplyImpulseAtPosition() on your chassis instead of using a constant vector force. This probably would need tweaking (may be much more behaviorly different from the vector force) but guarentees no overshoot because it applies all the force at once.

Just for your information: impulse is momentum change and is calculated by force multiplied by time. So if you decide to go with this, I think you can use the last frame time or an average frame time to gauge how much impulse you need so you get similar behavior between devices of different performances. If you know the amount of time that you will calculate for, you could also substep for smoother behavior in this case (longer intervals of time will probably need this).

1 Like

I would also like to add that there are algorithms out there to adapt to environment changes to correct forces and prevent overshoot that can be used with VectorForce. PID controllers are great for this if you want to learn how to use them; however, they require quite a lot of tuning. There are modules out there already like this one.

1 Like

Why you cannot use normal suspension?

This seems interesting how would i go on about implementing this with the current system? it might actually work out since its adding forces bit by bit until it reaches the final force

This would more be just setting a target position and forgetting the suspension / spring thing entirely. You would probably need to give it the difference in height for suspension every frame, and it will spit out a force strength to get closer to the target of 0 difference. Instead, though, this force will be a bit more adaptive to the situation since it will detect overshooting and correct itself.

IDK

The one on the right uses my prototype physics substep and the one on the left uses the normal spring equations with dampening.

It works a bit doesn’t immediately flip out but something weird happens.

Damping ratio is high at 0.8, the problems do not appear if it’s lower at 0.4.

Here is the place file if you are interested in taking over.

RaycastSuspensionTest.rbxl (495.4 KB)

It would be a lot easier in unity as you can just get the inertia tensor property, now I have no idea if the calculations actually work. I also heard there is rotational dampening in roblox which may mess up the calculations further more.

TBH, I think you should just be patient until roblox releases physics substepping to be competitive with unreal engine if it happens.

Jailbreak uses a similar suspension but not sure how they managed to fix it. it would be great if @badcc could give an insight on this issue (replying to @dthecoolest aswell)

Quick update: I found this video https://youtu.be/WSxGsKypFCw?t=123. They use raycast suspension but with ApplyImpulseAtPosition() like I suggested, so it is definitely possible to do this. In fact, it is nearly identical to what I was thinking about would work to fix your problem but without taking account time between impulses. (I do not know if this would be better or worse.) If you take into account the time and use the force multiplied by time formula you can also use substepping here to smoothen out how much impulse you apply per frame, so your code would be very much reusable with this implementation.

I don’t think jailbreak has solved it.

But I think I have done it!!!

In before roblox officialy creates their own physics substepping option but this will do for now.

Haven’t fully stressed test it releasing for free for feedback.

RaycastSuspensionTestFixed.rbxl (496.1 KB)

@LE4FBUG found out apply impulse or torque doesn’t matter.

The key I found is using 5 physics substeps and not 2… the suspensions are not that stable. Idea stolen from unity asset

Also the calculations include Roblox’s built in angular velocity dampening which I trialed through plotting a data.


Seems like the equation for the built in angular velocity dampening is e^ -0.05 * t where t is time, 1000 is the initial angular velocity.

The code for physics substepping if you don’t want to download:

module script:

Long code module and normal script
local module = {}

local function predictVelocity(acceleration, currentYVelocity, timeStep : number)
	local addedVelocity = acceleration*timeStep --Integrate acceleration, assuming constant acceleration
	return currentYVelocity + addedVelocity
end

local function predictDisplacement(acceleration, currentVelocity, timeStep)
	return currentVelocity*timeStep
end


function getVelocityAtPoint(velocity, rotationalVelocity, centerOfMass, worldPoint)
	return velocity + rotationalVelocity:Cross(worldPoint - centerOfMass)
end

function module.calculateNetSpringForce(chassisData, SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, raycast_visualization_dictionary, overlapParams, substepCalculation)
	local simulatedChassisCFrame = chassisData.CurrentCFrame --from primary part
	local centerOfMassOffset : Vector3 = chassisData.CenterOfMassOffset
	local assemblyAngularVelocity = chassisData.AssemblyAngularVelocity
	local assemblyVelocity = chassisData.AssemblyVelocity
	
	--if raycast_visualization_dictionary then
	--	local chassisDebug = raycast_visualization_dictionary["ChassisSimulation"]
	--	chassisDebug.CFrame = simulatedChassisCFrame
	--end
	
	local netTranslationForce = Vector3.zero
	local netTorque = Vector3.zero

	local centerOfMass = simulatedChassisCFrame*centerOfMassOffset
	
	for wheel_name, vectorforce : VectorForce in pairs(SpringVectorForceDictionary) do
		local attachment = vectorforce.Attachment0
		
		local wheelAttachmentCFrame = simulatedChassisCFrame*attachment.CFrame
		local wheelAttachmentPos = (wheelAttachmentCFrame).Position

		local rayDirection = -simulatedChassisCFrame.UpVector*MaxSpringLength
		local raycastResult = workspace:Raycast(wheelAttachmentPos, rayDirection, params)
		
		--print((wheelAttachmentPos- centerOfMass).Magnitude)
		local velocity = getVelocityAtPoint(assemblyVelocity, assemblyAngularVelocity, centerOfMass, wheelAttachmentPos)
		velocity = -rayDirection.Unit:Dot(velocity)

		local raycast_debug_part
		if raycast_visualization_dictionary then
			raycast_debug_part = raycast_visualization_dictionary[wheel_name]
			raycast_debug_part.CFrame = CFrame.lookAt(wheelAttachmentPos, wheelAttachmentPos+rayDirection)
		end
		
		--print(#collisionCheck)
		if raycastResult  then
			local raycast_distance = (wheelAttachmentPos - raycastResult.Position).Magnitude
			local spring_length = math.clamp(raycast_distance, 0, MaxSpringLength)
			
			local spring_displacement = MaxSpringLength - spring_length

			local springDirection = simulatedChassisCFrame.UpVector

			local springForceMagnitude = spring_displacement*calculatedStiffnessValue
			
			local dampening = velocity*dampingCoefficient --Problem damping not good 60 fps, need roblox 240 Hz or physics substepping, solution use bodyvelocity

			local radius = (wheelAttachmentPos - centerOfMass)

			local springForceVector = (springForceMagnitude - dampening)*springDirection
			
			netTorque += radius:Cross(springForceVector)

			netTranslationForce += springForceVector
			
			--if wheel_name == "back_left" then
			--	print("hit: ", wheel_name, spring_displacement)
			--end
			if raycast_debug_part then
				raycast_debug_part.BrickColor = BrickColor.Green()
				--raycast_debug_part.Size = Vector3.new(0.1, 0.1, raycast_distance)
				raycast_debug_part.CFrame *= CFrame.new(0, 0, -raycast_distance/2)
			end
		else
			if raycast_debug_part then
				raycast_debug_part.BrickColor = BrickColor.Red()
				raycast_debug_part.Size = Vector3.new(0.1, 0.1, rayDirection.Magnitude)
				raycast_debug_part.CFrame *= CFrame.new(0, 0, -rayDirection.Magnitude/2)
			end
		end
		
		--Do space query
		--doesn't seem to be the problem		
		local insidePart = workspace:GetPartBoundsInBox(wheelAttachmentCFrame, Vector3.new(0.1,0.1,0.1), overlapParams)
		if #insidePart > 0 then
			print("Inside")
		end
		
	end
	
	
	
	return netTranslationForce , netTorque
end

-- Function to normalize a vector
function normalizeVector(vx, vy, vz)
	local magnitude = math.sqrt(vx^2 + vy^2 + vz^2)
	return vx / magnitude, vy / magnitude, vz / magnitude
end

-- Function to calculate moment of inertia for an arbitrary axis, from chatgpt not sure if correct
function calculateGeneralizedMomentOfInertia(mass, width, height, depth, axisOfRotation : Vector3)
	 --Calculate the moments of inertia along the principal axes
	local Ixx = (1/12) * mass * (height^2 + depth^2)
	local Iyy = (1/12) * mass * (width^2 + depth^2)
	local Izz = (1/12) * mass * (width^2 + height^2)

	-- The inertia tensor in matrix form (assuming principal axes aligned with the cuboid)
	local inertiaTensor = {
		{Ixx, 0, 0},
		{0, Iyy, 0},
		{0, 0, Izz}
	}

	-- Normalize the arbitrary axis vector
	local vx, vy, vz = axisOfRotation.X, axisOfRotation.Y, axisOfRotation.Z

	-- Calculate moment of inertia along the arbitrary axis
	--local I_axis = vx * (vx * Ixx + vy * 0 + vz * 0)
	--	+ vy * (vx * 0 + vy * Iyy + vz * 0)
	--	+ vz * (vx * 0 + vy * 0 + vz * Izz)
	local I_axis = vx * (vx * inertiaTensor[1][1] + vy * inertiaTensor[1][2] + vz * inertiaTensor[1][3]) +
		vy * (vx * inertiaTensor[2][1] + vy * inertiaTensor[2][2] + vz * inertiaTensor[2][3]) +
		vz * (vx * inertiaTensor[3][1] + vy * inertiaTensor[3][2] + vz * inertiaTensor[3][3])

	
	--for a sphere
	--local I_axis = (2/5) * mass * (width/2)^2
	return I_axis
end

function module.physicsSubstep(chassisPrimaryPart : BasePart, SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, timeStep, rayVisualDictionary, substepDivisions, overlapParams, currentRobloxAngularAcceleration)

	local chassisMass = chassisPrimaryPart.AssemblyMass

	local simulatedSteppedFrameChassisData = {
		CurrentCFrame = chassisPrimaryPart.CFrame;--from primary part
		
		CenterOfMassOffset = chassisPrimaryPart.AssemblyCenterOfMass - chassisPrimaryPart.Position;
		
		AssemblyAngularVelocity = chassisPrimaryPart.AssemblyAngularVelocity;
		
		AssemblyVelocity = chassisPrimaryPart.AssemblyLinearVelocity;
	}

	local netSpringForce, netSpringMoment = module.calculateNetSpringForce(simulatedSteppedFrameChassisData, SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, rayVisualDictionary, overlapParams)
	
	local netMoment = netSpringMoment
	local netForce = netSpringForce - chassisMass*workspace.Gravity*Vector3.yAxis
		
	local linearAcceleration = netForce/chassisMass
	
	--Wrong
	local width = chassisPrimaryPart.Size.X
	local height = chassisPrimaryPart.Size.Y
	local depth = chassisPrimaryPart.Size.Z
	local momentOfInertia = calculateGeneralizedMomentOfInertia(chassisMass, width, height, depth, simulatedSteppedFrameChassisData.AssemblyAngularVelocity.Unit)
	local angularAcceleration = netMoment/momentOfInertia
	--print(momentOfInertia, angularAcceleration.Magnitude)
	if angularAcceleration ~= angularAcceleration then
		angularAcceleration = Vector3.zero
	end
	--Perform Semi-Implicit Euler integrator in half the time step.
	local simulationTimeStep = timeStep/substepDivisions
	
	local averageSpringMoment = netSpringMoment
	local averageSpringForce = netSpringForce
	
	local newStepForwardMoment = netSpringMoment
	for i = 1, substepDivisions - 1 do
		local momentOfInertia = calculateGeneralizedMomentOfInertia(chassisMass, width, height, depth, simulatedSteppedFrameChassisData.AssemblyAngularVelocity.Unit)
		local angularAcceleration = newStepForwardMoment/momentOfInertia

		--apply simulated acceleration
		simulatedSteppedFrameChassisData.AssemblyVelocity += linearAcceleration*simulationTimeStep
		if angularAcceleration == angularAcceleration then
			
			simulatedSteppedFrameChassisData.AssemblyAngularVelocity += angularAcceleration*simulationTimeStep

			local angleVel = simulatedSteppedFrameChassisData.AssemblyAngularVelocity
			simulatedSteppedFrameChassisData.AssemblyAngularVelocity = angleVel * math.exp(-0.06*simulationTimeStep)

		end

		--Change rotation and position
		--Do you rotate first or change position first? shouldn't matter
		--simulatedSteppedFrameChassisData.CurrentCFrame += simulatedSteppedFrameChassisData.AssemblyVelocity*simulationTimeStep
		
		local angularVelocityAtThisTime = simulatedSteppedFrameChassisData.AssemblyAngularVelocity
		simulatedSteppedFrameChassisData.CurrentCFrame += simulatedSteppedFrameChassisData.AssemblyVelocity*simulationTimeStep
		
		--This is the issue? yep causes simulated chassis to teleport far away
		if angularVelocityAtThisTime.Unit == angularVelocityAtThisTime.Unit then --NAN check
			local rotationalDisplacement = simulatedSteppedFrameChassisData.AssemblyAngularVelocity*simulationTimeStep
			
			if rotationalDisplacement.Unit == rotationalDisplacement.Unit then --another NAN check
				local rotation = CFrame.fromAxisAngle(angularVelocityAtThisTime.Unit, rotationalDisplacement.Magnitude)
				local initialPosition = simulatedSteppedFrameChassisData.CurrentCFrame.Position
				local newRotationCF = (rotation*simulatedSteppedFrameChassisData.CurrentCFrame).Rotation
				local newCFrame = newRotationCF+initialPosition
				simulatedSteppedFrameChassisData.CurrentCFrame = newCFrame 
			end
			--else
			--	print("No rotation")
			--end
		end

		
		--local part = Instance.new("Part")
		--part.FrontSurface = Enum.SurfaceType.Motor
		--part.BrickColor = BrickColor.Red()
		--	part.Anchored = true
		--	part.CanCollide = false
		--	part.CanQuery = false
		--part.Size = Vector3.new(1,1,1)
		--part.CFrame = simulatedSteppedFrameChassisData.CurrentCFrame
		--	part.Parent = workspace
		--	game.Debris:AddItem(part,0.2)

		--Recalculate spring force
		local newNetSpringForce, newNetSpringMoment = module.calculateNetSpringForce(simulatedSteppedFrameChassisData, SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, rayVisualDictionary, overlapParams, true)
		newStepForwardMoment = newNetSpringMoment
		--average force
		--local difference = newNetSpringForce - netSpringForce
		--local momentDifference = newNetSpringMoment - netSpringMoment
		----print(momentDifference.Magnitude)
		--averageSpringForce += -difference
		--averageSpringMoment += -momentDifference
		
		--print(newNetSpringMoment.Magnitude)
		--if newNetSpringMoment ~= newNetSpringMoment then
		--	--print("THIS IS IT!")
		--	--print("Chassis data: ", simulatedSteppedFrameChassisData)
		--else
		--end
		averageSpringMoment = (averageSpringMoment + newNetSpringMoment)

		averageSpringForce = (averageSpringForce + newNetSpringForce)

	end
	averageSpringForce /= substepDivisions
	averageSpringMoment /= substepDivisions
	
	return	averageSpringForce, averageSpringMoment, simulatedSteppedFrameChassisData
	--return	netSpringForce, netSpringMoment

end


function module.getNetForceNoSubstep(chassisPrimaryPart : BasePart, SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, timeStep, rayVisualDictionary, linearAcceleration, angularAcceleration)

	local chassisMass = chassisPrimaryPart.AssemblyMass

	local simulatedSteppedFrameChassisData = {
		CurrentCFrame = chassisPrimaryPart.CFrame;--from primary part
		CenterOfMassOffset = chassisPrimaryPart.AssemblyCenterOfMass - chassisPrimaryPart.Position;
		AssemblyAngularVelocity = chassisPrimaryPart.AssemblyAngularVelocity;
		AssemblyVelocity = chassisPrimaryPart.AssemblyLinearVelocity;
	}

	local netSpringForce, netSpringMoment = module.calculateNetSpringForce(simulatedSteppedFrameChassisData, SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, rayVisualDictionary)

	return	netSpringForce, netSpringMoment

end

return module

--normal script

local model = script.Parent.Parent

local ChassisSuspensionPhysicsSubstep = require(script.ChassisSuspensionPhysicsSubstep)

local chassis_primary_part = script.Parent
--chassis_primary_part.Shape = Enum.PartType.Ball
chassis_primary_part.Transparency = 1
chassis_primary_part.Size = Vector3.new(5,5,5)
warn("Mass: ", chassis_primary_part.AssemblyMass)
local collisionPart = Instance.new("Part")
collisionPart.Size = Vector3.new(6.2,0.5,11)
collisionPart.Massless = true
collisionPart.Anchored = true
collisionPart.CFrame = chassis_primary_part.CFrame
collisionPart.Parent = chassis_primary_part
model.PrimaryPart = collisionPart

local weld = Instance.new("WeldConstraint")
weld.Part0 = collisionPart
weld.Part1 = chassis_primary_part
weld.Parent = chassis_primary_part

chassis_primary_part.Anchored = false
collisionPart.Anchored = false
--chassis_primary_part.Size = Vector3.new(1,1,1)

local function getPartVolume(part)
	local size = part.Size
	local volume = size.X * size.Y * size.Z
	return volume
end

local function setPartMass(part, mass)
	local partProperties = part.CustomPhysicalProperties or PhysicalProperties.new(part.Material)
	--local partProperties = part.CustomPhysicalProperties
	local _, b, c, d, e =
		partProperties.Density, partProperties.Friction, partProperties.Elasticity, partProperties.FrictionWeight, partProperties.ElasticityWeight
	local newDensity = mass / getPartVolume(part)
	part.CustomPhysicalProperties = PhysicalProperties.new(newDensity, b, c, d, e)
end

--setPartMass(chassis_primary_part, 680)

warn("Mass: ", chassis_primary_part.AssemblyMass)

local wheel_positions = {
	front_left = Vector3.new(-2.75, 0, -5),
	front_right = Vector3.new(2.75, 0, -5),
	back_left = Vector3.new(-2.75, 0, 5),
	back_right = Vector3.new(2.75, 0, 5)
}

local attach = Instance.new("Attachment", chassis_primary_part)

local NetForceVectorForce = Instance.new("VectorForce")
NetForceVectorForce.RelativeTo = Enum.ActuatorRelativeTo.World
NetForceVectorForce.Attachment0 = attach
NetForceVectorForce.ApplyAtCenterOfMass = true
NetForceVectorForce.Parent = chassis_primary_part

local NetMoment = Instance.new("Torque")
NetMoment.RelativeTo = Enum.ActuatorRelativeTo.World
NetMoment.Attachment0 = attach
NetMoment.Parent = chassis_primary_part

--Setup forces
local SpringVectorForceDictionary = {}
local WheelDataTable = {}
local PreviousSpringLengthDictionary = {}

for wheel_name, original_position in pairs(wheel_positions) do
	PreviousSpringLengthDictionary[wheel_name] = 2.1
	--original_position = Vector3.yAxis
	local chassis_cframe = chassis_primary_part.CFrame
	local ray_origin = chassis_cframe:ToWorldSpace(CFrame.new(original_position))

	local attachment = Instance.new("Attachment")
	attachment.Visible = true
	attachment.WorldPosition = ray_origin.Position
	attachment.Parent = chassis_primary_part
	attachment.WorldPosition = ray_origin.Position

	local billboardGui = Instance.new("BillboardGui")
	billboardGui.Enabled = false
	billboardGui.Size = UDim2.fromOffset(100, 50)
	billboardGui.ResetOnSpawn = false
	billboardGui.Adornee = attachment
	billboardGui.AlwaysOnTop = true
	billboardGui.Parent = attachment

	local textLabel = Instance.new("TextLabel")
	textLabel.Size = UDim2.fromOffset(100, 50)
	textLabel.Parent = billboardGui

	local vectorForce = Instance.new("VectorForce")
	vectorForce.Force = Vector3.zero
	vectorForce.Visible = true
	vectorForce.RelativeTo = Enum.ActuatorRelativeTo.World
	vectorForce.ApplyAtCenterOfMass = false
	vectorForce.Attachment0 = attachment
	vectorForce.Parent = attachment

	SpringVectorForceDictionary[wheel_name] = vectorForce
	WheelDataTable[wheel_name] = {}
end

--Debug raycast suspension
local raycast_visualization_dictionary = {}
for wheel_name, original_position in pairs(wheel_positions) do
	local debugPart = Instance.new("Part")
	debugPart.Name = "RaycastDebugPart"
	debugPart.BrickColor = BrickColor.Red()
	debugPart.Anchored = true
	debugPart.CanCollide = false
	debugPart.CanQuery = false
	debugPart.Size = Vector3.new(1,1,1)
	debugPart.Parent = workspace
	raycast_visualization_dictionary[wheel_name] = debugPart
end
local debugPart = Instance.new("Part")
debugPart.Name = "ChassisDebugPart"
debugPart.BrickColor = BrickColor.Red()
debugPart.Anchored = true
debugPart.CanCollide = false
debugPart.CanQuery = false
debugPart.Size = Vector3.new(3,2,8)
debugPart.Parent = workspace
raycast_visualization_dictionary["ChassisSimulation"] = debugPart

local params = RaycastParams.new()
params.FilterDescendantsInstances = {chassis_primary_part}

local RunService = game:GetService("RunService")

if not workspace:GetAttribute("DampingRatio") then
	workspace:SetAttribute("DampingRatio", 0.8)
end

--Max raycast length, less = more stiffness
if not workspace:GetAttribute("spring_free_length_ratio") then
	workspace:SetAttribute("spring_free_length_ratio", 1.5)
end

if not workspace:GetAttribute("suspension_desired_length") then
	workspace:SetAttribute("suspension_desired_length", 2.1) --Lower values of length = more stiffness = more force = more unstable
end


local xZ_FrictionVelocity = Instance.new("BodyVelocity")
xZ_FrictionVelocity.Velocity = Vector3.new(0,0,0)
xZ_FrictionVelocity.Parent = chassis_primary_part

local PastLinearVelocity = Vector3.zero
local PastAngularVelocity = Vector3.zero
local LinearAcceleration = Vector3.zero
local AngularAcceleration = Vector3.zero

local overlapParams = OverlapParams.new()
overlapParams.FilterDescendantsInstances = {chassis_primary_part}

local pastDt = 1/60
RunService.Stepped:Connect(function(time, dt)
	
	LinearAcceleration = (chassis_primary_part.AssemblyLinearVelocity - PastLinearVelocity)
	AngularAcceleration = (chassis_primary_part.AssemblyAngularVelocity - PastAngularVelocity)/dt
	
	PastLinearVelocity = chassis_primary_part.AssemblyLinearVelocity
	PastAngularVelocity =chassis_primary_part.AssemblyAngularVelocity
		
	local assemblyMass = chassis_primary_part.AssemblyMass

	--Calculate spring constant, for given hipheight against gravity, from equilibrium
	local weight = assemblyMass*workspace.Gravity
	local springFreeLengthRatio = workspace:GetAttribute("spring_free_length_ratio")
	local suspensionDesiredLength = workspace:GetAttribute("suspension_desired_length")
	local ratio = suspensionDesiredLength*(springFreeLengthRatio-1)
	local calculatedStiffnessValue = weight/ratio/4

	--Calculating damping coefficient from ratio
	local totalStiffness = calculatedStiffnessValue * 4
	local dampingRatio = workspace:GetAttribute("DampingRatio")
	local dampingCoefficient = (dampingRatio * (2*math.sqrt(totalStiffness*assemblyMass))) / 4

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

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

	local wheelOffset = Vector3.new(0,0,0)
	
	local onGround = false
	
	
	local substepDivisions = 5
	local timeStepDT = dt
	local averageForce, averageMoment = ChassisSuspensionPhysicsSubstep.physicsSubstep(chassis_primary_part,SpringVectorForceDictionary, params, MaxSpringLength, calculatedStiffnessValue, dampingCoefficient, timeStepDT, raycast_visualization_dictionary, substepDivisions, overlapParams, AngularAcceleration)
	--averageMoment = Vector3.new(0,100000,0)
	--averageMoment = -averageMoment
	--print(averageForce.Magnitude, averageMoment.Magnitude)
	--chassis_primary_part.CFrame = simulatedSteppedFrameChassisData.CurrentCFrame
	--print("New moment")
	--print(averageMoment.Magnitude)
	--if averageMoment.Magnitude > 0.0001 then
	--	local part = Instance.new("Part")
	--	part.Anchored = true
	--	part.CanCollide = false
	--	part.CanQuery = false
	--	local pos = chassis_primary_part.Position
	--	local len = averageMoment.Magnitude/100000*10
	--	part.CFrame = CFrame.lookAt(pos, pos - averageMoment)*CFrame.new(0,0,len/2)
	--	part.Size = Vector3.new(0.1,0.1, len)
	--	part.Parent = workspace
	--	game.Debris:AddItem(part,0.1)
	--end
	--print(averageMoment.Magnitude)
	NetForceVectorForce.Force = averageForce
	--NetMoment.Torque = averageMoment	
	NetMoment.Torque = averageMoment	
	--chassis_primary_part:ApplyAngularImpulse(averageMoment*dt)
	--print(NetMoment.Torque.Magnitude, NetForceVectorForce.Force.Magnitude)
	--For friction XZ, for debugging or else it slides everywhere
	--local XZ_FrictionForce = Vector3.new(25000,0,25000)*5
	local XZ_FrictionForce = Vector3.new(1,0,1)*25000*10

	xZ_FrictionVelocity.MaxForce = XZ_FrictionForce
end)
2 Likes

You are still setting a force and torque statically here. I don’t know if this would be more stable or not, but it does not prevent long periods of time between frames from causing overshoot. It probably will work fine with stable fps of any sort because of the substepping, but that does not take into account lag spikes. It seems like your code uses the frame time before to calculate the strength of force later, meaning that sudden long frame times will still cause overshoot.

Is it possible to run this on each of 4 vector forces (or more bcz the recovery truck has 6 wheels) and not one single average vectorforce that lifts the entire chassis? because then i wouldnt have to use a torque which ive said causes issues with the chassis steering and traction of each wheel

Nope.

Every force has to be included in the physics substepping algorithm otherwise the calculations for predicting future position and orientation will be wrong.

This is because the forces are dependent on the position and even orientation of the chassis relative to the terrain for springs.

This means you will somehow need to incorporate steering force, driving force, traction/friction force into the netforce calculation of the physics substepping.

Good luck.

1 Like