CHAPPiE

Competition drift judging for CarX Drift Racing Online. Depth-graded line scoring against zones you author in your own map, host-controlled qualifying, live lobby scoreboard and CSV results.

Line Angle Consistency X-Factor

Download Get started Build a track View on GitHub

Naming contract

This is the entire interface between a track and CHAPPiE. The mod scans the loaded scene at load time and after every scene change, matches GameObject names against the patterns below, and ignores everything else.

No component needs to be present at runtime. Custom MonoBehaviours are stripped from the map’s AssetBundle when CarX loads it, which is why the contract is names rather than components. The SDK components exist to write these names for you and keep them consistent.

Names are matched case-sensitively for zone meshes and case-insensitively for special lines.


Zone meshes

The core of line scoring. Every scored zone is a grid of separate meshes.

Pattern Detection What counts as a hit
zone_{ZoneID}_{Length}_{Depth} Bumper Any point of the car’s body footprint inside the mesh bounds
wheel_{ZoneID}_{Length}_{Depth} Wheel Any of the 4 wheel positions inside the bounds
clip_{ZoneID}_{Length}_{Depth} Inner clip Front-bumper points (front-left, front-centre, front-right)
overshoot_{ZoneID}_{Length} Penalty Body footprint — no depth index, see below

All three indices are integers.

Detection uses the mesh’s renderer bounds, so a collider is not required. For Bumper segments on convex shapes you can add a convex MeshCollider with isTrigger = true for tighter body detection, but it’s optional.

Overshoot meshes

overshoot_{ZoneID}_{Length} carries no depth index — it is “past the deepest band”. Place it immediately beyond your highest-depth mesh at that length position. Touching it halves the depth ratio for that position even if max depth was reached, and fires an Overshoot deduction.

This is what stops “deeper is always better” from rewarding a driver who ran out of road.

Worked example

A three-length, three-depth zone with an overshoot strip:

zone_1_1_1   zone_1_1_2   zone_1_1_3   overshoot_1_1     ← entry
zone_1_2_1   zone_1_2_2   zone_1_2_3   overshoot_1_2     ← mid
zone_1_3_1   zone_1_3_2   zone_1_3_3   overshoot_1_3     ← exit
      ↑            ↑            ↑            ↑
  shallow                     deep      too deep

Max depth is detected automatically per zone — there is no config file listing it. If the deepest mesh you author for zone 1 is _3, then depth 3 is 100% for that zone.

Depth and length numbering must be contiguous from 1. A gap (say depth 1 and 3 with no 2) makes the missing band unreachable and skews the ratio.


Per-zone angle range

zone_angle_{ZoneID}_{Min}_{Max}

Overrides the angle window for one zone. Values may be integers or floats — zone_angle_2_35_55 and zone_angle_2_35.5_55.0 both parse.

Drop below Min inside that zone for UnderAngleSustainSec and you take an under-angle deduction. Put this on its own GameObject — never on a zone mesh, because both components rename their GameObject and would fight.

Parsed before the zone_ check, so the zone_angle_ prefix is not mistaken for a bumper segment.


Special lines

Matched case-insensitively. Exact names except the brake zones, which match on prefix.

Name Behaviour
run_start_line Car stationary with all 4 wheels inside → 5 s countdown → run starts
run_finish_line Crossing during an active run ends it
initiation_line Crossing without having initiated a drift → DQ
brake_zone_start_{N} Flashes BRAKE ZONE on the driver’s HUD
brake_zone_end_{N} Clears the flash

run_start_line needs a BoxCollider, not a mesh collider. The mod does an oriented-bounding-box test so the box can be rotated to follow the track. A MeshCollider is used as a fallback but is less predictable on a rotated box.

The other lines want a convex MeshCollider with isTrigger = true.


Start lights

Name Role
start_lights Root of the lamp post
panel_1 … panel_4 The four emissive panels, top to bottom

The mod finds these by name and writes _EmissionColor directly. Each panel renderer needs its own material instance — if they share one material, all four light up together.

Enable Emission on the material and make sure the _EMISSION keyword is ticked, or the runtime write has no visible effect. HDRP scenes usually need StartLightsEmissionIntensity in the 5–25 range; it’s in config.json.

Sequence: panel 1 white at 3 s remaining, panels 1–2 at 2 s, panels 1–2–3 at 1 s, all four red on GO.


offtrack_{Index}

Marks a surface as out, for the case the surface itself can’t express.

Off-course normally reads CarX’s per-wheel surfaceType and counts any wheel not on Asphalt. That works for grass, dirt and gravel, and needs no marker at all. It cannot work for a run-off that is itself paved — CarX reports asphalt under every wheel there, so a driver can run arbitrarily wide onto tarmac and never trigger off-course.

offtrack_ meshes close that hole. Any wheel inside one counts as off-surface, on top of whatever the surface type says. The two signals are combined per wheel, so a car with one wheel on grass and another on a marked paved run-off counts as two wheels off, not one.

The usual thresholds then apply: two wheels off for 0.15 s is a DQ, one wheel off accrues a per-second deduction.

Place these over paved run-off, escape roads and paved infield — anywhere going wide should be punished but the material won’t say so. You don’t need them over grass or dirt.

Build them like your zone meshes

Containment is tested against the mesh in its own local space, so a mesh rotated in the ground plane marks the strip you actually modelled rather than a world-aligned box around it. Two constraints come with that, and they’re the same ones overshoot_ meshes already have:

The mesh’s local Y must be its up axis. The test is generous vertically (−2 m to +4 m, to absorb the gap between a wheel’s centre and the ground) and exact across X and Z. A wheel roughly 0.3 m above the surface has to land in that vertical window. If the mesh is rotated so its local Y is no longer vertical — a Unity Quad laid flat is the usual way this happens — that 0.3 m falls along an axis with no tolerance and the wheel reads as outside. Use a box-like mesh with Y up.

Don’t flatten it by scaling Y toward zero. The tolerance is measured in local units, so it shrinks with the scale. At scale.y = 0.01 the ±4 m window becomes ±4 cm and a wheel sitting right on the mesh is outside it. Give the mesh real thickness — half a metre is plenty — and leave Y scale near 1.

A cube primitive, scaled in X and Z to cover the area and left alone in Y, does the right thing. So does anything exported from Maya or Blender as a slab.

The mesh also needs readable geometry (MeshFilter or MeshCollider). If it has none, the mod logs that it’s being skipped rather than failing silently — check debug.log if a marker seems to do nothing.

Before v1.9.2 this prefix was parsed only on a fallback path that never ran in practice, so placing these meshes did nothing. If you authored a map against that behaviour and put offtrack_ meshes somewhere merely decorative, they will now DQ people. RespectOffTrackMeshes: false in config.json turns the check off while you fix the map.


Names the mod does not act on

wall_{Index}

Accepted in a scene, causes no errors, drives no behaviour.

Wall contact is detected through CarX’s own collision events on the car (RaceCar.OnCollisionEnterEvent), which fire for any solid collider. The mod explicitly skips wall_-prefixed objects during the scene scan — no trigger is attached, no bounds cached.

You do not need to mark walls. Any solid barrier already registers wall taps and wall-impact DQs. Keep the marker if you use it to group barriers in your hierarchy; it costs nothing.


Authoring without the SDK

Everything above is a string. You can rename meshes by hand in Unity, or export them from Maya or Blender already named correctly, and CHAPPiE will score them.

The SDK’s value is that ZoneID, Length and Depth become inspector fields with a live gizmo, so a mistyped index shows up as a wrong colour in the scene view instead of a zone that silently never scores.