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.
image
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 :smiley:)

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! :smiley:

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