How to make my AI have better control over a vehicle?

Hello. I’m working on car AI, and it’s coming along ok. But for the past few days, I’ve been struggling on making it have better control over the vehicle it’s driving.

Here is a video of my problem:

Notice how when the police car tries to take down my car, it starts swerving alot, and can’t seem to stay in control of the car very well. I’ve tried taking into account the current angle of the steering wheels, and it’s distance from my vehicle, and it still has this issue.

I’m not sure what code I need to show, so I might as well give you the whole thing The cars.rbxl (99.1 KB)

I’ve already tried putting some of my values into a curve function (Such as an exponential curve) and it seems to help, but not to the extent that I want it to.

Here is the code, for those of you who don’t want to download it:

Beware, that it’s a bit messy.

local vehicle = game:GetService("ReplicatedStorage").Car
local cars = {}
game:GetService("ReplicatedStorage").SpawnCar.OnServerEvent:Connect(function()
	local clone = vehicle:Clone()
	clone.Parent = workspace.AICars
	clone.PrimaryPart:SetNetworkOwner(game:GetService("Players"):WaitForChild("x86_architecture"))
	cars[clone] = {Throttle = 0, Steer = 0, SteerValue = 0, CurrentClock = os.clock(), ThrottleControl = false, Car = clone}
end)

-- AI Config
local steerThreshold = 0
local frequencyMinThreshold = 0.5
local frequencyMaxThreshold = 1
local frequencyMinDist = 30
local throttleControlAmount = 1.5
local controlGainDist = 15
local followDist = 50
local maxOverSpeed = 20
local stopDist = 30
-- Chassis Config
local maxSteerAngle = 45
local acceleration = 50
local brakingAcceleration = 20
local brakingTorque = 3000
local torque = 5000
local turnSpeed = 5
local maxSpeed = 111
local wheelRad = 2.535 / 2
-- Config End
local lastSteer = 0

local function getAngleFromUnitVectors(a,b)
	local dot = a:Dot(b)
	return math.acos(dot)
end

local function angleBetweenVectors(u, v)
	return math.acos(u:Dot(v)/u.magnitude*v.magnitude)
end

local exp = function(x)
	return 2^x
end

local function update(dt, attFL, attFR, motorCyls, invertedCyls, car, seat)
	-- Steer:
	local steerGoal = seat.Steer
	--cars[car].SteerValue = cars[car].SteerValue + (steerGoal - cars[car].SteerValue) * math.min(dt * turnSpeed, 1)
	attFL.Orientation = Vector3.new(0, steerGoal, -90)
	attFR.Orientation = Vector3.new(0, steerGoal, -90)


	-- Throttle:
	local throttleGoal = -seat.Throttle * maxSpeed
	local torqueGoal = torque

	for _, cyl in pairs(motorCyls) do
		if invertedCyls[cyl] then
			if math.sign(throttleGoal) ~= 0 and math.sign(throttleGoal) ==  car.Chassis.Platform.Velocity:Dot(-car.Chassis.Platform.CFrame.LookVector) then
				cyl.AngularActuatorType = Enum.ActuatorType.Motor
				cyl.MotorMaxAngularAcceleration = brakingAcceleration
				cyl.MotorMaxTorque = brakingTorque
				cyl.AngularVelocity = -throttleGoal
			elseif math.sign(throttleGoal) == 0 then
				cyl.AngularActuatorType = Enum.ActuatorType.None
			else
				cyl.AngularActuatorType = Enum.ActuatorType.Motor
				cyl.MotorMaxAngularAcceleration = acceleration
				cyl.MotorMaxTorque = torqueGoal
				cyl.AngularVelocity = -throttleGoal
			end
		else
			if math.sign(throttleGoal) ~= 0 and math.sign(throttleGoal) ==  math.sign(car.Chassis.Platform.Velocity:Dot(-car.Chassis.Platform.CFrame.LookVector)) then
				cyl.AngularActuatorType = Enum.ActuatorType.Motor
				cyl.MotorMaxAngularAcceleration = brakingAcceleration
				cyl.MotorMaxTorque = brakingTorque
				cyl.AngularVelocity = throttleGoal
			elseif math.sign(throttleGoal) == 0 then
				cyl.AngularActuatorType = Enum.ActuatorType.None
			else
				cyl.AngularActuatorType = Enum.ActuatorType.Motor
				cyl.MotorMaxAngularAcceleration = acceleration
				cyl.MotorMaxTorque = torqueGoal
				cyl.AngularVelocity = throttleGoal
			end
		end
	end
end
print(os.clock())
game:GetService("RunService").Heartbeat:Connect(function(dt)
	for i, clone in pairs(cars) do
		coroutine.wrap(function()
			local car = clone.Car
			local motorCyls = {
				["cylBL"] = clone.Car.Chassis.Platform.CylindricalBL,
				["cylBR"] = clone.Car.Chassis.Platform.CylindricalBR,
				["cylFL"] = clone.Car.Chassis.Platform.CylindricalFL,
				["cylFR"] = clone.Car.Chassis.Platform.CylindricalFR,
			}
			local invertedCyls = {
				[clone.Car.Chassis.Platform.CylindricalFL] = true
			}
			local attFL = clone.Car.Chassis.Platform.AttachmentFL
			local attFR = clone.Car.Chassis.Platform.AttachmentFR
			local car = clone.Car
			local dot = -car.Chassis.Platform.CFrame.LookVector:Dot((workspace.Car.Chassis.TargetPart.Position - car.Chassis.Targetter.Position).Unit)
			local distance = (workspace.Car.Chassis.TargetPart.Position - car.Chassis.Targetter.Position).Magnitude
			local speed = car.Chassis.Platform.Velocity:Dot(-car.Chassis.Platform.CFrame.LookVector)
			local angle = math.deg(getAngleFromUnitVectors((car.Chassis.Targetter.Position - workspace.Car.Chassis.TargetPart.Position).Unit, car.Chassis.Platform.CFrame.LookVector.Unit)) * math.sign(-car.Chassis.Platform.CFrame.RightVector:Dot((car.Chassis.Targetter.Position - workspace.Car.Chassis.TargetPart.Position).Unit))
			local steer = math.clamp(angle, -maxSteerAngle, maxSteerAngle)
			local distanceCurve = math.min(math.abs(0 - distance) / 100, 1)-- * math.sqrt(math.abs(math.abs(steer) - 0) / maxSteerAngle)
			local steerCurve = math.abs( - math.abs(steer)) / maxSteerAngle
			steer = steer * steerCurve * distanceCurve
			if math.sign(steer) ~= math.sign(lastSteer) then
				local Time = os.clock() - clone.CurrentClock
				clone.CurrentClock = os.clock()
				if Time > frequencyMinThreshold and Time < frequencyMaxThreshold and distance < frequencyMinDist then
					--print(1)
					clone.ThrottleControl = true
				end
			end
			--print(steer)
			if math.abs(steer) < steerThreshold then
				steer = 0
			end
			local throttle = 0
			if distance > 0 and not clone.ThrottleClone then
				throttle = ((67 - math.abs(steer)) / 67)-- / clone.ThrottleControl
			else
				throttle = 0
			end
			local Time = os.clock() - clone.CurrentClock
			if Time > frequencyMaxThreshold or distance > controlGainDist and clone.ThrottleControl then
				--print(2)
				clone.ThrottleControl = false
			end
			update(dt, attFL, attFR, motorCyls, invertedCyls, car, {Throttle = throttle, Steer = steer, SteerValue = clone.SteerValue})
			lastSteer = math.sign(steer)
		end)()
	end
end)
2 Likes

First recommendation would be to drop out all the realistic car simulation stuff for the AI car. Trying to make it work with analog inputs that map to complex systems is needlessly raising the complexity. For example: instead of trying to work out what throttle it needs to resolve the proper torque at the current rpm/gear ratio to reach it’s target velocity as a difference from it’s current velocity, let it just directly mutate the velocity. You can then solve for an approximation of the car simulation stuff, like finding the current engine RPM to adjust engine sound pitch by tracing back thru the wheel angular velocity, then thru the gear train to the flywheel.

At first they might appear to lose their realism but you can increase the visual fidelity other hacky ways, like monitoring velocity against tire slip angle to fake a spin out when the difference gets too large, artificially delaying their response to changes made by the car they’re chasing, purposefully overcorrecting when swerving to avoid a collision/make a pit maneuver. Rocket League was a huge inspiration for this kind of approach.

No matter if you simplify the AI input methods or keep them realistic; you’re going to end up needing a tunable controller. Here’s my PID controller. There’s also a PI controller in that repo if you find cases where you want to drop the derivative term. Resources on how to use PI/PID controllers can be kinda complex, but I’ve linked a somewhat simple explanation at the top of the file. These controllers are all about eliminating oscillation, so hopefully they’ll help you out here regardless of what approach you decide to do.

4 Likes

I did some research on PID controllers, and I ended up writing my own PD controller (I assume PD controllers are commonly used for servo motors, and I’m pretty sure that steering is considered a servo). Here it is for those who want it

https://www.roblox.com/library/6812269277/PDController

Or the file PDController.lua (538 Bytes) .

Thank you so much for your help.

1 Like

You’re welcome; glad I could help. I really wouldn’t recommend using a PD controller however. The integral term acts as an accumulation of error, which you can kind of think of as it remembering previous deviations from the process variable.

P-D control is not commonly used because of the lack of the integral term. Without the integral term, the error in steady state operation is not minimized. P-D control is usually used in batch pH control loops, where error in steady state operation does not need to be minimized.

Above quote taken from this book

This video does an excellent job explaining PID controllers in the context of a car’s throttle pedal, shedding some light on why the integral term is important for your use case. In the context of steering, the system steady state is when the car needs to drive straight ahead. Omitting the integral is going to result in the steering oscillating back and forth instead of maintaining that straight line.

2 Likes