Suspension Forces are not equal across client and server scripts

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? When the player takes control of the car, they begin calculating the suspension forces for themselves using raycasts, however, the casts are completely messed up and result in forces much stronger than usually needed, making the car bounce up and usually fly off the map. As far as I know with this code, the math for both the client and server scripts are the same, so what is the issue?

SERVERSIDE CODE

if serverowner then
		for i,v in pairs(WheelPos) do
			local carpos = car.CFrame
			local rayOrigin = carpos:ToWorldSpace(CFrame.new(v)).Position
			local rayDirection = -(carpos.UpVector * (SuspMaxLength + WheelRadius))
			local rayParams = RaycastParams.new()
			rayParams.FilterDescendantsInstances = {car}
			local raycast = game.Workspace:Raycast(rayOrigin,rayDirection,rayParams)

			--checks if it hit smthn
			if raycast then

				local SuspMaxLength = car:GetAttribute('SuspMaxLength')
				local WheelRadius = car:GetAttribute("WheelRadius")
				local SuspStiffness = car:GetAttribute('SuspStiffness')
				local SuspDamping = car:GetAttribute('SuspDamping')
				local WheelFriction = car:GetAttribute('Traction')

				local raycastDistance = (rayOrigin - raycast.Position).Magnitude
				local SpringLength = math.clamp(raycastDistance - WheelRadius, 0, SuspMaxLength + WheelRadius)

					print(WheelSuspLength)
					print(SpringLength)
				--Forces
				local StiffnessForce = SuspStiffness * (SuspMaxLength - SpringLength)
				local DampingForce = SuspDamping * ((WheelSuspLength[i] - SpringLength) / delta)
				local SuspForceVector = carpos.UpVector * (StiffnessForce + DampingForce)
				--local SuspForceVector = -DampingForce * carpos.UpVector.Y - StiffnessForce * (SpringLength - SuspMaxLength)


				--Stores the last known Length Value and uses that to determine damping force (when spring in motion)
				WheelSuspLength[i] = SpringLength

				local RotationsOnlyWheelsDirCFrame = CFrame.lookAt(Vector3.zero, carpos.LookVector,carpos.UpVector)
				local LocalVel = RotationsOnlyWheelsDirCFrame:ToObjectSpace(CFrame.new(car:GetVelocityAtPosition(raycast.Position)))

				local XForce = carpos.RightVector * -LocalVel.X * WheelFriction
				car:ApplyImpulseAtPosition(SuspForceVector + XForce, raycast.Position)
				print(SuspForceVector)
			end
		end
	end

CLIENTSIDE CODE

if seated and seatpart then
				local car = seatpart.Parent
				local carPrimary = car.PrimaryPart
				
				local serverTakeOverWait = 0
				while true do
					local delta = game["Run Service"].Heartbeat:Wait()
					
					local carPrimary = car.PrimaryPart
					
					local wheels = car:FindFirstChild('Wheels')

					local WheelPos = {} 

					for i,v in ipairs(wheels:GetChildren()) do
						WheelPos[v.Name] = wheels:GetAttribute(v.Name)
					end



					local WheelSuspLength = {
						["FR"] = 1,
						["FL"] = 1,
						["RR"] = 1,
						["RL"] = 1
					}

					local SuspMaxLength = carPrimary:GetAttribute('SuspMaxLength')
					local WheelRadius = carPrimary:GetAttribute("WheelRadius")
					local SuspStiffness = carPrimary:GetAttribute('SuspStiffness')
					local SuspDamping = carPrimary:GetAttribute('SuspDamping')
					local ThrottlePercent = 0
	
					local delta = game["Run Service"].Heartbeat:Connect(function(delta)
						--set throttle% change
						if seatpart.ThrottleFloat < 0 and ThrottlePercent > -1 then
							ThrottlePercent -= .01
							--print(ThrottlePercent)
						elseif seatpart.ThrottleFloat > 0 and ThrottlePercent < 1 then
							ThrottlePercent += .01
							--print(ThrottlePercent)
						else
						--No input
						end
						for i,v in pairs(WheelPos) do
							local carpos = carPrimary.CFrame
							local rayOrigin = carpos:ToWorldSpace(CFrame.new(v)).Position
							local rayDirection = -(carpos.UpVector * (SuspMaxLength + WheelRadius))
							local rayParams = RaycastParams.new()
							rayParams.FilterDescendantsInstances = {car}
							local raycast = game.Workspace:Raycast(rayOrigin,rayDirection,rayParams)

							--checks if it hit smthn
							if raycast then

								local SuspMaxLength = carPrimary:GetAttribute('SuspMaxLength')
								local WheelRadius = carPrimary:GetAttribute("WheelRadius")
								local SuspStiffness = carPrimary:GetAttribute('SuspStiffness')
								local SuspDamping = carPrimary:GetAttribute('SuspDamping')
								local WheelFriction = carPrimary:GetAttribute('Traction')
									
								local Torque = carPrimary:GetAttribute("Torque")
								local MaxSpeed = carPrimary:GetAttribute('MaxSpeed')

								local raycastDistance = (rayOrigin - raycast.Position).Magnitude
								local SpringLength = math.clamp(raycastDistance - WheelRadius, 0, SuspMaxLength + WheelRadius)

									--print(WheelSuspLength)
									--print(SpringLength)
								--Forces
								local StiffnessForce = SuspStiffness * (SuspMaxLength - SpringLength)
								local DampingForce = SuspDamping * ((WheelSuspLength[i] - SpringLength) / delta)
								local SuspForceVector = carpos.UpVector * (StiffnessForce + DampingForce)
								--local SuspForceVector = -DampingForce * carpos.UpVector.Y - StiffnessForce * (SpringLength - SuspMaxLength)
							
									--print(StiffnessForce)
									--print(DampingForce)

								--Stores the last known Length Value and uses that to determine damping force (when spring in motion)
								WheelSuspLength[i] = SpringLength

								local RotationsOnlyWheelsDirCFrame = CFrame.lookAt(Vector3.zero, carpos.LookVector,carpos.UpVector)
								local LocalVel = RotationsOnlyWheelsDirCFrame:ToObjectSpace(CFrame.new(carPrimary:GetVelocityAtPosition(raycast.Position)))

								local XForce = carpos.RightVector * -LocalVel.X * WheelFriction
								local ZForce = carpos.LookVector * ThrottlePercent * Torque * (1 - math.min(1,math.abs(LocalVel.Z)/MaxSpeed))
								carPrimary:ApplyImpulseAtPosition(SuspForceVector + XForce, raycast.Position)
								print(SuspForceVector)
							end
						end
					end)

						
					end
				end
		end)

I know it is a mouthful, but if you guys come up with anything I’d be much obliged. Thank you.

1 Like

Attached is a video of this occuring:

The reason could be due to the initial suspension length value so it doesnt provide enough dampening.

The primary reason I believe is using apply impulse. I find vector forces to be more stable and prevents jittering as it is a force and not impulse (which is a lot of force done close to instant).

Right. Does ApplyImpulse work the same throughout the server and client? Because initializing the vehicle on the server has no issue, and the suspension length memory is updated every frame, so that there shouldn’t be an issue.