Vehicle AI Series: The CarController
Now that I’ve introduced the key components behind my vehicle AI system: the CarController, the AIDriver, and the Context. I want to start diving deeper into each part.
In this post, we’re going to look at the CarController, which is basically the “hands and feet” of the car.
No matter who is driving, the player or the AI, the CarController is responsible for applying steering, acceleration, and braking to the vehicle.
And at the center of that… is one really important function
set_inputs(acceleration, steering_angle)
This is the interface that both the Player Controller and the AIDriver will use to tell the car what to do.
What the CarController Actually Does
The CarController is tied to a VehicleBody3D, which handles the physics simulation; friction, suspension, wheel grip, torque, and so on.
But the CarController decides how the forces are applied.
It contains logic for:
- Engine force
- Braking force
- Steering angle
- Simple gear logic
- Engine braking
- Debug prints (very useful early on)
It’s not trying to be overly realistic. The goal here is to build a clean, predictable layer for applying movement forces, something both humans and AI can use.
The All Important set_inputs() Function
The whole reason this method is important is because it gives the AI a simple, clean, and controlled way of driving the car.
Here’s how the method looks like:
func set_inputs(acceleration: float, steering_angle: float) -> void:
_handle_gear_selection(acceleration)
_apply_engine_force(acceleration)
_apply_steering(steering_angle)
_apply_braking(acceleration)
if debug:
_update_debug_info()
When the AI wants the car to accelerate forward, it simply calls:
controller.set_inputs(1.0, 0.0)
When it wants to steer left:
controller.set_inputs(0.5, -0.3)
When it wants to slow down:
controller.set_inputs(-1.0, 0.0)
No extra complexity.
The AIDriver doesn’t care about engine torque curves, RPM, steering dampening, or brake fade.
It just outputs acceleration and steering, and the CarController handles the rest.
What Happens When set_inputs()Runs
Gear Selection
This is a simple rule:
If the acceleration is negative (meaning reverse input) and the car is moving slowly, switch to reverse gear.
If not, stay in forward gear.
Great for AI because it doesn’t need to think about the gearbox logic.
Engine Force Application
This scales the acceleration input with the max engine force.
The CarController smooths this using lerps, so the power doesn’t instantly spike.
Realistic enough, but still predictable.
Steering
The steering input gets scaled using max_steering_angle and smoothed using steering_response.
This means the AI only has to choose a steering direction, the CarController makes sure the car isn’t twitchy.
Braking
A negative acceleration input makes the car brake, and positive input releases the brake.
This is especially useful for AI, because braking becomes part of the same input range instead of a separate command.
Why This Matters for AI
The beauty of this setup is that the AI doesn’t need to understand anything complicated.
It only needs to answer two questions:
- How fast should I be going? → Gives
acceleration - Where do I need to turn? → Gives
steering_angle
That’s it.
Everything else is handled below this layer.
This makes the AIDriver easier to build and test later on because you’re not wrestling with physics every time you tweak the AI’s logic, you’re just playing with two parameters.
What’s Next
With the CarController in place, the next step is to explore the AIDriver the part that actually “thinks”.
In the next post, I’ll cover:
- How the AI decides when to accelerate
- How it determines the correct steering angle
- How it uses the Context data (like path points, obstacles, and player position)
- And how this all plugs into
set_inputs()
This is where things get fun because now we start building real behaviour.