Police Cars¶
Client-side AI for police pursuit vehicles. Lives in src/client/controllers/AI/.
Why Client-Side?¶
Roblox vehicle physics require the client that has network ownership of the car's PrimaryPart to drive it. DispatchService on the server transfers network ownership of a police vehicle to the pursuing player's client via AssignCopCar. That client's PoliceCarController then runs NPVCore each frame to steer toward the player.
PoliceCarController¶
Knit controller that listens for models tagged PoliceNPVs in the workspace.PoliceNPVs folder using the Component system.
Each entity gets per-entity state:
| Field | Purpose |
|---|---|
MaxSpeed |
Top speed cap (default 150) |
Speed |
Current throttle target |
PIDController |
Steering PID with output clamped to ±45° |
PursuitOvertake |
Random -1/0/1 offset so cars don't all line up |
StuckTick / AbsoluteStuckTick |
Timestamps for the stuck-detection logic |
EventData |
Flags for special maneuver events (e.g. safe distance) |
A car only runs NPVCore when its PrimaryPart attribute Controller matches Players.LocalPlayer.Name — only the owning client drives it. The update rate is throttled to ~50–100 ms per entity (staggered by id).
The controller also handles ramp physics: if the front bumper loses ground contact and the car has enough velocity, a temporary BodyForce and BodyGyro launch it over the ramp cleanly.
Collision Groups¶
While a car is assigned to a local client, all its parts move to collision group LocalPoliceCar so the player's own car clips through it. When unassigned they return to PoliceCar.
NPVCore — The Driving Logic¶
NPVCore.lua is the per-tick function called by PoliceCarController. It:
- Reads the cached
CrimeRecordfromCrimeRecordCache(replicated from server) - Selects the pursuit profile from
pursuitAggressionconfig based on the player's crime score - Sets
Entity.Target— the player's current position when identified, or the last-known position when not - Evaluates all states in priority order and transitions if
canEnterreturns true - Calls
currentState.tick(Entity)to move/steer - Runs all Setters in order to apply the resulting angle and speed to the car
Car States¶
| State | canEnter condition |
Behaviour |
|---|---|---|
Stuck |
Velocity very low for too long | Reverses away from obstacle |
Stop |
No crime record or car isn't being driven | Idles |
Search |
Target lost (not identified) | Drives to last-known position |
DriveDirectly |
Close to target and path not needed | Steers straight at target using PID |
DrivePath |
Default — has a target and needs to route around obstacles | Follows a computed node-graph path |
States are evaluated top-to-bottom; the first whose canEnter is true wins.
Setters (applied every tick after the state)¶
Setters run after the active state and layer on additional steering/speed corrections:
| Setter | What it does |
|---|---|
ReverseIfBack |
If the car is facing away from the target, briefly reverses |
SlowDownForCops |
Reduces speed when passing close to other police cars to avoid pile-ups |
SetAngle |
Applies Entity.Angle to the car's steering attribute |
SetSpeed |
Applies Entity.Speed to the car's throttle attribute |
Pursuit Profile¶
pursuitAggression config maps crime score to a profile object. NPVCore reads profile.stars, profile.pathPerformance, etc. to scale how aggressively the car drives. Higher wanted levels result in faster cars with tighter paths.
Lead Targeting¶
When the player is identified, NPVCore adds a lead offset to the target position:
leadVelocity = player.PrimaryPart.AssemblyLinearVelocity * 0.4
Entity.AngleTo = actualTarget + leadVelocity
This makes the car aim slightly ahead of the player rather than directly at their current position, compensating for travel time at speed. When the player is unidentified, the car drives to the raw last-known position with no lead.