Traffic System
The traffic system is the heart of Traffic Overtaker's gameplay tension. It populates the curving road with a fixed pool of AI-driven cars that the player must read, queue behind, and slip past. Two cooperating components do all the work: TO_TrafficManager spawns, recycles, and queries the traffic pool ahead of the camera, and TO_TrafficCar drives each individual car along its lane with its own micro-AI (speed regulation, lane keeping, signals, crash reaction). A third component, TO_LaneManager, defines the two lanes the whole system is built on. This page explains how those pieces fit together, the exact fields you can tune, the all-important lane identity model, and how the player's own driver AI reads the nearest car ahead to drive its adaptive cruise. If you are reskinning the game with your own car art, this is the system you will swap prefabs into.
All three scripts live under Assets/TO/Scripts/Traffic/ (TO_TrafficManager.cs, TO_TrafficCar.cs) and Assets/TO/Scripts/Roads and Path/ (TO_LaneManager.cs). They carry the TO_ prefix and live in the BoneCrackerGames namespace, consistent with the rest of the project.
How It Works: Pooling and the Camera Treadmill
Traffic is never created and destroyed during a run. Instead, TO_TrafficManager instantiates the entire pool once at Start(), disables every car, parents them all under a runtime container GameObject named TO_TrafficContainer, and then continuously recycles cars from behind the camera to ahead of it. This pooling model keeps the system allocation-free during gameplay (important on mobile) and means the visible traffic density is fixed by your prefab amounts, not by spawn timers.
The lifecycle runs in three stages:
CreateTraffic()(called fromStart()) loops thetrafficCarsarray, instantiatesamountcopies of each prefab, adds theTO_TrafficCarcomponent reference to thespawnedTrafficCarslist, sets each instance inactive, parents it underTO_TrafficContainer, and pushes it 100 m behind on Z. It then schedulesPopulate()after a0.1fsecond delay.Populate()does the initial layout by callingReAlignTraffic(..., ignoreDistanceToRef: true)on every car, which is allowed to place cars closer thanminDistance(between 60 m andminDistance) so the road is already busy when the countdown ends.AnimateTraffic()runs everyUpdate()whileGameplayManager.gameStartedis true. For each car it compares the main camera's Z to the car's Z: if the camera has passed the car by more than 100 m (cameraZ > carZ + 100f), or the car is more thanmaxDistanceahead (cameraZ < carZ - maxDistance), the car is recycled forward viaReAlignTraffic(..., ignoreDistanceToRef: false).
ReAlignTraffic() is the recycle heart. It deactivates the car (so that re-enabling re-runs OnEnable and resets all state), picks a lane from TO_LaneManager.lanes via PickBalancedLane() (which counts active cars per side and steers the split toward oncomingLaneShare, default 0.5, rather than an independent coin flip so the two lanes stay balanced), chooses a random spawn distance, projects the spawn point onto the chosen lane with TO_Lane.FindClosestPointOnPath(), orients the car (forward = lane.leftSide ? -dir : dir), applies the vertical spawnHeight offset, reactivates it, then applies the overtake speed bias and a clipping check. If the newly placed car overlaps an existing active car (CheckIfClipping() via TO_BoundsExtension.ContainBounds), it is simply disabled and will be retried on a later frame.
Lane Identity: The Forward Lane vs. the Oncoming Lane
Everything dangerous about Traffic Overtaker comes down to which lane a car belongs to. The road is a two-lane layout defined by TO_LaneManager, and lane membership — not a position threshold — is what decides whether a car is a harmless obstacle or a lethal head-on threat.
TO_LaneManager.Reset() seeds the canonical two-lane layout used by every shipping gameplay scene. Each lane is a child TO_Lane with a leftSide flag:
| Lane GameObject | Local X offset | leftSide |
Role in gameplay |
|---|---|---|---|
Lane_Left |
-1.61 m |
true |
Oncoming traffic — drives against your travel direction. This is the danger lane you pull into to overtake. |
Lane_Right |
+1.61 m |
false |
Your forward travel lane — same-direction traffic you catch up to and queue behind. |
The crucial design rule, stated directly in the TO_LaneManager.Reset() comments, is that consumers select lanes by the leftSide flag, not by array index. TO_TrafficManager, TO_TrafficCar, and the player's AI all branch on leftSide / oppositeDirection, so the order of entries in the lanes array is purely cosmetic. When a car is recycled, ReAlignTraffic() reads the chosen lane's leftSide to set the car's facing, and TO_TrafficCar.OnEnable() then derives its own oppositeDirection from that facing: oppositeDirection = Vector3.Dot(transform.forward, Vector3.forward) < 0. A left-lane car ends up facing backward relative to your travel, so oppositeDirection becomes true and the car is marked oncoming.
TO_LaneManager is decorated with [DefaultExecutionOrder(-10)] so its lanes initialize before the traffic and player systems read them. It also subscribes to TO_CurvedRoadManager.OnAllRoadsAligned / OnRoadAligned to build and refresh each lane's waypoints whenever the road treadmill re-aligns. To re-collect lanes after editing the hierarchy, use the Get All Lanes context-menu entry on the component (GetLanes()), which finds every child TO_Lane. For more on the road and waypoint pipeline these lanes ride on, see 07_CurvedRoads.md.
The Overtake Speed Bias
A one-button overtaking game only works if you constantly catch up to slower cars in your own lane while oncoming cars stay fast and threatening. TO_TrafficManager enforces this asymmetry at spawn time. In ReAlignTraffic(), after a car is placed, same-direction (right-lane, leftSide == false) cars have their maximumSpeed multiplied by forwardTrafficSpeedScale (default 0.6), turning them into slow obstacles you must pass, while oncoming (left-lane) cars keep their full lane speed — so a head-on closes at the sum of both speeds.
On top of that lane asymmetry, both lanes are additionally scaled by a player-relative factor, playerSpeedScale = effectivePlayerCruise / nominalPlayerSpeed, so a slower player car faces proportionally slower traffic and the catch-up/overtake feel stays constant across the vehicle roster. playerSpeedScale is 1.0 until the player spawns, then refreshes on OnPlayerSpawned. forwardTrafficSpeedScale is the main knob for difficulty pacing: lower it and forward traffic crawls, making cars easy to catch but trivial to plan around; raise it toward 1.0 and forward traffic nearly matches your cruise, shrinking the windows in which queuing and overtaking happen.
TO_TrafficManager Configurable Fields
These fields appear on the TO_TrafficManager component in the gameplay scene's hierarchy (the TO_TrafficManager root object). All values below are the in-code defaults.
| Field | Type | Default | Purpose |
|---|---|---|---|
trafficCars |
TrafficCars[] |
— | The spawn list. Each entry pairs a trafficCar prefab with an amount (how many copies to pool). This is the array you edit to add or replace traffic vehicles. |
minDistance |
float |
300 |
Minimum distance ahead of the camera at which a car may be recycled. |
maxDistance |
float |
600 |
Maximum recycle distance ahead; also the cull threshold — a car more than this far ahead is recycled. |
forwardTrafficSpeedScale |
float (Range 0.1–1) |
0.6 |
Speed multiplier applied to same-direction traffic on spawn so the player catches up. Oncoming cars keep their full lane speed. |
nominalPlayerSpeed |
float (Range 60–400) |
240 |
Player cruise speed (km/h) the authored traffic speeds were balanced against. All traffic scales by playerCruise / nominalPlayerSpeed; 240 reproduces the original tuning for a top-tier car. |
oncomingLaneShare |
float (Range 0–1) |
0.5 |
Target share of active traffic in the oncoming (left) lane. 0.5 = even split; lower = more forward obstacles to overtake, higher = more oncoming danger. Used by PickBalancedLane(). |
leadQueryLateralGate |
float |
2 |
Lateral half-width (m) for the lead-car query's lane gate. Must be smaller than the ~3.2 m lane spacing so the adjacent lane (the car being passed) is excluded. |
spawnedTrafficCars |
List<TO_TrafficCar> |
runtime | The live pool. Populated at Start(); read by every query and by TO_TrafficCar.DetectVehicleAhead(). |
The nested TrafficCars class is intentionally tiny — just trafficCar (the TO_TrafficCar prefab) and amount (int, default 1). The per-scene TO_TrafficManager.trafficCars array described here is the sole spawn source the manager iterates, so it is the one to edit when changing what spawns in a given scene (there is no traffic-prefab list on TO_Settings).
TO_TrafficCar: The Per-Car AI
Every traffic car carries its own lightweight AI in TO_TrafficCar. It does not use RCCP physics — traffic cars are kinematic-feeling Rigidbody movers steered along the lane waypoints. The component [RequireComponent(typeof(Rigidbody))] and configures that Rigidbody (mass 1500, interpolation on, frozen X/Z rotation and Y position) so cars glide along the road without tipping.
Per-car configurable fields
| Field | Type | Default | Purpose |
|---|---|---|---|
spawnHeight |
float |
0 |
Vertical offset added when the car is spawned/re-aligned. |
currentLane |
TO_Lane |
— | The lane this car is currently following (assigned by the manager on recycle). |
oppositeDirection |
bool |
false |
true when the car drives oncoming. Recomputed in OnEnable() from facing. |
maximumSpeed |
float |
10 |
Top speed (km/h). Randomized to 85–100% of the authored value in OnEnable(). |
wheelModels |
Transform[] |
— | Wheel meshes spun visually based on speed. |
collisionLayer |
LayerMask |
1 |
Layers that trigger a crash in OnCollisionEnter. |
engineSound |
AudioClip |
— | Engine loop; an AudioSource is created in Awake() and pitch-modulated by speed. |
headLights |
Light[] |
— | Headlight lights (auto-on at night via GameplayManager.dayOrNight). |
brakeLights |
Light[] |
— | Brake lights (on when braking). |
signalLights |
Light[] |
— | Turn-signal / hazard lights, flashed on a 0.5 s cycle. |
yieldSpeedFactor |
float (Range 0–1) |
0.3 |
Speed (as a fraction of maximumSpeed) this car cuts to while yielding to a returning player. |
yieldRightShift |
float (Range 0–3) |
1.2 |
Extra rightward (road-edge) offset (m) the car eases to while yielding, giving the player room to slot back in. |
yieldHoldTime |
float (Min 0) |
0.75 |
Seconds a yield request persists after the player's AI stops requesting it before auto-clearing. |
stopDistance |
float (Min 0) |
15 |
Gap (m) to the car ahead at/below which this car slows to a full stop. |
clearDistance |
float (Min 0) |
50 |
Gap (m) to the car ahead at/above which this car ignores it and runs at full speed. |
brakeDistance |
float (Min 0) |
40 |
Gap (m) to the car ahead below which the brake lights come on. |
laneDetectionGate |
float (Min 0) |
3 |
Lateral half-width (m) DetectVehicleAhead() uses to treat another car as being in this car's lane. |
crashImpulseThreshold |
float (Min 0) |
1000 |
Collision impulse magnitude above which this car is marked crashed. |
Several runtime fields are hidden with [HideInInspector] because the system manages them: triggerCollider (the front detection box), crashed (crash state), and yieldingToPlayer (set each frame by the player AI's RequestYield() while it eases back into the right lane). The component also exposes a read-only CurrentSpeed property in km/h, derived from Rigid.linearVelocity.magnitude * 3.6f — this is the value the player AI reads as the lead car's speed.
What each car does every frame
OnEnable()resets velocity and constraints, clearscrashed, randomizesmaximumSpeedtoRandom.Range(_maximumSpeed * 0.85f, _maximumSpeed), recomputesoppositeDirection, recaptures the closest path point, and turns headlights on if the scene is set to night.Navigation()(inFixedUpdate) aims the car's heading at a speed-scaled look-ahead goal point on its lane (lane-frame pure-pursuit viaTO_LaneManager.FindPointAheadOnLane()), setsdesiredSpeed = Mathf.InverseLerp(stopDistance, clearDistance, distance) * maximumSpeedso the car eases off when something is close ahead, flipsbrakingOnwhendistance < brakeDistance, and lerps the Rigidbody velocity towardtransform.forward * (desiredSpeed / 3.6f). There is no lane-changing: on the 2-lane road each car holds its assigned lane.DetectVehicleAhead()runs on a0.4fsecond cadence. It scansspawnedTrafficCarsfor the nearest same-direction car ahead within thelaneDetectionGate(3 m) lateral band and stores the Z-gap in the privatedistancefield thatNavigation()reads. This is the cars' own car-following — separate from the player AI's lead query below.RequestYield()is called each frame byTO_OvertakeAIon the car the player is returning alongside: the car cuts toyieldSpeedFactorof its top speed and easesyieldRightShiftmetres toward the road edge foryieldHoldTimeseconds so the player can tuck back in without a side-swipe.OnCollisionEnter()marks the carcrashedwhen it is hit oncollisionLayerwith impulse magnitude overcrashImpulseThreshold(1000), past a0.5 sspawn-protection window; a crashed car coasts to a stop and flips on hazard signals.
In Awake(), each car re-layers all of its child renderers to the physics layer named by TO_Settings.Instance.trafficCarsLayer (default TO_TrafficCar), skipping anything on Ignore Raycast. The front detection volume (TO_TriggerVolume) is created on the Ignore Raycast layer so it never collides. This layering is what lets the player's collision and forward-raycast logic in TO_Player recognize a contact as a traffic car versus scenery — see 06_PlayerVehicles.md.
How the Player AI Reads the Nearest Car Ahead
The player's driver AI, TO_OvertakeAI, never touches individual traffic cars directly. Each FixedUpdate it calls one query on the manager:
TO_TrafficManager tm = TO_TrafficManager.Instance;
if (tm != null)
hasLead = tm.GetNearestCarAhead(P, T, out lead, out gap, out leadKmh);
Here P is the player's world position and T is the player's along-lane travel direction. The method signature is:
public bool GetNearestCarAhead(Vector3 fromPos, Vector3 alongDir,
out TO_TrafficCar car, out float distance, out float speed)
GetNearestCarAhead() walks spawnedTrafficCars and, for each candidate, skips cars that are null, inactive (which includes the whole pool when TO_TrafficContainer is disabled), or crashed. It then skips any car whose oppositeDirection is true — oncoming cars are never a brake or lead target, because the player owns the head-on risk. For the survivors it projects the offset onto the travel direction (Vector3.Dot) to get the signed along-lane gap, rejects anything not strictly ahead, and rejects anything whose perpendicular distance (Vector3.Cross(dir, toCar).magnitude) exceeds leadQueryLateralGate. The nearest remaining car wins, and its CurrentSpeed is returned as speed. The AI feeds distance and speed into its adaptive-cruise (ACC) gap controller so it slows and sits behind the lead car without rear-ending it — the "stuck behind a slow car" state that motivates pressing Overtake.
Because the lateral gate is tighter than the lane spacing, once the player swings into the oncoming lane to pass, the car being overtaken falls outside the gate and stops registering as a lead — so the AI accelerates past instead of braking for it.
A mirror query, GetNearestOncomingAhead(fromPos, alongDir, lateralGate, out car, out distance, out speed), returns the nearest oncoming car using the inverse filter. It is consumed only by the HUD's oncoming-threat warning (which estimates time-to-collision from the closing speed) and is never a control input — the AI will not brake or steer for it.
Adding or Replacing Traffic Car Prefabs
To change which vehicles populate traffic in a scene, edit the trafficCars array on the TO_TrafficManager component in that gameplay scene. Each element is a TrafficCars entry with a trafficCar prefab slot and an amount. A practical workflow:
- Build or import your car prefab and add a
TO_TrafficCarcomponent (aRigidbodyis auto-required; itsReset()applies the standard mass/damping/constraints). Wire upwheelModels,headLights,brakeLights, andsignalLightsif you want the visual extras. - Optionally set the prefab's
maximumSpeed(km/h) andengineSound. Remember the manager randomizes speed down to 85% and scales forward-lane cars byforwardTrafficSpeedScale. - In the gameplay scene, select
TO_TrafficManager, add an array element, assign your prefab totrafficCar, and setamountto how many copies should exist in the pool. Higher totals mean denser traffic and more GameObjects. - Make sure the project has a physics layer named exactly
TO_TrafficCar(the value ofTO_Settings.trafficCarsLayer); the car re-layers its children to it at runtime so the player's detection works.
You do not need to position traffic cars in the scene — they are pooled, disabled, and teleported by the manager. The Highlight Traffic Cars Folder menu item under Tools > BoneCracker Games > Traffic Overtaker jumps you to the bundled traffic prefabs to use as templates. What actually spawns in a scene is the trafficCars array on that scene's TO_TrafficManager component — the array the spawn loop iterates.
See Also
- 07_CurvedRoads.md — the road treadmill,
TO_PathManager, and the waypoints that lanes are built from. - 06_PlayerVehicles.md — the player car,
TO_Player, and how it detects traffic collisions via theTO_TrafficCarlayer. - 04_SystemArchitecture.md — how the managers, event bus, and singletons fit together.
- 14_APIReference.md — full signatures for
GetNearestCarAhead,GetNearestOncomingAhead, and the traffic API.