Zebra Crossing System
Overview
The Zebra Crossing system makes NPC vehicles automatically yield to pedestrians and cyclists at crosswalks. It consists of two components:
ZebraCrossingArea— attached to an individual crosswalk trigger. Detects occupants and exposes a static query API for NPC vehicles.ZebraCrossingConfig— optional parent component. Defines a shared whitelist of occupant types for all childZebraCrossingAreaobjects.
How It Works
Full Interaction Pipeline
sequenceDiagram
participant P as Pedestrian / Cyclist
participant ZA as ZebraCrossingArea (Trigger)
participant CS as NPCVehicleCognitionStep
participant ST as NPCVehicleInternalState
participant DS as NPCVehicleDecisionStep
P->>ZA: OnTriggerEnter
ZA->>ZA: Add root to occupantRoots (if whitelisted)
Note over CS: Every FixedUpdate — after ground/obstacle jobs complete
CS->>ZA: TryGetBlockedStop(frontCenterPos, forward)
ZA-->>CS: blocked=true, distance=12.3m, area="CrosswalkNorth"
CS->>ST: IsZebraBlocked = true
CS->>ST: DistanceToZebraStop = 12.3
CS->>ST: ZebraAreaName = "CrosswalkNorth"
CS->>ST: WasZebraBlocked = true
Note over DS: Same FixedUpdate — after Cognition
DS->>ST: Read IsZebraBlocked, DistanceToZebraStop
DS->>DS: distanceToStop = Min(frontVehicle, trafficLight, rightOfWay, zebraStop)
DS->>ST: SpeedMode = STOP (or SUDDEN_STOP / ABSOLUTE_STOP)
P->>ZA: OnTriggerExit
ZA->>ZA: Remove root from occupantRoots
CS->>ZA: TryGetBlockedStop → blocked=false
CS->>ST: IsZebraBlocked = false
CS->>ST: WasZebraBlocked = false (log "cleared" if debug enabled)
DS->>ST: SpeedMode resumes NORMAL
Where the Code Lives
| Code location | What it does |
|---|---|
ZebraCrossingArea.OnTriggerEnter/Exit |
Maintains the set of current occupants |
NPCVehicleCognitionStep.UpdateZebraCrossingAwareness() |
Queries every active area for every NPC vehicle each FixedUpdate |
NPCVehicleInternalState fields |
Stores the result between Cognition and Decision steps |
NPCVehicleDecisionStep.UpdateSpeedMode() |
Includes DistanceToZebraStop in the minimum stop distance calculation |
NPC Vehicle State Fields
These fields are written by the Cognition step and read by the Decision step each frame:
| Field | Type | Description |
|---|---|---|
IsZebraBlocked |
bool |
True when a blocked crossing is detected ahead |
DistanceToZebraStop |
float |
Distance to the stop point (metres), float.MaxValue if not blocked |
ZebraAreaName |
string |
Name of the matched ZebraCrossingArea (for debug logging) |
WasZebraBlocked |
bool |
Previous frame's value — used to detect the cleared transition |
How the Stop Distance Is Used
In NPCVehicleDecisionStep, the zebra stop distance competes with all other stop conditions:
var distanceToStopPointByZebra = state.IsZebraBlocked
? state.DistanceToZebraStop
: float.MaxValue;
var distanceToStopPoint = Mathf.Min(
distanceToStopPointByFrontVehicle,
distanceToStopPointByTrafficLight,
distanceToStopPointByRightOfWay,
distanceToStopPointByZebra // ← zebra crossing
);
The resulting distanceToStopPoint then determines the speed mode:
| Condition | Speed Mode |
|---|---|
distance ≤ absoluteStopDistance |
ABSOLUTE_STOP |
distance ≤ suddenStopDistance |
SUDDEN_STOP |
distance ≤ stopDistance |
STOP |
distance ≤ slowDownDistance |
SLOW |
| otherwise | NORMAL |
Where the distances depend on current speed and the deceleration values in NPCVehicleConfig:
So a faster NPC vehicle needs more distance to stop and may enter SUDDEN_STOP or ABSOLUTE_STOP if the crossing is detected late.
ZebraCrossingArea
Setup
- Create an empty GameObject positioned over the crosswalk.
- Add a Collider component and enable Is Trigger.
- Scale the collider to cover the full crossing surface.
- Attach the ZebraCrossingArea script.
- Assign a Stop Point transform — place this just before the stop line where vehicles should halt.
Occupant Detection
The area automatically identifies the following types:
| Type | Detection method |
|---|---|
NPCPedestrian |
GetComponentInParent<NPCPedestrian>() |
SimpleCyclistMovement |
Component name search in parent hierarchy |
BicycleMoveRotate |
Component name search in parent hierarchy |
ClearWaypointFollower |
Component name search in parent hierarchy |
WaypointFollower |
Component name search in parent hierarchy |
The occupant's root transform is stored (not the collider's transform), so compound colliders on a single character count as one occupant.
Whitelist Filtering
If a ZebraCrossingConfig parent is present with non-empty lists, only occupants matching the whitelist are counted:
allowedPrefabs— matched by normalized name (removes(Clone)suffix).allowedRootNameSubstrings— matched by case-insensitive substring of the root name.
If both lists are empty, all detected types are allowed.
Inspector Fields
| Field | Default | Description |
|---|---|---|
stopPoint |
This object | Transform where vehicles should stop. Defaults to the area's own position if unset. |
maxDetectionDistance |
40 m | How far ahead a vehicle will look for this crossing |
forwardAngleTolerance |
80° | Angular cone (half-angle) in front of vehicle for detection |
stopBuffer |
1.5 m | Extra gap kept between the vehicle and the stop point |
enableDebugLogs |
false | Log occupancy changes and stop decisions to the Console |
Static Query API
NPC vehicles call this once per FixedUpdate during the decision step:
bool blocked = ZebraCrossingArea.TryGetBlockedStop(
vehiclePosition, // vehicle's world position
vehicleForward, // vehicle's forward direction (Y is ignored)
out float distance, // distance to the stop point (minus buffer)
out ZebraCrossingArea area // the matched area, or null
);
The method checks all active, occupied ZebraCrossingArea instances and returns the closest one that:
- Is within
maxDetectionDistance - Falls within the
forwardAngleTolerancecone in front of the vehicle
If blocked == true, the NPC sets its speed mode to STOP and uses distance as the stopping distance.
Gizmo Visualization
When the GameObject is selected in the Scene view:
- Green wireframe — crossing is clear (no occupants)
- Red wireframe — crossing is occupied
- Cyan sphere — stop point position
Enable enableDebugLogs to see occupant enter/exit events in the Console, plus two extra NPC-level log messages per crossing event:
[NPC Zebra] Vehicle 'Bus_01' stopping for zebra 'CrosswalkNorth' at ~12.3m.
[NPC Zebra] Vehicle 'Bus_01' cleared zebra 'CrosswalkNorth'.
The first message fires the first frame an NPC detects the blocking state (WasZebraBlocked transition false → true). The second fires when the crossing clears (true → false).
ZebraCrossingConfig
Purpose
Place this component on a parent GameObject that groups multiple ZebraCrossingArea children. All child areas inherit its whitelist settings.
ZebraCrossingConfig (parent)
├── ZebraCrossingArea (north crosswalk)
├── ZebraCrossingArea (south crosswalk)
└── ZebraCrossingArea (east crosswalk)
Fields
| Field | Description |
|---|---|
allowedPrefabs |
Only occupants whose root name matches one of these prefab names are counted |
allowedRootNameSubstrings |
Only occupants whose root name contains one of these strings (case-insensitive) are counted |
Examples
allowedRootNameSubstrings: ["Human", "Cyclist", "Pedestrian"]
This ensures that vehicles that enter the trigger zone accidentally (e.g., due to overlapping colliders) are not counted as occupants.
Complete Setup Example
Intersection (GameObject)
└── ZebraCrossingConfig
├── allowedRootNameSubstrings: ["Human", "Bicycle"]
│
├── CrosswalkNorth (GameObject)
│ ├── BoxCollider [Is Trigger = true]
│ ├── ZebraCrossingArea
│ │ └── stopPoint → StopLineNorth (Transform)
│ └── StopLineNorth (child Transform)
│
└── CrosswalkEast (GameObject)
├── BoxCollider [Is Trigger = true]
├── ZebraCrossingArea
│ └── stopPoint → StopLineEast (Transform)
└── StopLineEast (child Transform)
Collider Must Be a Trigger
The ZebraCrossingArea script calls Reset() automatically to set isTrigger = true when first added. If you replace the collider later, make sure to re-enable the trigger flag.
Placement
Place the collider so it covers the full painted area of the crosswalk, not just the curb edge. Pedestrians need to be inside the zone for the system to work correctly.