Adding friction to raycast/spherecast car suspension

Hello! Ive been making a spherecast / raycast car suspension system and Ive been running into a problem. The car suspension works perfectly except the fact that the car moves like its on ice. I figure it needs friction but Im not sure how to implement this to my current math so I was wondering if anyone could help me with this.

This is my suspension module code:

-- Services
local replicatedStorage = game:GetService("ReplicatedStorage")
local runService = game:GetService("RunService")

-- Modules
local debugEffects = require(replicatedStorage.Effects.Debug)

-- Module
local suspension = {}
suspension.__index = suspension

local debugEffect

-- Functions
local function absoluteVector(vector : Vector3)
	return Vector3.new(math.abs(vector.X), math.abs(vector.Y), math.abs(vector.Z))
end

function suspension.new(vehicle : Model, wheelAttachment : Attachment)
	local self = setmetatable({}, suspension)
	
	self.vehicle = vehicle
	self.wheel = wheelAttachment
	self.wheelPart = self.vehicle.Wheels[self.wheel.Name]
	self.vectorForce = self.wheel.VectorForce
	
	self.restLength = self.wheelPart:GetAttribute("RestLength")
	self.wheelRadius = self.wheelPart:GetAttribute("WheelRadius")
	self.springStiffness = self.wheelPart:GetAttribute("SpringStiffness")
	self.springTravel = self.wheelPart:GetAttribute("SpringTravel")
	self.damperStiffness = self.wheelPart:GetAttribute("DamperStiffness")
	
	self.minLength = (self.restLength - self.springTravel)
	self.maxLength = (self.restLength + self.springTravel)
	
	self.lastLength = self.wheelRadius
	self.spherecast = nil
	
	self.debug = true
	
	if self.debug then
		self.debugOrigin = self.vehicle.Debug.Origin[string.gsub(self.wheel.Name, "Wheel", "Origin")]
		self.debugEnd = self.vehicle.Debug.End[string.gsub(self.wheel.Name, "Wheel", "End")]
		self.debugSphere = self.vehicle.Debug.Spheres[string.gsub(self.wheel.Name, "Wheel", "Sphere")]
		self.debugRay = self.debugOrigin.Beam
	end
	
	self.raycastParams = RaycastParams.new()
	self.raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	self.raycastParams.FilterDescendantsInstances = {self.vehicle}
	
	return self
end

function suspension:update(deltaTime : number)
	self:updateProperties()
	self:calculatePhysics(deltaTime)
	self:applyForce()
end

function suspension:updateProperties()
	self.restLength = self.wheelPart:GetAttribute("RestLength")
	self.wheelRadius = self.wheelPart:GetAttribute("WheelRadius")
	self.springStiffness = self.wheelPart:GetAttribute("SpringStiffness")
	self.springTravel = self.wheelPart:GetAttribute("SpringTravel")
	self.damperStiffness = self.wheelPart:GetAttribute("DamperStiffness")
end

function suspension:calculatePhysics(deltaTime : number)
	self.spherecast = workspace:Spherecast(self.wheel.WorldPosition, self.wheelRadius / 2, -self.vehicle.PrimaryPart.CFrame.UpVector * self.wheelRadius, self.raycastParams)

	if self.spherecast then
		self:updateDebug(self.wheel.WorldPosition)
		
		local extension = self.wheelRadius - self.spherecast.Distance
		local relativeVelocity = self.vehicle.PrimaryPart.CFrame:VectorToObjectSpace(self.wheelPart.Velocity)
		
		self.springLength = (self.spherecast.Distance - self.wheelRadius)
		local springLength = math.clamp(self.springLength, self.minLength, self.maxLength)
		self.springVelocity = (self.lastLength - self.springLength) / deltaTime
		self.springForce = self.vehicle.PrimaryPart.CFrame.UpVector * (self.springStiffness * springLength - self.damperStiffness * relativeVelocity.Y)
		self.damperForce = self.damperStiffness * self.springVelocity
		
		self.suspensionForce = (Vector3.new(0, self.springForce, 0) + Vector3.new(0, self.damperForce, 0))
		
		self.lastLength = self.springLength
	end
end

function suspension:updateDebug(origin : Vector3)
	if not self.debug then return end
	self.raycast = workspace:Raycast(self.wheel.WorldPosition, -self.vehicle.PrimaryPart.CFrame.UpVector * self.wheelRadius * 100, self.raycastParams)
	
	if self.spherecast and self.raycast then
		local distance = (origin - self.raycast.Position)
		local position = Vector3.new(origin.X, distance.Y, origin.Z)
		
		self.debugRay.Color = ColorSequence.new(Color3.fromRGB(255, 220, 23))

		self.debugOrigin.Position = origin
		self.debugEnd.Position = self.raycast.Position
		
		local radius = self.wheelRadius / 2
		self.debugSphere.Position = self.raycast.Position + Vector3.new(0, radius, 0)
		self.debugSphere.Size = Vector3.new(radius * 2, radius * 2, radius * 2)
		
		self.debugOrigin.Orientation = Vector3.zero
		self.debugEnd.Orientation = Vector3.zero
		
	elseif not self.raycast or self.spherecast then
		self.debugRay.Color = ColorSequence.new(Color3.fromRGB(73, 255, 7))
	end
end

function suspension:applyForce()
	if not self.spherecast then self.vectorForce.Force = Vector3.zero return end
	
	self.vectorForce.Force = self.suspensionForce
end

-- Returning
return suspension

This code is ran on the server in a loop for now.
Heres an example of the car skating:

External Media
2 Likes

For a simple raycast vehicle system i’d use the formula: F = μ*N

μ being the coefficient of friction
N being the normal force, in my case N is calculated like this: springVerticalForce + mass * 9.81

To apply the calculated force you would simply do:

local WorldCFrame = self.wheel.WorldCFrame
local WheelLookVector = WorldCFrame.LookVector
local WheelRightVector = WorldCFrame.RightVector

local g = 9.81
local Fz = self.suspensionForce.Magnitude + mass * g
local F = grip * Fz

local lateralFriction = WheelRightVector * F
local longitudinalDriveForce = WheelLookVector * Torque -- this is not how you would realistically do it but its just so u can test it out

self.vectorForce.Force = self.suspensionForce + lateralFriction + longitudinalDriveForce

I’d recommend watching this video which explains how to create a raycast based vehicle system:
https://www.youtube.com/watch?v=CdPYlj5uZeI

1 Like

The code you provided here doesn’t make any sense in the context of raycast cars. The lateral friction value will always be applied in the same direction and will only vary depending on the suspension force being applied and not the actual sliding motion of the car. Your lateral friction force should be dependent on the lateral motion of the wheels.

Reading my code i determined you are correct! I forgot to add the part where its based on how much lateral velocity there is. Thanks for bringing my attention to this.