How to Rig a Car

A little off topic, but what makes ackerman steering better than “Same Angle Steering”?

3 Likes

With “Same Angle Steering”

See how one of the wheels is kind of misaligned? That wheel causes friction and skidding.

Ackermann:

The difference is subtle but it works, I assure you.

2 Likes

and now to work out how to apply that ackerman code into mine

1 Like

I may have broken the car…

local Car = script:WaitForChild("Car").Value
local StopValue = script:WaitForChild("Stop")
local SteeringAngle = script:WaitForChild("SteeringAngle")
local Player = game.Players.LocalPlayer

local Seat = Car.Body.VehicleSeat
local FL = Car.Chassis.Platform.AttachmentFL
local FR = Car.Chassis.Platform.AttachmentFR

local CFFL = Car.Chassis.Platform.CylindricalConstraintFL
local CFFR = Car.Chassis.Platform.CylindricalConstraintFR
local CFRL = Car.Chassis.Platform.CylindricalConstraintRL
local CFRR = Car.Chassis.Platform.CylindricalConstraintRR

local Steer = 0
local Throttle = 0

local heartbeat

local function Update(dt)
	
	-- Steer:
	local Goal = Seat.SteerFloat * SteeringAngle.Value
	Steer = Steer + (Goal - Steer) * math.min(dt * Seat.TurnSpeed, 1)
	FL.Orientation = Vector3.new(0, -Steer, -90)
	FR.Orientation = Vector3.new(0, -Steer, -90)
	
	-- Throttle:
	local ThrottleGoal = Seat.ThrottleFloat
	Throttle = Throttle + (ThrottleGoal - Throttle) * math.min(dt * Seat.TurnSpeed, 1)
	local Torque = Seat.Torque
	local Speed = Seat.MaxSpeed * Throttle
	CFFL.MotorMaxTorque = Torque
	CFFR.MotorMaxTorque = Torque
	CFRL.MotorMaxTorque = Torque
	CFRR.MotorMaxTorque = Torque
	CFFL.AngularVelocity = Speed
	CFFR.AngularVelocity = -Speed
	CFRL.AngularVelocity = Speed
	CFRR.AngularVelocity = -Speed
	
	Player.Character.Humanoid.JumpPower = 0
end

local function Start()
	print("Start Client")
	heartbeat = game:GetService("RunService").Heartbeat:Connect(Update)
end

local function Stop()
	print("Stop Client")

	heartbeat:Disconnect()
	wait(1)
	Player.Character.Humanoid.JumpPower = 50
	script:Destroy()
end

Start()

if (StopValue.Value) then Stop() return end

StopValue.Changed:Connect(function(ShouldStop)
	if (not ShouldStop) then return end
	
	Stop()
end)

game:GetService("UserInputService").InputBegan:Connect(function(Input)
	if (Input.KeyCode == Enum.KeyCode.F) then
		Player.Character.Humanoid.Jump = true
		Player.Character.HumanoidRootPart.CFrame = Car.TpPart.CFrame
	end
end)

edit: nvm i am dumb

3 Likes

Great tutorial, loved the way you navigated and explained everything and why it was in great detail. I do want to point out there is a large difference with cylinder vs sphere wheels. Sphere wheels have by far better collision that cylindrical ones, even more noticeable when turning.
(The gifs might not show it all but in-game its its 100% visible)

Here is the car I rigged with the tutorial using cylindrical wheel collision:
53e0ece5633175c0f7560aa9c7134aa1
Notice how when turning the wheels ‘phase’ and jump through the road when turning, and (not shown) when going at high speeds the wheels suffer from this as it gets more intense.

Heres the rig using spheres as wheel collision:
d1e2cbf77785700a71866b74c37a4153
Every turn now is silky smooth, no visible phasing as with cylindrical collisions. At high speeds it remains this smooth with no issues!

Now I do want to point out when changing to sphere collisions you loose the ability to grind into into walls as the sphere would collide with it before the cars body did, I guess a simple way to fix this would be to set it different collision in collisions group.
image|481x393

Either way, great tutorial. Hope to see more tutorials like this! (Was wondering how to do car gear calculations :eyes:)

16 Likes

There is a good topic on this:

Thats how I do my gears, and rpm’s and stuff ^

2 Likes

That looks great, I am doing something like that of my own: https://i.imgur.com/M6Ehzpm.png
Using a car that is close to my heart. It was my first car that i worked on with my dad

2 Likes

Out of curiosity, does anybody know what causes this / why it happens? I’ve always wanted to know.

2 Likes

I think the reason is due to lateral forces. On cylinders, there’s a hard edge. On actual tires, you don’t have a hard edge (wheels are rubbery and slightly rounded on the edges). Using a sphere simulates this a little better. This is just my assumption. I don’t actually know.

4 Likes

Am I going mad?
image
Because car is a value??

3 Likes

The “Car” object there is an ObjectValue in the script that points back to the actual Car model. This is needed because the script lives in the player’s backpack, so it doesn’t know which car model to use otherwise. Thus, we grab the value of that ObjectValue right away.

The Stop object is a BoolValue, but we don’t want to grab the value of it. We want the actual object because we will use it to listen for changes to the value.

7 Likes

Crazyman32 released a block of code that had the logic for ackerman steering, I thought it may be a good idea to incorporate that into the code he wrote for this tutorial. Anyways, here it is. Hope you find it useful.

local function Update(dt)
	-- Steer:
	local goal = -seat.SteerFloat * maxSteerAngle
	steer = steer + (goal - steer) * math.min(dt * seat.TurnSpeed, .5)
	local radians = math.rad(90 - math.abs(steer))
	local h =(attFR.WorldPosition - attRR.WorldPosition).Magnitude
	local z = (attRR.WorldPosition - attRL.WorldPosition).Magnitude
	local x = math.tan(radians) * h
	local y = (x + z)
	local outerTurnAngle = 90 - math.deg(math.atan2(y, h))
	if (steer > 0) then 
		attFL.Orientation = Vector3.new(0, steer, -90)
		attFR.Orientation = Vector3.new(0, outerTurnAngle, -90)
	else
		-- Right
		outerTurnAngle = -outerTurnAngle
		attFL.Orientation = Vector3.new(0, outerTurnAngle, -90)
		attFR.Orientation = Vector3.new(0, steer, -90)
	end
		
	-- Throttle:
	local throttleGoal = seat.ThrottleFloat
	throttle = throttle + (throttleGoal - throttle) * math.min(dt * seat.TurnSpeed, 1)
	local torque = seat.Torque
	local speed = seat.MaxSpeed * throttle
	cylFL.MotorMaxTorque = torque
	cylFR.MotorMaxTorque = torque
	cylRL.MotorMaxTorque = torque
	cylRR.MotorMaxTorque = torque
	cylFL.AngularVelocity = speed
	cylFR.AngularVelocity = -speed
	cylFL.AngularVelocity = speed
	cylRR.AngularVelocity = -speed
end

15 Likes

Since you are detecting inputs from the seat, why not just update the car on the server?

1 Like

There would be a noticeable delay between pressing a key to turn and the car turning. It takes time to send the request to the server and get a response. Doing it on the client guarantees instant feedback for controlling the car. It makes it feel less laggy

4 Likes

Beat you to it:


Additionally, real life tires are squishy.

Roblox Parts are not squishy.

Ensue instant, intense feedback whenever the physics engine does not like the hard edge colliding with the ground.

2 Likes

Thanks for the awesome tutorial. I had one issue i wanted to ask about. When my car makes a heavy impact the wheels tend to get knocked through the body and inverted. I added a flat part above the platform part and set it to collide with the wheels to limit the upward travel of the wheels. This helped but I still find sometimes my wheels get dislodged and wind up hanging from the springs. Is there something else that is needed to make the wheels more ‘attached’?

1 Like

It might help to add min/max length limits for the cylindrical constraint

1 Like

I think the spherical wheel use may depend on the use of the vehicle. I tried adding spherical wheels to mine and when testing in the Roblox demo racing came, they kept trying to climb over bridge railings and cliff walls where the cylindrical ones would bounce off and keep me on course better.

As for other suggestions, maybe add the collision group creation steps into the script so the vehicle model is more game portable.

Here is the code I added to mine so the script would create the collision groups automatically and assign the parts to them. I put this in the CarHandler script near the beginning just after the Cooldown function:

function getGroupId(name)
	-- GetCollisionGroupId will throw error if it does not exist
	local ok, groupId = pcall(physicsService.GetCollisionGroupId, physicsService, name)
	return ok and groupId or nil
end

local checkforgroup
checkforgroup = getGroupId("Character")
if checkforgroup == nil then physicsService:CreateCollisionGroup("Character") end
checkforgroup = getGroupId("CarBody")
if checkforgroup == nil then physicsService:CreateCollisionGroup("CarBody") end
checkforgroup = getGroupId("CarWheel")
if checkforgroup == nil then physicsService:CreateCollisionGroup("CarWheel") end
physicsService:CollisionGroupSetCollidable("Character", "CarBody", false)
physicsService:CollisionGroupSetCollidable("CarWheel", "CarBody", false)
for _,part in ipairs(script.Parent:GetDescendants()) do
		if (part:IsA("BasePart")) then
			if part.Name == "PhysicalWheel" then physicsService:SetPartCollisionGroup(part, "CarWheel")  end
			if part.Name == "Body" then physicsService:SetPartCollisionGroup(part, "CarBody") end
		end
end

This will allow you to export the model and import it into another game and not have to manually recreate the groups and assign the parts, assuming you labeled the parts exactly the same and the game doesn’t have the groups already in which case you may have some overlap issues. This worked for me.
(edit: I misunderstood how the check function worked, I’ve updated this script clip to work properly now if the group already exists)

Hello, I finished part 3, and it will not let me inside the car for some reason. Can someone help?

local car = script.Parent
local seat = car.Body.VehicleSeat
local body = car.Body.Body

local physicsService = game:GetService("PhysicsService")
local defaultCollisionGroup = "Default"
local characterCollisionGroup = "Character"

local cooldown = 0

local occupiedPlayer

local function Cooldown(duration)
	local cooldownTag = tick()
	cooldown = cooldownTag
	delay(duration, function()
		if (cooldown == cooldownTag) then
			cooldown = 0
		end
	end)
end

local function SetCharacterCollide(character, shouldCollide)
	local group = (shouldCollide and defaultCollisionGroup or characterCollisionGroup)
	for _,part in ipairs(character:GetDescendants()) do
		if (part:IsA("BasePart")) then
			part.Massless = not shouldCollide
			physicsService:SetPartCollisionGroup(part, group)
		end
	end
end

local function BodyTouched(part)
	
	if (seat.Occupant or cooldown ~= 0) then return end
	
	local character = part.Parent
	local player = game.Players:GetPlayerFromCharacter(character)
	if (not player) then return end
	
	local humanoid = character:FindFirstChildOfClass("Humaniod")
	if (not humanoid) then return end
	
	
	seat:Sit(humanoid)
	occupiedPlayer = player
	SetCharacterCollide(character, false)
	car.PrimaryPart:SetNetworkOwner(player)
	Cooldown(1)
end

local function OccupantChanged()
	if (seat.Occupant) then return end
	if (occupiedPlayer.Character) then
		SetCharacterCollide(character, false)
	end
	car.PrimaryPart:SetNetworkOwnershipAuto()
	occupiedPlayer = nil
end

body.Touched:Connect(BodyTouched)
seat:GetPropertyChangedSignal("Occupant"):Connect(OccupantChanged)