Raycast Vehicle Suspension (Help!)

Hey guys,

I rarely come on here for help, but I’ve been left headscratching regarding my Raycasting Suspension Module.

I understand that there’s engine limitations when accessing the internal Fixed Delta Time which poses significant issues for most raycast vehicle systems.

I’ve managed to overcome the suspension issue for the most part in my code, the only issue I am getting relating to FPS and lag is turning and forces on acceleration and deceleration.

An example of this bug:
If I sit in my vehicle and then enable my lag machine, it’ll be fine up until I start using w,a,s,d (movement controls). Upon using these controls, the car starts flinging etc.

I don’t believe this is related to the suspension of the vehicle, but the forces behind turning and speeding up.

If someone could take a look at the code below and give me some feedback, that’d be amazing.

local CollectionService = game:GetService("CollectionService")

local WheelClass = {}
WheelClass.__index = WheelClass

-- Constructor
function WheelClass.new(Vehicle, Wheel)
	local self = setmetatable({}, WheelClass)
	self._Vehicle = Vehicle
	self._Wheel = Wheel
	self._WheelAttachment = self._Vehicle._Model.PrimaryPart:FindFirstChild(Wheel.Name)

	self.Metadata = {
		SuspensionOutput = Vector3.zero,
		TurningOutput = {X = Vector3.zero, Z = Vector3.zero},
		SmoothSteer = 2,
	}

	return self
end

-- Private Methods
function WheelClass:_Raycast(At)
	local Config = self._Vehicle._Config;
	local VehiclePrimaryPart = self._Vehicle._Model.PrimaryPart;
	
	local RayOrigin = VehiclePrimaryPart.CFrame:ToWorldSpace(CFrame.new(At)).p
	local RayDirection = -VehiclePrimaryPart.CFrame.UpVector * (Config.SuspensionMaxLength + Config.wheelRadius)

	local RayParams = RaycastParams.new()
	RayParams.FilterType = Enum.RaycastFilterType.Exclude
	RayParams.FilterDescendantsInstances = {
		self._Vehicle._Model,
		table.unpack(CollectionService:GetTagged("CharacterComponent"))
	}

	local Raycast = workspace:Raycast(RayOrigin, RayDirection, RayParams)
	return Raycast
end

function WheelClass:_GetSmoothSteer(DeltaTime)
	local Config = self._Vehicle._Config;
	local VehiclePrimaryPart = self._Vehicle._Model.PrimaryPart;
	local VehicleSeat = self._Vehicle._Model.DriveSeat;
	
	local CurrentSmoothSteer = self.Metadata.SmoothSteer
	
	--self.Metadata.SmoothSteer = CurrentSmoothSteer + (VehicleSeat.SteerFloat - CurrentSmoothSteer) * math.min(DeltaTime * 10, 1)
	
	local SteerDifference = VehicleSeat.SteerFloat - CurrentSmoothSteer
	local SteerChange = SteerDifference * math.min(DeltaTime * 5, 1)

	self.Metadata.SmoothSteer = CurrentSmoothSteer + SteerChange
	self.Metadata.SmoothSteer = math.clamp(self.Metadata.SmoothSteer, -1, 1) 
end

function WheelClass:_GetSuspension(Raycast)
	local Config = self._Vehicle._Config;
	local VehiclePrimaryPart = self._Vehicle._Model.PrimaryPart;
	local VehicleSeat = self._Vehicle._Model.DriveSeat;
	
	local SpringLength = math.clamp(Raycast.Distance - Config.wheelRadius, 0, Config.SuspensionMaxLength)
	local SpringDir = VehiclePrimaryPart.CFrame.UpVector
	local TireWorldVelocity = VehiclePrimaryPart:GetVelocityAtPosition(Raycast.Position)
	local Offset = Config.SuspensionMaxLength - SpringLength
	local Vel = TireWorldVelocity:Dot(SpringDir)
	local Force = (Offset * Config.Stiffness) - (Vel * Config.Damper)
	local OutputForce = (SpringDir * Force)

	self.Metadata.SuspensionOutput = OutputForce
	return self.Metadata.SuspensionOutput
end

function WheelClass:_GetTurning(Raycast)
	local Config = self._Vehicle._Config;
	local VehiclePrimaryPart = self._Vehicle._Model.PrimaryPart;
	local VehicleSeat = self._Vehicle._Model.DriveSeat;

	local CarCFrame = VehiclePrimaryPart.CFrame
	local RotationsOnlyWheelDirCFrame = CFrame.lookAt(Vector3.zero, CarCFrame.LookVector, CarCFrame.UpVector)

	if string.sub(self._Wheel.Name, 1, 1) == 'F' then
		RotationsOnlyWheelDirCFrame = RotationsOnlyWheelDirCFrame * CFrame.Angles(0, -math.rad(self.Metadata.SmoothSteer * Config.SteerAngle), 0)
	end

	local LocalVelocity = RotationsOnlyWheelDirCFrame:ToObjectSpace(CFrame.new(VehiclePrimaryPart:GetVelocityAtPosition(Raycast.Position)))
	local Xforce = RotationsOnlyWheelDirCFrame.RightVector * -LocalVelocity.x * Config.wheelFriction
	local Zforce = RotationsOnlyWheelDirCFrame.LookVector * VehicleSeat.ThrottleFloat * Config.torque * (math.sign(-LocalVelocity.z) == VehicleSeat.Throttle and (1 - math.min(1, math.abs(LocalVelocity.z) / Config.MaxSpeed)) or 1)

	self.Metadata.TurningOutput = {
		X = Vector3.new(Xforce.X, 0, Xforce.Z),
		Z = Vector3.new(Zforce.X, 0, Zforce.Z),
	}

	return self.Metadata.TurningOutput
end

-- Public Methods
function WheelClass:Update(DeltaTime)
	local Raycast = self:_Raycast(self._WheelAttachment.Position)

	if Raycast then
		self:_GetSmoothSteer(DeltaTime)
		local SuspensionForce = self:_GetSuspension(Raycast)
		local TurningForce = self:_GetTurning(Raycast)

		self._WheelAttachment.SpringForce.Force = (SuspensionForce + ((TurningForce.X + TurningForce.Z) * 40))
	else
		self._WheelAttachment.SpringForce.Force = Vector3.zero
	end
end

function WheelClass:Destroy()

end

return WheelClass