Skip to content

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:

  1. Reads the cached CrimeRecord from CrimeRecordCache (replicated from server)
  2. Selects the pursuit profile from pursuitAggression config based on the player's crime score
  3. Sets Entity.Target — the player's current position when identified, or the last-known position when not
  4. Evaluates all states in priority order and transitions if canEnter returns true
  5. Calls currentState.tick(Entity) to move/steer
  6. 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.