# Help On Scripted Suspension

G’day,

So a lot of people like me are struggling to figure out how the jeep model (found in suburban) works and how it is scripted. Here are some of my questions on it

Here is what I have figured out so far:

The ‘Thruster’ parts of the car are welded to the wheels meaning as a Thruster part rises, the wheel rises. The configurations folder is a place where all the ‘settings’ of the jeep is found as well.

Now going into the ‘CarScript’

This is all the variables

``````--Scripted by DermonDarble

local car = script.Parent
local stats = car.Configurations
local Raycast = require(script.RaycastModule)
``````

The pairs loops through all the BaseParts in the model and gets the mass and multiplies it by gravity then storing it in the mass variable for later use.

What does this formula do?

``````local mass = 0

for i, v in pairs(car:GetChildren()) do
if v:IsA("BasePart") then
mass = mass + (v:GetMass() * 196.2)
end
end
``````

What is the BodyPosition force used for?

What is the BodyGyro force used for?

``````local bodyPosition = Instance.new("BodyPosition", car.Chassis)
bodyPosition.MaxForce = Vector3.new()
local bodyGyro = Instance.new("BodyGyro", car.Chassis)
bodyGyro.MaxTorque = Vector3.new()
``````

How does this function work? (Please explain simply)

As far as I am aware, this function just updates the position of a Thruster

``````local function UpdateThruster(thruster)
-- Raycasting
local hit, position = Raycast.new(thruster.Position, thruster.CFrame:vectorToWorldSpace(Vector3.new(0, -1, 0)) * stats.Height.Value) --game.Workspace:FindPartOnRay(ray, car)
local thrusterHeight = (position - thruster.Position).magnitude

-- Wheel
local wheelWeld = thruster:FindFirstChild("WheelWeld")
wheelWeld.C0 = CFrame.new(0, -math.min(thrusterHeight, stats.Height.Value * 0.8) + (wheelWeld.Part1.Size.Y / 2), 0)
-- Wheel turning
local offset = car.Chassis.CFrame:inverse() * thruster.CFrame
local speed = car.Chassis.CFrame:vectorToObjectSpace(car.Chassis.Velocity)
if offset.Z < 0 then
local direction = 1
if speed.Z > 0 then
direction = -1
end
wheelWeld.C0 = wheelWeld.C0 * CFrame.Angles(0, (car.Chassis.RotVelocity.Y / 2) * direction, 0)
end

-- Particles
if hit and thruster.Velocity.magnitude >= 5 then
wheelWeld.Part1.ParticleEmitter.Enabled = true
else
wheelWeld.Part1.ParticleEmitter.Enabled = false
end
end
``````

Not sure what this function does, neither do I know what the ‘LocalCarScript’ does, please explain (simply)

``````car.DriveSeat.Changed:connect(function(property)
if property == "Occupant" then
if car.DriveSeat.Occupant then
local player = game.Players:GetPlayerFromCharacter(car.DriveSeat.Occupant.Parent)
if player then
car.DriveSeat:SetNetworkOwner(player)
local localCarScript = script.LocalCarScript:Clone()
localCarScript.Parent = player.PlayerGui
localCarScript.Car.Value = car
localCarScript.Disabled = false
end
end
end
end)
``````

What does this function do?

``````spawn(function()
while true do
game:GetService("RunService").Stepped:wait()
for i, part in pairs(car:GetChildren()) do
if part.Name == "Thruster" then
UpdateThruster(part)
end
end
if car.DriveSeat.Occupant then
bodyPosition.MaxForce = Vector3.new()
bodyGyro.MaxTorque = Vector3.new()
else
local hit, position, normal = Raycast.new(car.Chassis.Position, car.Chassis.CFrame:vectorToWorldSpace(Vector3.new(0, -1, 0)) * stats.Height.Value)
if hit and hit.CanCollide then
bodyPosition.MaxForce = Vector3.new(mass / 5, math.huge, mass / 5)
bodyPosition.Position = (CFrame.new(position, position + normal) * CFrame.new(0, 0, -stats.Height.Value + 0.5)).p
bodyGyro.MaxTorque = Vector3.new(math.huge, 0, math.huge)
bodyGyro.CFrame = CFrame.new(position, position + normal) * CFrame.Angles(-math.pi/2, 0, 0)
else
bodyPosition.MaxForce = Vector3.new()
bodyGyro.MaxTorque = Vector3.new()
end
end
end
end)
``````

Here is the LocalCarScript

``````--Scripted by DermonDarble
local userInputService = game:GetService("UserInputService")
local camera = game.Workspace.CurrentCamera
local player = game.Players.LocalPlayer
local character = player.Character
local humanoidRootPart = character.HumanoidRootPart
local car = script:WaitForChild("Car").Value
local stats = car:WaitForChild("Configurations")
local Raycast = require(car.CarScript.RaycastModule)

local cameraType = Enum.CameraType.Follow

local movement = Vector2.new()

local seat = car:WaitForChild("DriveSeat")

seat.Changed:Connect(function(property)
if property == "Throttle" then
movement = Vector2.new(movement.X, seat.Throttle)
end
if property == "Steer" then
movement = Vector2.new(seat.Steer, movement.Y)
end
end)

local force = 0
local damping = 0

local mass = 0

for i, v in pairs(car:GetChildren()) do
if v:IsA("BasePart") then
mass = mass + (v:GetMass() * 196.2)
end
end

force = mass * stats.Suspension.Value
damping = force / stats.Bounce.Value

local bodyVelocity = Instance.new("BodyVelocity", car.Chassis)
bodyVelocity.velocity = Vector3.new(0, 0, 0)
bodyVelocity.maxForce = Vector3.new(0, 0, 0)

local bodyAngularVelocity = Instance.new("BodyAngularVelocity", car.Chassis)
bodyAngularVelocity.angularvelocity = Vector3.new(0, 0, 0)
bodyAngularVelocity.maxTorque = Vector3.new(0, 0, 0)

local rotation = 0

local function UpdateThruster(thruster)
--Make sure we have a bodythrust to move the wheel
local bodyThrust = thruster:FindFirstChild("BodyThrust")
if not bodyThrust then
bodyThrust = Instance.new("BodyThrust", thruster)
end
--Do some raycasting to get the height of the wheel
local hit, position = Raycast.new(thruster.Position, thruster.CFrame:vectorToWorldSpace(Vector3.new(0, -1, 0)) * stats.Height.Value)
local thrusterHeight = (position - thruster.Position).magnitude
if hit and hit.CanCollide then
--If we're on the ground, apply some forces to push the wheel up
bodyThrust.force = Vector3.new(0, ((stats.Height.Value - thrusterHeight)^2) * (force / stats.Height.Value^2), 0)
local thrusterDamping = thruster.CFrame:toObjectSpace(CFrame.new(thruster.Velocity + thruster.Position)).p * damping
bodyThrust.force = bodyThrust.force - Vector3.new(0, thrusterDamping.Y, 0)
else
bodyThrust.force = Vector3.new(0, 0, 0)
end

--Wheels
local wheelWeld = thruster:FindFirstChild("WheelWeld")
if wheelWeld then
wheelWeld.C0 = CFrame.new(0, -math.min(thrusterHeight, stats.Height.Value * 0.8) + (wheelWeld.Part1.Size.Y / 2), 0)
-- Wheel turning
local offset = car.Chassis.CFrame:inverse() * thruster.CFrame
local speed = car.Chassis.CFrame:vectorToObjectSpace(car.Chassis.Velocity)
if offset.Z < 0 then
local direction = 1
if speed.Z > 0 then
direction = -1
end
wheelWeld.C0 = wheelWeld.C0 * CFrame.Angles(0, (car.Chassis.RotVelocity.Y / 2) * direction, 0)
end
wheelWeld.C0 = wheelWeld.C0 * CFrame.Angles(rotation, 0, 0)
end
end

--A simple function to check if the car is grounded
local function IsGrounded()
local hit, position = Raycast.new((car.Chassis.CFrame * CFrame.new(0, 0, (car.Chassis.Size.Z / 2) - 1)).p, car.Chassis.CFrame:vectorToWorldSpace(Vector3.new(0, -1, 0)) * (stats.Height.Value + 0.2))
if hit and hit.CanCollide then
return(true)
end
return(false)
end

local oldCameraType = camera.CameraType
camera.CameraType = cameraType

spawn(function()
while character.Humanoid.SeatPart == car.DriveSeat do
game:GetService("RunService").RenderStepped:wait()

--Broken gyro input
--[[if userInputService.GyroscopeEnabled then
local inputObject, cframe = userInputService:GetDeviceRotation()
local up = cframe:vectorToWorldSpace(Vector3.new(0, 1, 0))
local angle = (1 - up.Y) * (math.abs(up.X) / up.X)
movement = Vector2.new(angle, movement.Y)
end]]

if IsGrounded() then
if movement.Y ~= 0 then
local velocity = car.Chassis.CFrame.lookVector * movement.Y * stats.Speed.Value
car.Chassis.Velocity = car.Chassis.Velocity:Lerp(velocity, 0.1)
bodyVelocity.maxForce = Vector3.new(0, 0, 0)
else
bodyVelocity.maxForce = Vector3.new(mass / 2, mass / 4, mass / 2)
end
local rotVelocity = car.Chassis.CFrame:vectorToWorldSpace(Vector3.new(movement.Y * stats.Speed.Value / 50, 0, -car.Chassis.RotVelocity.Y * 5 * movement.Y))
local speed = -car.Chassis.CFrame:vectorToObjectSpace(car.Chassis.Velocity).unit.Z
rotation = rotation + math.rad((-stats.Speed.Value / 5) * movement.Y)
if math.abs(speed) > 0.1 then
rotVelocity = rotVelocity + car.Chassis.CFrame:vectorToWorldSpace((Vector3.new(0, -movement.X * speed * stats.TurnSpeed.Value, 0)))
bodyAngularVelocity.maxTorque = Vector3.new(0, 0, 0)
else
bodyAngularVelocity.maxTorque = Vector3.new(mass / 4, mass / 2, mass / 4)
end
car.Chassis.RotVelocity = car.Chassis.RotVelocity:Lerp(rotVelocity, 0.1)

--bodyVelocity.maxForce = Vector3.new(mass / 3, mass / 6, mass / 3)
--bodyAngularVelocity.maxTorque = Vector3.new(mass / 6, mass / 3, mass / 6)
else
bodyVelocity.maxForce = Vector3.new(0, 0, 0)
bodyAngularVelocity.maxTorque = Vector3.new(0, 0, 0)
end

for i, part in pairs(car:GetChildren()) do
if part.Name == "Thruster" then
UpdateThruster(part)
end
end
end
for i, v in pairs(car:GetChildren()) do
if v:FindFirstChild("BodyThrust") then
v.BodyThrust:Destroy()
end
end
bodyVelocity:Destroy()
bodyAngularVelocity:Destroy()
camera.CameraType = oldCameraType
userInputService.ModalEnabled = false
script:Destroy()
end)
``````

And the RaycastModule

``````local module = {}

function module.new(startPosition, startDirection)
local maxDistance = startDirection.magnitude
local direction = startDirection.unit
local lastPosition = startPosition
local distance = 0
local ignore = {}

local hit, position, normal

repeat
local ray = Ray.new(lastPosition, direction * (maxDistance - distance))
hit, position, normal = game.Workspace:FindPartOnRayWithIgnoreList(ray, ignore, false, true)
if hit then
if not hit.CanCollide then
table.insert(ignore, hit)
end
end
distance = (startPosition - position).magnitude
lastPosition = position
until distance >= maxDistance - 0.1 or (hit and hit.CanCollide)
return hit, position, normal
end

return module
``````

If someone could explain how all this works in clear steps, I know a lot of people would benefit from this. I myself have been struggling to understand how this works for ages, thanks SOOOOO much if you can get to me

7 Likes

This is the best overview of the portions you highlighted I can give:

This calculates the total weight of the vehicle.

The bodyposition is used to keep the car in place position-wise and the bodygyro is used to keep the car in place rotation-wise while there is nobody driving it.

Some things to note before we dive into the function:

• "Thruster" in this case refers to the wheels of the car.
• A ray trace occurs to determine how far the car is from the ground.
This function does multiple things:
1. Wheel: It updates the wheel's position relative to the chassis such that the wheel touches the ground where the ray hits. The wheel's distance from the chassis of the car is limited by thrusterHeight. This acts like wheel suspension.
2. Wheel turning: It compares the wheel's relative position to the chassis to find out what direction it's turning. It then finds how much the car is turning, and rotates the wheel proportionally.
3. Particles: If the ray trace hit a part, and the car is moving fast enough, particles will emit from the wheels.

When someone gets in the car, it sets the network owner of the car to the player. This means that the player will calculate the physics of the car, instead of the server. This prevents any lag you may see where you jump back in time and saves the server’s processing power.

It will then allow the player to control the car via the localCarScript that gets sent to the player to run.

This function will run every Heartbeat (aka every physics calculation loop). It does two things:

1. Update the position/rotation of the wheels via the UpdateThruster method as described previously.
2. If the car is being driven by a player, it will disable the bodyPosition/bodyGyro that is keeping the car in place.
3. Otherwise if the car is empty, it will calculate the car's distance from the ground and adjust the car's bodyPosition accordingly so that it stays on the surface of the ground perfectly. It will also determine if the car is on a slope(the normal of the surface) and change the car's bodyGyro accordingly so that it will be aligned with the slope. If the car is floating, it will disable the bodyPosition/bodyGyro until the car is back on the ground.

Here is the LocalCarScript

Some things that we haven’t touched on yet that may be important

• Keep in mind that updateThruster() is purely visual. The movement of the car relies on the bodyVelocity/bodyAngularVelocity. This script will listen for Throttle/Steer values that the VehicleSeat provides when the player provides input.

And the RaycastModule

Basically, this will cast a ray until it can’t find a part within maxDistance or if the hits a part that is collideable.

Hope this helps! I probably missed a few things or explained too far in detail so don’t be afraid to ask for clarification if you’re still confused on some things. You may need to take a step back and review some things provided by the wiki. I sure had fun dissecting through this though!

10 Likes

In the CarScript parented to the Jeep model, when it casts a ray down, wouldn’t the ray intersect the wheel and nothing else?

2 Likes

The wheels are noncollidable so the RayModule will ignore them.

3 Likes

And also, on line 27 why does it multiply the stats.Height.Value by 0.8? Why 0.8 and what does the number mean?

2 Likes

Since the height is the distance from the chassis to the ground, and the raycast uses that distance to determine if the wheels are touching the ground, 80% of the height is used to compensate for the height of the wheels since the position of the wheel is the center of it. Otherwise, the wheels would be exceeding the specified height of the vehicle. Seems like that number worked best for the situation.

1 Like

With the improvements to physics constraints over the past few years, I would definitely recommend using constraint suspension now instead of trying to use coded suspension unless you’re an experienced scripter.

3 Likes

Thanks! I have looked at those tutorials, but I want to try something different and challenging.

1 Like

I’m confused with the whole of line 27. When it says wheelWeld.C0,‘C0’ isn’t even a property of ‘wheelWeld’ if you look at the properties of wheelWeld. What even does line 27 do in general?

1 Like

C0 and C1 of welds is hidden from the properties but they do exist. As I’ve described above, it sets the position of the wheel so that the wheel will always be in contact with the ground (using the distance from raycasting) so it acts like a suspension system.

1 Like

But the wheels are just for decoration, they don’t actually affect the physics of the car as they have CanCollide off?

1 Like

Yes the wheels are “moving” like a suspension would, but it wouldn’t do anything regarding the physics of the car.

2 Likes

Does C0 refer to the position of the thruster and C1 refers to the position wheel?

1 Like

Here’s an image that illustrates what C0 and C1 is.

Originally, C0 would be in the center of Part0, but if you modify the C0 value, the point at which the weld connects to the part would move.

Let’s say that the C0 point on the image is CFrame.new(-1, 1, 0). That means that C0 = Part0.Position * CFrame.new(-1, 1, 0).
If C0 == C1, then the parts would be inside each other.
Basically the difference of C0 and C1 correlates to offset of the two parts’ position.

How is this useful?
If you want to set the C1 of the weld but you want the offset to originate from a position that is not the center of the part, then you would modify C0.

2 Likes

Also, (on line 27) why does it add (wheelWeld.Part1.Size.Y / 2) to the y axis?

1 Like

The ray hit the ground, but the wheel’s position is where the wheel’s center is. So you need to add wheelWeld.part1.Size.Y/2 to get the center of the wheel.

2 Likes

Well, I have to go now, will you be available for questions some other day? (Btw you were extremely helpful and thank you so much for all the effort you put into answering my questions and helping me out )

2 Likes

On line 29, what does it do in general? And why does it use :inverse()? What does :inverse() do in this context?

1 Like

On line 29 in the car script, can you please explain what is happening in regards with the :Inverse()?
Thanks!

1 Like

I’m struggling with the part of the Car Script where it uses body movers to keep the car in place. I don’t understand lines 77 and 79:

``````bodyPosition.Position = (CFrame.new(position, position + normal) * CFrame.new(0, 0, -stats.Height.Value + 0.5)).p
bodyGyro.MaxTorque = Vector3.new(math.huge, 0, math.huge)
bodyGyro.CFrame = CFrame.new(position, position + normal) * CFrame.Angles(-math.pi/2, 0, 0)
``````

I don’t understand the use of normal and what it does, also when bodygyro.CFrame is set I again don’t understand the use of normal and also math.pi and what this does. Thanks