Damage System
Table of Contents
- Damage System
- RCCP_Damage Component
- Key Properties
- Zone Health & Telemetry (V2.58+)
- Configuration
- Query API
- Damage Events
- Mesh Deformation
- How It Works
- Configuration
- Example: Adjusting Deformation Sensitivity
- Detachable Parts (RCCP_DetachablePart)
- Setup Requirements
- Part Types
- Damage Lifecycle
- Configuration
- RCCP_Damage Part Settings
- Wheel Damage
- Configuration
- Light Damage
- RCCP_Light Damage Properties
- RCCP_Damage Light Settings
- Damage Mechanics (RCCP_DamageMechanics, V2.58+)
- Engine Damage
- Steering Misalignment
- Staged Deflation
- Fire -> Engine Death
- Repair Contract
- Cross-Cutting
- Editor
- Damage VFX (RCCP_Particles, V2.58+)
- Impact Feedback
- Smoke & Fire Stages
- Event Bursts
- Scrape Ramp
- Soft Particles
- Settings Reference
- Damage HUD (RCCP_SportyDamageUI, V2.58+)
- Display Modes and Visibility
- Tuning Reference: Damage Scaling Constants
- Saving and Loading Damage
- DamageData Class
- Save Format & Versioning
- Save / Load API
- Repairing Vehicles
- Using the Public API
- What Repair Does
- Common Issues
- Damage not showing on collision
- Detachable parts not falling off
- Repair not working
- Wheel detachment not triggering
- Damage Mechanics has no effect
- Damage smoke/fire not appearing
- Performance concerns with deformation
- Related Topics
RCCP includes a comprehensive damage system that supports four types of cosmetic vehicle damage: mesh deformation, detachable parts, wheel damage, and light breakage. All damage types are managed through the RCCP_Damage component attached to the vehicle, and every type can be toggled independently.
When a collision occurs, RCCP calculates the impulse magnitude, converts it to a contact point, and distributes the damage across all enabled subsystems within range. The system caches original mesh data at startup so that vehicles can be fully repaired at any time.
V2.58 layers three additive systems on top of this cosmetic base, all opt-in and inert until you use them:
- Zone health & telemetry on
RCCP_Damageitself -- a queryable Front/Rear/Left/Right damage model with events, on by default (telemetry only, zero gameplay effect on its own). RCCP_DamageMechanics-- a separate, optional component that turns zone health into actual consequences: torque loss, steering misalignment, staged tire deflation, and a fire-to-engine-death sequence.- Damage VFX in
RCCP_Particles-- impact-scaled sparks and lights, engine-bay smoke/fire stages, one-shot event bursts (blowout, part debris, glass), a sustained-scrape ramp, and a soft-particle shader. RCCP_SportyDamageUI-- an optional runtime HUD visualizing zone health, flat tires, radiator heat, and engine fire.
RCCP_Damage Component
The RCCP_Damage component is the central controller for all vehicle damage. Add it to any vehicle that should support collision damage.
Key Properties
| Property | Type | Default | Description |
|---|---|---|---|
automaticInstallation | bool | true | When enabled, automatically finds all damageable meshes, detachable parts, lights, and wheels on the vehicle at startup. When disabled, you must assign each array manually in the Inspector. |
damageFilter | LayerMask | Everything | Controls which layers can cause damage to the vehicle. Only collisions with objects on these layers will trigger damage calculations. |
maximumDamage | float | 0.75 | Maximum vertex/wheel displacement distance in meters. Limits how far any single vertex (or a wheel) can move from its original position. Also doubles as the wheel-detach trigger (see Wheel Damage). Set to 0 to disable the limit. |
processInactiveGameobjects | bool | false | Whether to include inactive child GameObjects when collecting meshes and parts during automatic installation. |
saveName | string | Vehicle name | Identifier used for saving and loading damage data via JSON. Auto-populated from the vehicle's GameObject name. |
Zone Health & Telemetry (V2.58+)
Independently of the four cosmetic damage toggles below, RCCP_Damage tracks structural health per zone -- a lightweight, queryable damage model gameplay code can read without caring whether mesh deformation, wheel damage, part damage, or light damage are individually enabled.
Each qualifying collision resolves to one of four zones from the collision's root-local contact point: whichever local axis has the larger magnitude decides front/rear vs. left/right, and its sign picks the specific zone (+Z = Front, -Z = Rear, +X = Right, -X = Left).
public enum RCCP_DamageZone { Front, Rear, Left, Right }
Each zone holds a health value from 100 (intact) down to 0 (fully damaged). Per qualifying hit: zoneHealth[zone] -= normalizedImpulse * zoneDamageMultiplier, clamped to [0, 100]. There is no distance falloff on zone damage -- it's a flat deduction driven purely by impulse, independent of which cosmetic subsystems are enabled or where exactly the contact point landed within the zone.
Tracking is on by default because it has no behavioral consequence by itself -- it's pure telemetry. All mechanical consequences require adding the separate RCCP_DamageMechanics component (below).
Configuration
| Property | Type | Default | Description |
|---|---|---|---|
trackZoneHealth | bool | true | Master toggle for zone-health accumulation. Telemetry only -- mechanical consequences require RCCP_DamageMechanics. |
zoneDamageMultiplier | float | 3 | Health removed per hit = normalizedImpulse * zoneDamageMultiplier. At the default, a typical solid hit (normalized impulse ~3) removes ~9 health -- roughly 8-12 solid hits to zero out one zone. |
All four zones reset to 100 whenever the vehicle finishes repairing (see Repairing Vehicles).
Query API
// 0 (fully damaged) - 1 (intact)
float frontHealth = damage.GetZoneHealth(RCCP_DamageZone.Front);
// 0 (pristine) - 1 (all four zones fully damaged)
float overallDamage = damage.GetOverallDamageRatio();
// Accumulated displacement (meters) of one wheel from its authored rest position.
float wheelOffset = damage.GetWheelDamageOffset(0);
| Method | Returns | Description |
|---|---|---|
GetZoneHealth(RCCP_DamageZone zone) | float, 0-1 | Normalized health of one zone. 1 = intact, 0 = fully damaged. Returns 1 if zone tracking hasn't initialized yet. |
GetOverallDamageRatio() | float, 0-1 | Average damage across all four zones. 0 = pristine, 1 = every zone at zero health. |
GetWheelDamageOffset(int wheelIndex) | float, meters | Magnitude of a wheel's displacement from its original local position -- the same accumulated state that was previously write-only internal state. Index order matches CarController.AllWheelColliders. |
Damage Events
Eight static events on RCCP_Events let gameplay code react to damage without polling any component. The first three shipped in V2.51; the remaining five are new in V2.58.
| Event | Signature | Fired When |
|---|---|---|
OnRCCPDamaged | onRCCPDamaged(RCCP_CarController rccp) | A vehicle first takes deformation/collision damage after being intact. Latched -- only fires on the intact-to-damaged transition, and only if a damage subsystem actually processed the hit (see the damaged-latch note under Common Issues). |
OnRCCPRepaired | onRCCPRepaired(RCCP_CarController rccp) | A vehicle finishes repairing (damage fully reset, including zone health). |
OnRCCPImpact | onRCCPImpact(RCCP_CarController rccp, float impulse) | A debounced collision impact (per-vehicle cooldown + minimum impulse). Use this one for gameplay reactions (sound stingers, score penalties, UI shake) -- it won't spam you with one event per contact point. The raw OnRCCPCollision event still fires on every contact and remains the right hook for damage/particle work. |
OnRCCPZoneDamaged | onRCCPZoneDamaged(RCCP_CarController rccp, RCCP_DamageZone zone, float health01) | A collision reduces a zone's health. health01 is the zone's health after the hit, 0-1. |
OnRCCPPartDetached | onRCCPPartDetached(RCCP_CarController rccp, RCCP_DetachablePart part) | A detachable part fully detaches from the vehicle. |
OnRCCPWheelDetached | onRCCPWheelDetached(RCCP_CarController rccp, RCCP_WheelCollider wheel) | A wheel detaches due to accumulated damage. |
OnRCCPLightBroken | onRCCPLightBroken(RCCP_CarController rccp, RCCP_Light light) | A vehicle light breaks from collision damage. |
OnRCCPCaughtFire | onRCCPCaughtFire(RCCP_CarController rccp) | Critical engine-zone damage ignites a fire. Requires the RCCP_DamageMechanics component -- RCCP_Damage alone never fires this one. |
void OnEnable() {
RCCP_Events.OnRCCPImpact += HandleImpact;
RCCP_Events.OnRCCPZoneDamaged += HandleZoneDamaged;
RCCP_Events.OnRCCPWheelDetached += HandleWheelDetached;
RCCP_Events.OnRCCPRepaired += HandleRepaired;
}
void OnDisable() {
RCCP_Events.OnRCCPImpact -= HandleImpact;
RCCP_Events.OnRCCPZoneDamaged -= HandleZoneDamaged;
RCCP_Events.OnRCCPWheelDetached -= HandleWheelDetached;
RCCP_Events.OnRCCPRepaired -= HandleRepaired;
}
void HandleImpact(RCCP_CarController vehicle, float impulse) {
Debug.Log(vehicle.name + " hit something with impulse " + impulse);
}
void HandleZoneDamaged(RCCP_CarController vehicle, RCCP_DamageZone zone, float health01) {
Debug.Log(vehicle.name + " zone " + zone + " health now " + (health01 * 100f) + "%");
}
void HandleWheelDetached(RCCP_CarController vehicle, RCCP_WheelCollider wheel) {
Debug.Log(wheel.name + " fell off " + vehicle.name);
}
void HandleRepaired(RCCP_CarController vehicle) {
Debug.Log(vehicle.name + " is fully repaired.");
}
Mesh Deformation
Mesh deformation displaces individual vertices of the vehicle's body meshes when a collision occurs. Vertices within the deformation radius of the contact point are pushed inward along the collision direction, producing realistic crumple effects.
How It Works
- On collision, the system converts the contact point to local space for each mesh.
- An octree spatial structure is used for fast nearest-vertex lookup, avoiding the cost of iterating every vertex on every collision.
- Vertices within
deformationRadiusof the contact point are displaced. Damage is stronger at the center and falls off linearly to zero at the edge of the radius. - Original mesh vertex positions are cached at startup so they can be restored during repair.
- Meshes must have Read/Write Enabled in their import settings. Non-readable meshes are automatically skipped with a console warning.
Configuration
| Property | Type | Default | Description |
|---|---|---|---|
meshDeformation | bool | true | Master toggle for mesh deformation. |
deformationRadius | float | 0.75 | Radius around the contact point (in meters) within which vertices are affected. Larger values create wider dents. |
deformationMultiplier | float | 1.0 | Scales the amount of vertex displacement. Internally divided by 10 (see Tuning Reference below) -- a value of 1 applies 10% of the raw computed displacement; set to 10 for a 1:1 mapping. |
deformationDirectionMode | enum (Legacy, Corrected) | Legacy | V2.58. Controls which direction dents push in. Legacy (default) reproduces the original radial-toward-root-origin vector computed in root-local space and applied directly in each mesh's local space -- this can look skewed on child meshes that are rotated relative to the root, but is kept as the default for byte-identical behavior on existing vehicles. Corrected derives the dent direction from the actual collision contact normal, transformed into each mesh's own local space via InverseTransformDirection -- fixes the skew on rotated child meshes. This only changes *direction*; falloff shape and radius behavior are unchanged in both modes. |
recalculateNormals | bool | false | Recalculates mesh normals after deformation. Enable this if lighting looks incorrect on deformed areas. Costs some performance. |
recalculateBounds | bool | false | Recalculates mesh bounds after deformation. Enable this if parts of the mesh disappear from view after heavy damage. Costs some performance. |
Example: Adjusting Deformation Sensitivity
// Make the vehicle more resistant to dents
RCCP_Damage damage = vehicle.GetComponentInChildren<RCCP_Damage>();
damage.deformationMultiplier = 0.5f; // Half the normal deformation
damage.deformationRadius = 0.5f; // Smaller affected area
damage.maximumDamage = 0.4f; // Limit maximum displacement
Detachable Parts (RCCP_DetachablePart)
Detachable parts are vehicle body panels (hoods, doors, bumpers, trunks) that can become loose and eventually fall off the vehicle when damaged. Each part uses a ConfigurableJoint to attach it to the vehicle body.
Setup Requirements
- The part must be a separate child GameObject of the vehicle with its own
Rigidbody. - A
ConfigurableJointis required (automatically created when adding the component viaReset()). - The part's GameObject and children must be on the RCCP_DetachablePart layer.
- The joint's
connectedBodyshould reference the vehicle's mainRigidbody. - BodyTilt is optional. If the vehicle has an
RCCP_BodyTiltaddon, the part automatically follows it via aParentConstraint. If not, the part simply follows the vehicle hierarchy directly -- no constraint is created, and no error is raised (fixed in V2.58; previously any detachable part on a vehicle without BodyTilt threw an NRE inAwake).
Part Types
The DetachablePartType enum identifies the role of each part:
| Part Type | Damage Multiplier | Description |
|---|---|---|
Bumper_F | 1.5x | Front bumper -- takes the most damage |
Bumper_R | 1.5x | Rear bumper -- takes the most damage |
Trunk | 1.2x | Trunk lid |
Hood | 1.0x | Engine hood |
Other | 1.0x | Any other body panel |
Door | 0.8x | Doors -- slightly more resistant |
Damage multipliers are applied when useDamageWeighting is enabled (default: true). Bumpers absorb 50% more damage per collision than hoods, while doors absorb 20% less.
Damage Lifecycle
A detachable part goes through three stages as its strength decreases:
- Locked -- The
ConfigurableJointmotions are locked. The part is rigidly attached to the vehicle. - Loose (
strength <= loosePoint) -- Joint motions are unlocked to their original settings. The part wobbles and can flap in the wind (controlled byaddTorqueAfterLoose, applied inFixedUpdateso the effect is framerate-independent). - Detached (
strength <= detachPoint) -- The part breaks free from the vehicle, becomes an independent physics object, firesOnRCCPPartDetached, and is deactivated afterdeactiveAfterSeconds.
Configuration
| Property | Type | Default | Description |
|---|---|---|---|
partType | DetachablePartType | Hood | Identifies this part's role for damage weighting. |
strength | float | 100 | Current durability. Decreases on each collision. |
lockAtStart | bool | true | Lock the ConfigurableJoint motions at startup so the part stays firmly attached. |
isDetachable | bool | true | Whether this part can fully detach. If false, the part can become loose but never falls off. |
loosePoint | int | 50 | Strength threshold below which the part becomes loose (joint unlocks). |
detachPoint | int | 0 | Strength threshold below which the part fully detaches from the vehicle. |
deactiveAfterSeconds | float | 5.0 | Seconds after detachment before the part's GameObject is deactivated. |
addTorqueAfterLoose | Vector3 | (0,0,0) | Torque applied in local space (in FixedUpdate) when the part is loose, scaled by vehicle speed. Creates a flapping effect. |
useDamageWeighting | bool | true | Apply the part-type-based damage multiplier. |
onDamaged | UnityEvent<float> | (none) | Invoked every time this part takes damage; the float argument is the damage amount just applied to strength. V2.58 -- this field was declared since the original Enhancements pass but was never actually invoked; it now fires on every OnCollision. |
COM | Transform | Auto-created | Optional center of mass override for the part's Rigidbody. |
Broken (read-only) | bool | false | Public accessor for whether the part has fully detached. |
RCCP_Damage Part Settings
The RCCP_Damage component has its own toggles that control whether detachable parts receive damage at all:
| Property | Type | Default | Description |
|---|---|---|---|
partDamage | bool | true | Master toggle for detachable part damage. |
partDamageRadius | float | 1.0 | Radius around the contact point in which parts are checked for damage. |
partDamageMultiplier | float | 1.0 | Global multiplier applied to all part damage (stacks with per-part type multipliers). |
Wheel Damage
Wheel damage displaces RCCP_WheelCollider positions on collision, simulating bent axles and misaligned wheels. When damage exceeds maximumDamage, wheels can optionally detach from the vehicle entirely.
Configuration
| Property | Type | Default | Description |
|---|---|---|---|
wheelDamage | bool | true | Master toggle for wheel damage. |
wheelDamageRadius | float | 2.0 | Radius around the contact point within which wheels are affected. |
wheelDamageMultiplier | float | 1.0 | Scales the amount of wheel displacement. |
wheelDetachment | bool | true | When enabled, wheels that exceed maximumDamage displacement will detach from the vehicle (after a spawnDetachGrace window from spawn -- default 1 second -- so a spawn-drop impact can't instantly shed wheels). |
When a wheel detaches, RCCP_WheelCollider.DetachWheel() is called and OnRCCPWheelDetached fires, which separates the wheel model from the vehicle and creates an independent physics object.
Raw wheel displacement is cosmetic by itself -- it has no effect on handling until you add RCCP_DamageMechanics (below), which reads the same accumulated offset via GetWheelDamageOffset() to drive steering misalignment and staged deflation.
Light Damage
Light damage reduces the strength of RCCP_Light components near the collision point. When a light's strength falls below its breakPoint, the light is marked as broken, turns off, and fires OnRCCPLightBroken.
RCCP_Light Damage Properties
Each RCCP_Light component has its own durability settings:
| Property | Type | Default | Description |
|---|---|---|---|
isBreakable | bool | true | Whether this light can be broken by collisions. Honored since V2.51 -- setting it false genuinely prevents breakage. |
strength | float | 100 | Current durability. Reduced by the light's computed damage * 20 on each nearby collision (see Tuning Reference). |
breakPoint | int | 35 | Strength threshold below which the light is considered broken. |
broken | bool | false | Read at runtime to check if the light is broken. |
RCCP_Damage Light Settings
| Property | Type | Default | Description |
|---|---|---|---|
lightDamage | bool | true | Master toggle for light damage. |
lightDamageRadius | float | 0.75 | Radius around the contact point within which lights are checked. |
lightDamageMultiplier | float | 1.0 | Scales the damage applied to lights. |
Damage Mechanics (RCCP_DamageMechanics, V2.58+)
RCCP_DamageMechanics is a separate, opt-in component that turns the zone-health telemetry above into actual driving consequences. RCCP_Damage never requires it and stays purely cosmetic without it; add RCCP_DamageMechanics (menu: BoneCracker Games > Realistic Car Controller Pro > Addons > RCCP Damage Mechanics) to a vehicle that already has RCCP_Damage with trackZoneHealth enabled, and four independent systems activate.
Everything this component writes goes through inert hooks on the existing components (RCCP_Engine.damageTorqueMultiplier, RCCP_WheelCollider.damageSteerBias, RCCP_WheelCollider.deflationWobble) -- removing or disabling the component restores stock behavior, resetting every hook back to its neutral value (1 for the torque multiplier, 0 for the bias/wobble fields).
It runs at [DefaultExecutionOrder(-8)] -- the same slot as RCCP_Limiter -- so its FixedUpdate writes land before RCCP_Engine (-7) reads damageTorqueMultiplier the same frame. If no RCCP_Damage component is found on the vehicle, it logs one warning, clears any consequence it had already applied, and idles; it never throws.
Zone health drives the engine and fire systems only. Steering misalignment and staged deflation read wheel displacement instead, so they keep working with RCCP_Damage.trackZoneHealth off -- in that configuration the component logs one warning naming the two systems that will see a permanently healthy engine zone, rather than idling everything.
Engine Damage
| Property | Type | Default | Description |
|---|---|---|---|
engineDamage | bool | true | Master toggle. Reduces engine torque as the engine zone takes damage. |
engineZone | RCCP_DamageZone | Front | Which zone hosts the engine. Flip to Rear for rear-engine layouts. |
healthToTorqueCurve | AnimationCurve | Linear (0, 0.4) -> (1, 1.0) | Torque multiplier over engine-zone health. Full power at health 1, 40% at health 0 -- the 0.4 floor is baked into the curve's endpoint, not a separate field. |
overheatHealthThreshold | float, 0-1 | 0.5 | Engine-zone health below which the radiator is considered damaged and heat starts building. |
overheatBuildTime | float, min 1 | 60 | Seconds until heat reaches maximum, measured at the reference RPM (where rpmToHeatRateCurve reads 1). |
rpmToHeatRateCurve | AnimationCurve | (0, 0.1) -> (0.5, 1.0) -> (1, 2.0) | Heat build rate over normalized engine RPM, X = (rpm - minEngineRPM) / (maxEngineRPM - minEngineRPM). Idling barely heats; redline heats ~2x as fast. Evaluated result is clamped at 0, so a curve dipping negative cannot actively cool. |
overheatCoolTime | float, min 1 | 45 | Seconds to shed all heat while the engine is off or the radiator is intact again. Independent of build rate. |
overheatTorqueFade | float, 0-1 | 0.3 | Additional torque fade at maximum heat -- up to 30% further power loss on top of the health curve. |
Radiator heat (EngineHeat01, read-only) builds only while both conditions hold: the engine is actually running (Engine.engineRunning) and engine-zone health sits below overheatHealthThreshold. The rate is scaled by rpmToHeatRateCurve evaluated at normalized RPM, so a damaged radiator cooks quickly under sustained high revs but barely warms at idle. In every other case -- engine switched off, killed by fire, radiator intact, or repaired -- heat dissipates over overheatCoolTime.
This gating matters beyond the gauge: heat also feeds the smoke tier in RCCP_Particles (see below), so an unattended parked wreck neither creeps to 100% heat nor smokes from an engine that is not running, and it does not carry a stale torque penalty into the next start.
The final multiplier is healthToTorqueCurve.Evaluate(health) * (1 - EngineHeat01 * overheatTorqueFade), clamped to [0, 1], written to Engine.damageTorqueMultiplier every FixedUpdate.
Steering Misalignment
| Property | Type | Default | Description |
|---|---|---|---|
steeringMisalignment | bool | true | Master toggle. Bends steering alignment (toe error) as wheels take collision damage -- the car pulls to one side. |
maxToeError | float, 0-10 | 3 | Maximum toe error in degrees at full wheel displacement. |
offsetToToeCurve | AnimationCurve | Linear (0,0) -> (1,1) | Toe error over normalized wheel displacement (offset / RCCP_Damage.maximumDamage), as a fraction of maxToeError. |
Applied to all wheels (a bent rear wheel makes the car crab slightly -- correct behavior). The sign comes from the lateral (local X) component of the wheel's accumulated displacement: a wheel shoved toward +X toes right, toward -X toes left.
Staged Deflation
| Property | Type | Default | Description |
|---|---|---|---|
stagedDeflation | bool | true | Master toggle. Heavily damaged wheels develop a slow leak instead of staying magically inflated. |
leakThreshold | float, 0-1 | 0.5 | Wheel displacement (as a fraction of RCCP_Damage.maximumDamage) that triggers a slow leak. |
slowLeakDuration | float, min 0 | 10 | Seconds for a slow leak to fully deflate the tire, via RCCP_WheelCollider.Deflate(duration). |
wobbleAmplitude | float, 0-10 | 1.5 | Per-revolution visual wobble amplitude (degrees) on deflated wheel models, driven by deflationWobble. |
deflatedBrakeDrag | float, min 0 | 50 | Constant brake drag (Nm) applied on a deflated wheel via the existing brake-torque accumulator pipeline, scaled by deflationT -- pulls the car toward the flat without fighting ESP. |
The slow-leak trigger fires once, on the rising edge (when accumulated wheel displacement first crosses leakThreshold); once triggered, RCCP_WheelCollider.Deflate(slowLeakDuration) lerps radius and stiffness down over that duration, ending at the existing binary-deflation values (radius x0.8, stiffness x0.25, from RCCP_WheelCollider's own deflatedRadiusMultiplier/deflatedStiffnessMultiplier). Existing binary deflation behavior (via spike strips or Deflate()) is unchanged -- staging only animates the transition when the trigger comes from accumulated collision damage.
Fire -> Engine Death
| Property | Type | Default | Description |
|---|---|---|---|
fireDamage | bool | true | Master toggle. Critical engine-zone damage ignites a fire that kills the engine unless repaired. |
fireHealthThreshold | float, 0-1 | 0.15 | Engine-zone health at or below which the fire can ignite. |
fireIgnitionDelay | float, min 0 | 3 | Seconds of sustained critical damage before the fire actually ignites. |
fireToEngineDeathTime | float, min 1 | 20 | Seconds of burning until the engine dies. Repair extinguishes the fire and restores everything. |
Runtime read-only state: bool IsOnFire, float FireIntensity01 (0-1 progress toward engine death), float EngineHeat01 (0-1 radiator heat, also drives the VFX smoke tier below). Ignition fires OnRCCPCaughtFire once; the countdown to death then drives FireIntensity01. Engine death goes through the public CarController.KillEngine() facade (not a raw Engine.engineRunning write).
The restore latch is deliberately narrow: the component only claims the right to restart the engine when it actually stopped a running one, so a repair never hot-starts an engine the player had switched off themselves. That latch is released on repair, on disabling fireDamage, and on disabling or removing the component -- each of those hands a fire-killed engine back rather than leaving the vehicle permanently dead.
Repair Contract
RCCP_DamageMechanics subscribes to OnRCCPRepaired. When it fires, the component resets EngineHeat01, IsOnFire, FireIntensity01, and both fire timers to zero, sets Engine.damageTorqueMultiplier back to 1, and restarts the engine via CarController.StartEngine() if it had been killed by fire. Steering bias and deflation wobble don't need an explicit reset in the handler -- by the time OnRCCPRepaired fires, RCCP_Damage has already restored wheel positions and re-inflated tires, so the very next FixedUpdate naturally recomputes zero toe bias and inert deflation state from the now-pristine wheel offsets.
Cross-Cutting
| Property | Type | Default | Description |
|---|---|---|---|
recomputeInertiaOnDetach | bool | false | When enabled, subscribes to OnRCCPPartDetached/OnRCCPWheelDetached and calls RCCP_AeroDynamics.RecomputeInertia() on each -- fulfilling the V2.53 frozen-tensor contract, which lists damage-driven mass changes as a recompute trigger. Off by default because it's an extra physics recalculation per detach event. |
Missing Engine or wheels (e.g. trailers, EV rigs mid-setup) simply idle their respective system -- nothing throws.
Editor
The custom inspector (RCCP_DamageMechanicsEditor) groups the four systems behind their master toggles (only the relevant fields show once a toggle is on), and while in Play Mode adds a live Runtime readout block: per-zone health (Front / Rear / Left / Right, as a percentage), Engine Heat, and Fire status (burning percentage or --). The readout uses the shared RCCP_DesignSystem.RepaintInspectorIfHovered() gate, so it only repaints while your cursor is actually over the Inspector.

Damage VFX (RCCP_Particles, V2.58+)
RCCP_Particles remains the single owner of collision, scratch, and damage VFX. It reacts to damage state without requiring RCCP_DamageMechanics for most of it -- only the fire tier of the smoke/fire stages needs the mechanics component.
Impact Feedback
On every qualifying collision (OnCollision), beyond the existing contact-spark burst (rateOverTime = collision.impulse.magnitude / 500, unchanged):
- Impulse-scaled extra burst.
impactExtraParticlesMax(default60) extra one-shot particles are emitted on top of the base burst, scaled by how hard the hit was:extra = impactExtraParticlesMax * InverseLerp(0.5, 10, normalizedImpulse)(same 0.5-10 normalized-impulse scaleRCCP_Damageuses). Set to0to disable the extra burst entirely. - Impact light.
impactLights(bool, defaulttrue) enables a brief warm point-light (color(1, 0.6, 0.3), no shadows) on the same pooled contact-spark slot, aboveimpactLightMinImpulse(default2, normalized). Intensity scalesLerp(1.5, 4, InverseLerp(2, 10, normalizedImpulse))and fades out over ~0.15 seconds. Mirrors the exhaust flame-light pattern. - Trails. All three spark systems (contact, scratch, wheel) ship with a Trails module authored into the prefabs, so hard hits leave brief motion streaks (this is prefab content, not an inspector field).
- Layer-filter fix.
OnCollisionnow honorscollisionFilter-- previously onlyOnCollisionStaychecked the layer mask, so off-filter objects could still trigger contact sparks on the initial hit.
Smoke & Fire Stages
enableDamageSmoke (bool, default false -- this is a visible behavior change, so it's opt-in) drives a looping engine-bay smoke emitter from the engine zone's health alone -- no RCCP_DamageMechanics required for the smoke tier:
- Below health
0.7: emitter turns on, rateLerp(6, 28)and colorLerp(light grey, near-black)by severity (InverseLerp(0.7, 0, health)). - At or above
0.7: emitter off. - If
RCCP_DamageMechanicsis present, radiator heat (EngineHeat01) further drags the effective health down (Mathf.Min(health, 1 - EngineHeat01 * 0.5)), so an overheating engine smokes earlier than health alone would suggest.
The fire tier reads RCCP_DamageMechanics.IsOnFire and requires the mechanics component -- without it, the flame system never enables regardless of enableDamageSmoke. Smoke and fire drive independently of each other, so a fire-only configuration (no smoke prefab assigned) still burns correctly.
damageSmokeAnchor (Transform, optional) positions the emitter; if left empty it auto-resolves to front-center of the vehicle's body-mesh bounds at hood height (only MeshRenderer/SkinnedMeshRenderer bounds are used -- idle ParticleSystemRenderer bounds report a zero-size box at world origin and would otherwise drag the computed anchor toward (0,0,0)).
Event Bursts
Three one-shot bursts, each a single pooled ParticleSystem instance (no runtime Instantiate after Start()):
| Trigger | Prefab slot | Behavior |
|---|---|---|
Tire deflates (rising edge of RCCP_WheelCollider.deflated) | blowoutBurstPrefab | Plays at the wheel's position. Because staged deflation flips deflated to true the instant a slow leak begins (not when it finishes), the blowout burst fires at the start of a slow leak, not the end -- it reads as "something just gave way," which matches an instant spike-strip deflation too. |
OnRCCPPartDetached | detachDebrisPrefab | Plays at the detached part's position. |
OnRCCPWheelDetached | detachDebrisPrefab (same pooled instance as part detach) | Plays at the wheel's position. |
OnRCCPLightBroken | glassShardPrefab | Plays at the light's position. |
Scrape Ramp
Long, continuous body-grinds (OnCollisionStay) now ramp up over time instead of staying at a constant rate:
| Property | Type | Default | Description |
|---|---|---|---|
scrapeRampDelay | float, min 0 | 1 (second) | How long a scrape must continue before the ramp starts. |
scrapeRampMaxMultiplier | float, min 1 | 3 | Maximum emission multiplier reached after a long continuous scrape. 1 disables the ramp. |
Once scrapeRampDelay has elapsed, the emission rate multiplier lerps from 1 to scrapeRampMaxMultiplier over the following 3 seconds of continued contact.
Soft Particles
RCCP_ParticleSoft.shader ("BoneCracker Games/RCCP/Particles/Alpha Blended Soft") is a Built-in-RP depth-fade particle shader swapped into the collision-smoke materials, softening the intersection where smoke meets the ground or body panels. It requires a camera depth texture (SOFTPARTICLES_ON); when no depth texture is available (e.g. some mobile configurations), it automatically falls back to plain, un-faded alpha blending -- the shader still renders correctly, just without the soft edge. Fallback "Legacy Shaders/Particles/Alpha Blended" covers any pipeline that can't run the custom pass at all.
Settings Reference
Five new prefab slots on RCCP_Particles, auto-assigned from RCCP_Settings in Reset() (same pattern as the three pre-existing contactSparklePrefab/scratchSparklePrefab/wheelSparklePrefab slots). Reset() only runs when the component is first added or manually reset in the Inspector, so any of these five slots that deserialize null on a pre-existing vehicle (e.g. one built before V2.58) are backfilled at runtime from the same RCCP_Settings fields in Start() -- an explicit per-vehicle assignment always wins over the backfill. A null slot on the vehicle therefore does not disable that effect by itself; the actual global off-switch is the corresponding field being null on the RCCP_Settings asset, since that is what the runtime backfill falls back to.
RCCP_Particles field | RCCP_Settings source field | Used for |
|---|---|---|
damageSmokePrefab | damageSmokeParticles | Looping engine-bay smoke. |
damageFirePrefab | damageFireParticles | Looping fire (requires RCCP_DamageMechanics). |
blowoutBurstPrefab | blowoutParticles | One-shot tire blowout burst. |
detachDebrisPrefab | detachDebrisParticles | One-shot part/wheel detach debris burst. |
glassShardPrefab | glassShardParticles | One-shot light-break glass shard burst. |
Mobile posture: everything is pooled and one-shot where applicable; impact lights are capped at one per contact-spark slot and can be disabled entirely (impactLights = false); soft particles auto-fallback to plain alpha blend with no depth texture; the smoke emitter is a single looping system in the 6-28 rate range.
Damage HUD (RCCP_SportyDamageUI, V2.58+)
RCCP_SportyDamageUI (Scripts/UI/, a RCCP_UIComponent) is the runtime damage readout: the four zone glows, per-wheel flat-tire icons, a radiator heat slider, and a fire warning badge + fire intensity slider. It reads the active player vehicle from RCCP_SceneManager every frame, so it works as a scene-level HUD and does not need to be parented to a vehicle. Zone glows come from RCCP_Damage.GetZoneHealth(); the heat and fire readouts require RCCP_DamageMechanics on the vehicle and hide themselves when it is absent or disabled.
Display Modes and Visibility
| Field | Default | Meaning |
|---|---|---|
displayMode | ShowOnCollision | AlwaysOn keeps the HUD at full alpha permanently. ShowOnCollision fades it in on an event and back out afterwards. |
visibilityDuration | 5 | Seconds the HUD stays up after a triggering event (ShowOnCollision only). |
fadeSpeed | 3 | Alpha units per second for the fade in/out. |
minCollisionForce | 2.5 | Minimum collision.relativeVelocity.magnitude that counts as a HUD-triggering impact. |
holdWhileCritical | true | Keeps the HUD visible for as long as the vehicle stays in a critical state, ignoring the visibility timer (ShowOnCollision only). |
criticalHeatThreshold | 0.5 | Normalized engine heat (RCCP_DamageMechanics.EngineHeat01, 0-1) at or above which the vehicle counts as critical. |
In ShowOnCollision mode the timer is refilled by four edge events: an impact at or above minCollisionForce, an overall damage ratio increase (> 0.005), a wheel newly going flat, and the engine igniting.
holdWhileCritical adds a level-triggered hold on top of that. While any of the following is true, the HUD stays at full alpha regardless of the timer:
- the engine is on fire (
RCCP_DamageMechanics.IsOnFire), EngineHeat01 >= criticalHeatThreshold,- any wheel is
deflated.
This matters because radiator heat has no edge to trigger on -- it climbs gradually over overheatBuildTime after zone health drops below overheatHealthThreshold. Without the hold, the HUD faded out on its normal timer while the temperature gauge was still rising, hiding the readout precisely when the driver needed to watch it. The visibility timer keeps draining in the background during a hold, so once the vehicle leaves the critical state (repair, cooldown, tire re-inflation) the HUD fades out immediately rather than earning a fresh visibilityDuration.
Set holdWhileCritical = false to restore the pure timer behavior, or raise criticalHeatThreshold toward 1 to hold only on near-terminal overheating. The flag has no effect in AlwaysOn mode.
Tuning Reference: Damage Scaling Constants
These constants were previously implicit in the code. All hit-scaling shares one normalized impulse value, computed once per collision:
normalizedImpulse = collision.impulse.magnitude / 7500
In RCCP_Damage.OnCollision, a value below 0.5 is treated as zero -- the hit is discarded and no subsystem (including zone tracking) processes it at all -- and a value above 10 is capped at 10. So the effectively-processed range is [0.5, 10], not a symmetric clamp. RCCP_Particles computes the same ratio for its own impact-scaling (extra burst size, impact light) but applies a true Mathf.Clamp(x, 0, 10) -- its floor is 0, not 0.5, since it has no "discard the hit" case.
| Subsystem | Formula | Falloff shape |
|---|---|---|
| Mesh deformation | displacement = direction * (normalizedImpulse * (1 - clamp01(dist / deformationRadius))) * (deformationMultiplier / 10) | Linear, 100% at the contact point down to 0% at the radius edge. |
| Wheel displacement | displacement = (normalizedImpulse * wheelDamageMultiplier / 30) * (1 - 0.5 * clamp01(distSqr / radiusSqr)) | Squared-distance, up to 50% reduction at the radius edge. |
| Part strength | damageToApply = (normalizedImpulse * partDamageMultiplier * (1 - 0.5 * squaredFalloff)) * 5 * typeMultiplier | Squared-distance, up to 50% reduction. typeMultiplier is the Part Types table above (0.8x-1.5x). |
| Light strength | strengthLoss = (normalizedImpulse * lightDamageMultiplier * (1 - 0.5 * squaredFalloff)) * 20 | Squared-distance, up to 50% reduction. |
| Zone health | zoneHealth -= normalizedImpulse * zoneDamageMultiplier (default 3), clamped [0, 100] | None -- flat deduction, no distance falloff, independent of which cosmetic subsystems are enabled. |
These scales are unchanged by V2.58 (mesh /10, wheel /30, part x5, light x20, the 0.5 floor, and the 10 cap were all load-bearing before this release and remain load-bearing now) -- V2.58 only adds the zone-health row and the parallel VFX impulse scale.
Saving and Loading Damage
Damage state can be persisted between sessions using JSON serialization via PlayerPrefs.
DamageData Class
The RCCP_Damage.DamageData class stores the complete damage snapshot:
originalMeshData-- Original vertex positions for all meshesdamagedMeshData-- Current (deformed) vertex positionsoriginalWheelData-- Original wheel positions and rotationsdamagedWheelData-- Current wheel positionslightData-- Boolean array of broken/intact state for each lightdataVersion-- V2.58. Save format version;0(or the field absent entirely) means a legacy pre-V2.58 save.zoneHealth-- V2.58. Per-zone structural health (Front, Rear, Left, Right), persisted sincedataVersion 1.partData-- V2.58. Per-detachable-part{ strength, broken }state, index-matched toRCCP_Damage.parts, persisted sincedataVersion 1.wheelDeflated-- V2.58. Per-wheel deflated boolean, index-matched toCarController.AllWheelColliders, persisted sincedataVersion 1.
Detachable part state and wheel deflation are persisted and restored on load (as of dataVersion 1) -- a saved broken part re-runs its detach flow on load, a saved-loose part re-applies its loose joint state, and a saved-deflated wheel re-deflates (or a saved-inflated wheel re-inflates, restoring both directions).
Save Format & Versioning
Every save writes dataVersion = 1 and the full zone/part/wheel-deflation payload. On load:
- A legacy save (
dataVersionabsent or0) restores meshes, wheel positions, and light broken-state exactly as before V2.58 -- the new zone/part/wheel-deflation fields are simply absent from the JSON,JsonUtilityleaves them at their C# defaults, and the load path skips that whole block. Behavior is identical to pre-V2.58 RCCP. - A
dataVersion 1save additionally restores zone health, per-part strength/broken state, and per-wheel deflation. - Fields are never renamed, retyped, or removed once shipped -- only added -- so old saves keep loading unchanged and newer saves degrade gracefully if loaded by an older build (unknown fields are simply ignored by
JsonUtility). - Load validation (V2.58): before applying anything, the loader checks the saved mesh vertex counts, wheel count, and light count against the vehicle's current rig. If any of them mismatch (e.g. the model changed since the save was written), the load is skipped entirely and the vehicle is left intact rather than half-restored or throwing an exception. Light restoration is also clamped to the shorter of the saved/current light arrays for the same reason.
Save / Load API
RCCP_Damage damage = vehicle.GetComponentInChildren<RCCP_Damage>();
// Save current damage state (always synchronous)
damage.Save();
// Load previously saved damage state (always synchronous)
damage.Load();
// Delete saved damage data
damage.Delete();
The saveName property is used as the PlayerPrefs key (with _DamageData appended). Make sure each vehicle has a unique saveName if you want independent save slots.
Save()/Load() always route through the synchronous RCCP_DamageData.SaveDamageRaw/LoadDamageRaw paths (see Common Issues below for what changed here in V2.58). The older RCCP_DamageData.SaveDamage/LoadDamage async overloads still exist but are [Obsolete] -- they now simply forward to the synchronous versions.
Repairing Vehicles
There are two ways to repair a vehicle:
Using the Public API
// Repair a specific vehicle
RCCP.Repair(carController);
// Repair the current player vehicle
RCCP.Repair();
Both methods set repairNow = true on the vehicle's RCCP_Damage component, which triggers the repair process on the next frame.
What Repair Does
When repairNow is set to true:
- Meshes -- All deformed vertices are moved back to their original positions.
- Wheels -- Wheel position is restored to its original local value; wheel rotation is force-set to
Quaternion.identity(not restored to whatever its original rotation was). Deflated tires are re-inflated viaInflate(). - Detachable Parts -- Each part's
OnRepair()is called: strength is restored, the ConfigurableJoint is recreated if destroyed, joint properties are restored, and the part is re-enabled if it was deactivated. - Lights -- Each light's
OnRepair()is called: strength is restored andbrokenis set tofalse. - Zone health -- All four zones reset to
100(V2.58). - Mechanics state -- If
RCCP_DamageMechanicsis present,OnRCCPRepairedresets heat, fire, and the torque multiplier, and restarts the engine if it had been killed by fire (V2.58; see Repair Contract above).
Repair is a one-shot, single-pass operation, not an animated interpolation: the vertex/wheel-position math is a direct assignment back to the cached original values (vertices[i] += (original[i] - vertices[i]) algebraically reduces to vertices[i] = original[i]), so repaired becomes true in the same call that processed it -- there's no "not complete in one frame, continues on subsequent frames" case in practice. RCCP_Events.Event_OnRCCPRepaired fires once, at the end of that same pass.
Common Issues
Damage not showing on collision
- Verify
automaticInstallationis enabled, or that meshes are manually assigned to themeshFiltersarray. - Check that the colliding object's layer is included in
damageFilter. - Confirm the vehicle's meshes have Read/Write Enabled in their import settings. Non-readable meshes are silently skipped.
- Ensure meshes have enough vertices for visible deformation. Low-poly meshes may not show noticeable dents.
OnRCCPDamaged/repairedonly flip when a damage subsystem actually ran on the hit (V2.58 fix) -- with all four cosmetic toggles (meshDeformation,wheelDamage,partDamage,lightDamage) off, a qualifying collision no longer falsely marks the vehicle as "damaged." Zone-health tracking is independent of these four toggles and still accumulates even with all of them off.
Detachable parts not falling off
- The part's GameObject and its children must be on the RCCP_DetachablePart layer.
- Verify that
strength,loosePoint, anddetachPointare set correctly. The default values are100,50, and0respectively. - Make sure
isDetachableistrueon theRCCP_DetachablePartcomponent. - Check that the part has a valid
ConfigurableJointconnected to the vehicle'sRigidbody.
Repair not working
repairNowmust be set totrue. UseRCCP.Repair(vehicle)rather than manipulating the flag directly.- If the vehicle was not damaged (
repairedis alreadytrue), the repair process will not run. - Repair completes in the same call it starts (see What Repair Does) -- if it looks like nothing happened, check that
repairNowwas actuallytrueon the frame you expected, not that repair is still "in progress."
Wheel detachment not triggering
- Confirm
wheelDetachmentistrueon theRCCP_Damagecomponent. - The wheel displacement must exceed
maximumDamagebefore detachment is triggered. - Make sure
wheelDamageis enabled. - Check
spawnDetachGrace-- wheel detachment (not deformation) is suppressed for this many seconds after spawn (default 1s) so a spawn-drop impact can't instantly shed wheels.
Damage Mechanics has no effect
RCCP_DamageMechanicsrequires anRCCP_Damagecomponent on the same vehicle withtrackZoneHealthenabled -- without it, the component logs one warning and idles everyFixedUpdate.- Each of the four systems (
engineDamage,steeringMisalignment,stagedDeflation,fireDamage) has its own master toggle; check the specific one for the behavior you expected. - Disabling or removing the component restores stock behavior immediately (
OnDisablezeroes every inert hook it wrote) -- if you toggled it off mid-session and effects lingered, that's a different bug, not expected behavior.
Damage smoke/fire not appearing
enableDamageSmokeonRCCP_Particlesdefaults tofalse-- it must be explicitly enabled.- The smoke prefab and fire prefab are independent; assign both if you want both tiers, or just one for a partial effect.
- The fire tier requires
RCCP_DamageMechanicson the same vehicle -- smoke alone works fromRCCP_Damagezone health with no mechanics component.
Performance concerns with deformation
- Reduce
deformationRadiusto limit the number of vertices processed per collision. - Disable
recalculateNormalsandrecalculateBoundsif they are not visually necessary. - Mesh deformation and repair are always synchronous, single-threaded operations (the inner loop is microseconds for typical vehicle meshes) -- there is no multithreading toggle to enable for this system.
Related Topics
- Vehicle Setup -- Adding components to a vehicle
- Customization -- Paint, wheels, and upgrades
- Troubleshooting -- General debugging guide
Support: bonecrackergames@gmail.com | www.bonecrackergames.com
Need help? See Troubleshooting