VectorForce Suspension Unstable

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