THE TOPIC IS FOCUSING ON RAYCAST SUSPENSION NOT ROBLOX’S SUSPENSION CONSTRAINTS, AND IS AN INDEPENDANT SYSTEM.
This is approximately my 7th time trying to achieve an Editable, Open, Stable and Optimized Ray-cast Suspension, I’ve seen so many developers do this saying its simple which makes me clench my fists and want to absolutely obliterate my setup, but I don’t have the energy nor the ability to replace it. I’ve created this post to work together to solve this problem; Jittering, Flinging, Odd Events, etc.
Before I start packing my bags to go to Godot and becoming an actual Solo Dev, let me give this one more full on try.
Why Choose Ray-cast/Independently Created Suspension?
- Full Customization & Control
- Optimized Performance (Roblox’s spring constraints has features we do not need most of the time)
- More Realistic & Versatile Behavior
- Compatibility & Expandability (unlike Spring Constraints)
- Easier Debugging & Troubleshooting
Researched Articles and Resources
Briefly, making suspension In other games does not require much thinking and effort since the Physics engine is quite linear and predictable. For example this is what I would do for a drivable body,
--PSEUDOCODE
VEHICLE_BODY = ...
WHEEL_POINTS = {...} OR [...]
UPDATE:
FOR EACH WHEEL_POINTS (POINT):
POINT:ApplyForce(UPVECTOR*STRENGTH*math.clamp(rayDistance/MAXLENGTH, 0, 1)
But much more calculations are involved just for some normal looking suspension in roblox.
Resources I’ve Used for Ray-cast Based Suspension
- Space Dust Racing UE4 Arcade Vehicle Physics Tour
- Suspension vector direction of a raycast vehicle
- How to implement raycast car?
- How simple suspensions work
- Making Custom Car Physics in Unity (for Very Very Valet)
- Racing Game Guide #3 [ My Suspension method And Logic ] | ashdev
The Problems
But for some fudging reason, I need to know copious amounts of rocket science just to make it stably float on a flat surface and nothing else; Its very complicated to achieve pleasing but simple results. Here are some factors/facts and results about it:
- Impulse Sucks, Vector Forces do nothing but are the best way to go and Body Velocities/Gyros are deprecated (buggy)
- Friction/Traction Forces on the Right-Vector are not accurate, for whatever reason.
- Adding other features like changeable dampening breaks the system completely.
- Suspension Results are either to Upright-ish (not realistic) or very buggy and unrealistic in certain cases, for example your car flipping onto its side and instantly getting back onto its wheels
Visual Examples (Projects I was trying to make)
Project Build
This is where the research is started and concluded.
Project Overview
Car Hierarchy
Functionality
Main Script
--================
--> SERVICES
--================
local RunService = game:GetService("RunService")
--================
--> Set-up
--================
local CarModel = workspace.Car
local Root = CarModel.PrimaryPart or CarModel:WaitForChild("Root")
--================
--> MODULES
--================
local ValueHandler = require(script.ValueHandler)
local SuspensionModule = require(script.SuspensionModule)
local GizmoModule = require(script.GizmoModule)
local Config = ValueHandler.New(CarModel:FindFirstChild("Configuration"))
--=====================
--> OBJECTS AND VALUES
--=====================
local WheelAttachments = Root:GetChildren()
local CachedWheelData = {}
local THRUST_OFFSET_Z = 3
local THRUST_OFFSET_Y = -1
local MIN_LENGTH = 1
local TORQUE = 18
local Velocity = Vector3.zero
-- Properties
local SpringStiffness = Config:GetValue("SpringStiffness")
local SpringDamping = Config:GetValue("SpringDamping")
local SpringLength = Config:GetValue("SpringLength")
local WheelRadius = Config:GetValue("WheelRadius")
local WheelStiffness = Config:GetValue("WheelStiffness")
local WheelFriction = Config:GetValue("WheelFriction")
local WheelDamping = Config:GetValue("WheelDamping")
local Gas = Config:GetValue("Gas")
--=====================
--> FUNCTIONS
--=====================
GizmoModule:Enable()
local function updateProperties()
SpringStiffness = Config:GetValue("SpringStiffness")
SpringDamping = Config:GetValue("SpringDamping")
SpringLength = Config:GetValue("SpringLength")
WheelRadius = Config:GetValue("WheelRadius")
WheelStiffness = Config:GetValue("WheelStiffness")
WheelFriction = Config:GetValue("WheelFriction")
WheelDamping = Config:GetValue("WheelDamping")
Gas = Config:GetValue("Gas")
end
local function updateWheel(attachment:Attachment, delta)
local WheelRaycast = SuspensionModule.Raycast(attachment.WorldPosition, -attachment.WorldCFrame.UpVector, SpringLength, WheelRadius, attachment.CFrame.LookVector, {CarModel})
local VectorForce = attachment:FindFirstChild("VectorForce")
local PointCFrame = attachment.WorldCFrame
local PointPosition = attachment.WorldPosition
local RightVector = PointCFrame.RightVector
local UpVector = PointCFrame.UpVector
local FowardVector = PointCFrame.LookVector
if not VectorForce or not WheelRaycast then
if not WheelRaycast and VectorForce then
VectorForce.Force = Vector3.zero
end
return
end
local RayDistance = WheelRaycast.Distance
-- Steering/Friction
local MaxFrictionForce = WheelFriction * WheelRaycast.Normal
local RightAxisForce = MaxFrictionForce
-- Suspension
local CompressionRatio = 1-(RayDistance/(SpringLength+WheelRadius))
local PreviousCompressionRatio = CachedWheelData[attachment.Name] and CachedWheelData[attachment.Name].CompressionRatio or 0
local UpAxisForce = SpringStiffness * CompressionRatio
-- Gas/Thrust/Braking
local FowardAxisForce = 0 -- keeping focus on suspension..
VectorForce.Force = (RightVector * RightAxisForce) + (UpVector * UpAxisForce) + (FowardVector * FowardAxisForce)
CachedWheelData[attachment.Name] = {
CompressionRatio = CompressionRatio,
SurfaceImpactPoint = WheelRaycast.Position,
SurfaceImpactNormal = WheelRaycast.Normal
}
end
--=====================
--> CONNECTIONS
--=====================
Config.ValueChanged:Connect(function(valueName, newValue)
updateProperties()
end)
RunService.Heartbeat:Connect(function(delta)
for _, attachment in WheelAttachments do updateWheel(attachment,delta) end
--local Gas = Config:GetValue("Gas")
local DriveOutput = Gas * TORQUE
local RootCFrame = Root.CFrame
local RootPosition = Root.Position
local RightVector = RootCFrame.RightVector
local UpVector = RootCFrame.UpVector
local FowardVector = RootCFrame.LookVector
-- Gas/Braking
Root:ApplyImpulseAtPosition((FowardVector * DriveOutput), Root.Position + (FowardVector * THRUST_OFFSET_Z) + (UpVector * THRUST_OFFSET_Y))
end)
Suspension Module
--================
--> Set-up
--================
local Gizmos = require(script.Parent.GizmoModule)
local suspension_module = {}
--================
--> Raycasting
--================
function suspension_module.Raycast(start : Vector3, direction : Vector3, length:number, wheelRadius:number, foward, ignore:{any}) : RaycastResult
local raycast_params = RaycastParams.new()
raycast_params.FilterType = Enum.RaycastFilterType.Exclude
raycast_params.FilterDescendantsInstances = ignore
local ray_end = direction*length
local raycast_result = workspace:Raycast(start, direction * (length+wheelRadius), raycast_params)
Gizmos.Point(start, 0.1)
if raycast_result then
Gizmos.Arrow(start, raycast_result.Position, 0.5)
local WheelCFrame = CFrame.new(raycast_result.Position) * CFrame.new(0,wheelRadius,0) * CFrame.lookAt(start, start+foward).Rotation
Gizmos.Circle(WheelCFrame, wheelRadius, 0.02)
return raycast_result
else
Gizmos.Point(start+ray_end, 0.1)
Gizmos.Line(start, start+ray_end, 5)
local WheelCFrame = CFrame.new(start+ray_end) * CFrame.new(0,wheelRadius,0) * CFrame.lookAt(start, start+foward).Rotation
Gizmos.Circle(WheelCFrame, wheelRadius, 0.02)
end
return false
end
function suspension_module.SphereCast(start : Vector3, direction : Vector3, length : number, radius : numbers, ignore : {any}) : RaycastResult
local sphere_cast_params = RaycastParams.new()
sphere_cast_params.FilterType = Enum.RaycastFilterType.Exclude
sphere_cast_params.FilterDescendantsInstances = ignore
local sphere_cast_result = workspace:Spherecast(start, radius, direction * length, sphere_cast_params)
if sphere_cast_result then
return sphere_cast_result
end
return false
end
function suspension_module.PartCast(start, direction, length, part, ignore) : RaycastResult
local part_cast_params = RaycastParams.new()
part_cast_params.FilterType = Enum.RaycastFilterType.Exclude
part_cast_params.FilterDescendantsInstances = ignore
local part_cast_result = workspace:Shapecast(part, direction * length, part_cast_params)
if part_cast_result then
return part_cast_result
end
return false
end
return suspension_module
Current Project ) Car | Project File (86.2 KB)
Concluded Project ) No Official Conclusion yet...
Record Keeping
Established Date : 2025-02-21T00:00:00Z
Ended Data : N/A