PID control: the workhorse of feedback
Keeping a car centred in its lane is a classic feedback control problem: measure an error (how far off-centre the car is), and continuously correct the steering to drive that error toward zero. A PID controller — proportional, integral, derivative — computes the steering correction as a weighted sum of three terms, each reacting to a different aspect of the error signal:
error = lane_center_offset
steer = Kp * error // P: react to current error
+ Ki * integral(error dt) // I: cancel steady-state bias
+ Kd * d(error)/dt // D: damp oscillation, react to trend
The P term alone would work if the car had zero mass and infinite responsiveness, but real steering has inertia and delay: a pure-P controller left alone tends to either respond too weakly (understeer, drifting off-centre on curves) or oscillate if pushed too hard. The D term anticipates where the error is heading and brakes the correction before it overshoots, which is what makes a well-tuned PID loop feel smooth rather than twitchy. The I term matters less for lane-keeping specifically (there's rarely a steady-state bias to cancel) but is essential in the general PID toolbox for problems like cruise control on a hill, where a P-only controller settles into a permanent speed error.
Raycasting for lane detection
To compute the lane-centre error in the first place, the car casts a fan of rays outward from itself and measures where each one intersects the road boundary or lane markings — the same technique used for line-of-sight and sensor simulation in countless 2D games. A short ray straight ahead and a pair of angled rays to the left and right are usually enough: comparing the left and right boundary distances gives the lateral offset directly, and comparing the angle of the nearest boundary segment to the car's heading gives the heading error the D term needs.
Time-to-collision: when to brake or change lane
Lane-keeping alone doesn't stop the car from hitting something in its own lane, which is why a separate reactive layer runs in parallel, watching a forward raycast for obstacles and computing time-to-collision rather than raw distance:
TTC = distance_to_obstacle / closing_speed if TTC < braking_threshold: apply brakes proportional to 1/TTC if TTC < lane_change_threshold and a gap exists: steer to change lane
TTC is the right quantity rather than raw distance because it folds in relative speed: a car 40 metres ahead but closing fast is far more urgent than a stationary obstacle 15 metres away that the car is barely approaching, and braking or lane-change decisions triggered on distance alone would either brake far too early on a slow-closing gap or far too late on a fast one. Real advanced driver-assistance systems (forward collision warning, automatic emergency braking) use TTC as their primary trigger for exactly this reason.
Why two layers instead of one
Lane-keeping and collision avoidance are kept as separate controllers rather than merged into one, because they operate on different timescales and different failure modes: PID lane-keeping is a smooth, continuous, low-priority correction, while a raycast detecting an imminent collision needs to be able to override it immediately and forcefully. Layering — a calm baseline controller with a reactive layer that can interrupt it under threat — is the same architecture used in real autonomous-driving stacks, where a planning layer sets a smooth intended trajectory and a much faster reactive safety layer is allowed to violate that plan the instant time-to-collision drops below a hard threshold.
Frequently asked questions
What do the P, I and D terms in PID control actually do?
P reacts proportionally to the current lane-offset error — the main corrective force. D reacts to how fast the error is changing, which anticipates and damps overshoot so the car doesn't oscillate across the lane. I accumulates past error over time to cancel any persistent steady-state bias, though for lane-keeping specifically P and D usually do most of the work.
What is time-to-collision and why is it used instead of raw distance?
TTC = distance divided by closing speed. It captures urgency the way raw distance cannot: an obstacle far away but closing fast can demand braking sooner than a near but slow-closing one. Real forward-collision-warning and automatic-emergency-braking systems use TTC as their primary trigger for exactly this reason.
Why combine PID lane-keeping with a separate reactive avoidance layer?
Because the two problems operate on different timescales: lane-keeping is a smooth, continuous, low-urgency correction, while collision avoidance needs the ability to override that smooth control instantly and forcefully when time-to-collision drops below a safe threshold. Layering a calm baseline controller under a fast reactive safety layer mirrors the architecture of real autonomous-driving stacks.
Try it live
Everything above runs in your browser — open Self-Driving Car and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open Self-Driving Car simulation