Thruster Whitelist for Roblox Car

I’m having trouble with roblox’s default car suspension. I’m trying to script the cars to not collide with each other using collision groups, but the thrusters in the wheels that keep the wheels at the right level corresponding with the ground will try readjust when going through another car, causing the car to flip out. I’m trying to create a whitelist for the thrusters to avoid this behaviour and make it possible to pass through another car while driving. I’ve tried setting the suspension configuration to 0 which realisticly fixes the previous problem, but of course now the car has no suspension and is horrible to control over uneven ground.

Here’s the script that sets all of the car parts into a collision group (unoptimized)

local PhysicsService = game:GetService("PhysicsService")
local car = game.ReplicatedStorage.Car

local cars = "Cars"

-- Add an object to each group
PhysicsService:SetPartCollisionGroup(car.Chassis, cars)
PhysicsService:SetPartCollisionGroup(car.Union1, cars)
PhysicsService:SetPartCollisionGroup(car.Union2, cars)
PhysicsService:SetPartCollisionGroup(car.Union3, cars)
PhysicsService:SetPartCollisionGroup(car.Union4, cars)
PhysicsService:SetPartCollisionGroup(car.Union5, cars)
PhysicsService:SetPartCollisionGroup(car.Union6, cars)
PhysicsService:SetPartCollisionGroup(car.Back, cars)
PhysicsService:SetPartCollisionGroup(car.Light1, cars)
PhysicsService:SetPartCollisionGroup(car.Light2, cars)
PhysicsService:SetPartCollisionGroup(car.Part, cars)
PhysicsService:SetPartCollisionGroup(car.Part2, cars)
PhysicsService:SetPartCollisionGroup(car.Part3, cars)
PhysicsService:SetPartCollisionGroup(car.DriveSeat, cars)
PhysicsService:SetPartCollisionGroup(car.BackLeftWheel, cars)
PhysicsService:SetPartCollisionGroup(car.BackRightWheel, cars)
PhysicsService:SetPartCollisionGroup(car.FrontLeftWheel, cars)
PhysicsService:SetPartCollisionGroup(car.FrontRightWheel, cars)
for i, v in pairs(car:GetChildren()) do 
	if v.Name == "Thruster" then 
		PhysicsService:SetPartCollisionGroup(v, cars)
	end
end

and here are the 3 scripts used in the car:

--Car Script, Server side
--Scripted by DermonDarble

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

local mass = 0

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

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

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

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)

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)

--LocalCarScript, local script, child of carscript
--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)

--RaycastModule, Module Script, child of carscript
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

Are you sure you’ve got all the car Parts set to the proper CollisionGroup?

Also, and fairly important, you can just create CollisionGroups manually, then select every Part in the car Model you want to not Collide with another car’s Parts, then drag and drop them into the proper collision group you created in the folder. They’ll automatically be changed and you won’t have to script any of it.

Yeah all parts are definitely in the collision group and I’m aware the method I did it is far from effective but it works. The only issue is the thrusters that simulate the suspension still treat the other car parts as solid objects, and I’m just trying to create a whitelist so they ignore any parts within that collisiongroup

The cars on ROBLOX’s built-in places use a long-deprecated means of raycasting that does not allow you to specify a CollisionGroup. This modified version of the raycast module uses the current raycasting method while maintaining the original behavior and uses the “Cars” CollisionGroup to determine collisions.

The collision group it uses to perform the raycast can be anything so long as that group does not collide with the “Cars” group.

local module = {}

local raycast_params = RaycastParams.new()
--Set the collision group to any group that is set not to collide with "Cars"
raycast_params.CollisionGroup = "Cars"
raycast_params.IgnoreWater = true
raycast_params.FilterType = Enum.RaycastFilterType.Blacklist

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

	local hit
	
	while distance < maxDistance - 0.1 do
		raycast_params.FilterDescendantsInstances = ignore
		
		local ray = Ray.new(lastPosition, direction * (maxDistance - distance))
		hit = workspace:Raycast(lastPosition, direction * (maxDistance - distance), raycast_params)
		if hit then
			if hit.Instance.CanCollide then
				break
			else
				table.insert(ignore, hit.Instance)
				distance = (startPosition - hit.Position).Magnitude
				lastPosition = hit.Position
			end
		else
			break
		end
	end
	if hit then
		return hit.Instance, hit.Position, hit.Normal
	else
		return nil, startPosition + startDirection, Vector3.zero
	end
end

return module

1 Like

Logged out of my account, sorry for taking so long to get back to you. This worked perfectly

1 Like