Damage System

Table of Contents

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:

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

PropertyTypeDefaultDescription
automaticInstallationbooltrueWhen 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.
damageFilterLayerMaskEverythingControls which layers can cause damage to the vehicle. Only collisions with objects on these layers will trigger damage calculations.
maximumDamagefloat0.75Maximum 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.
processInactiveGameobjectsboolfalseWhether to include inactive child GameObjects when collecting meshes and parts during automatic installation.
saveNamestringVehicle nameIdentifier 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

PropertyTypeDefaultDescription
trackZoneHealthbooltrueMaster toggle for zone-health accumulation. Telemetry only -- mechanical consequences require RCCP_DamageMechanics.
mechanicalDamageboolfalseOpt-in switch for mechanical consequences (torque loss, radiator overheat, steering pull, flat tires, fire). Ticking it creates the RCCP_DamageMechanics child object; unticking it removes it. See Damage Mechanics below.
zoneDamageMultiplierfloat3Health 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);
MethodReturnsDescription
GetZoneHealth(RCCP_DamageZone zone)float, 0-1Normalized health of one zone. 1 = intact, 0 = fully damaged. Returns 1 if zone tracking hasn't initialized yet.
GetOverallDamageRatio()float, 0-1Average damage across all four zones. 0 = pristine, 1 = every zone at zero health.
GetWheelDamageOffset(int wheelIndex)float, metersMagnitude 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.

EventSignatureFired When
OnRCCPDamagedonRCCPDamaged(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).
OnRCCPRepairedonRCCPRepaired(RCCP_CarController rccp)A vehicle finishes repairing (damage fully reset, including zone health).
OnRCCPImpactonRCCPImpact(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.
OnRCCPZoneDamagedonRCCPZoneDamaged(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.
OnRCCPPartDetachedonRCCPPartDetached(RCCP_CarController rccp, RCCP_DetachablePart part)A detachable part fully detaches from the vehicle.
OnRCCPWheelDetachedonRCCPWheelDetached(RCCP_CarController rccp, RCCP_WheelCollider wheel)A wheel detaches due to accumulated damage.
OnRCCPLightBrokenonRCCPLightBroken(RCCP_CarController rccp, RCCP_Light light)A vehicle light breaks from collision damage.
OnRCCPCaughtFireonRCCPCaughtFire(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.

An optional, physically-simulated alternative to this instant-displacement model is available -- see Physically-Simulated Deformation (XPBD) below. It is opt-in and changes nothing unless you add it.

How It Works

  1. On collision, the system converts the contact point to local space for each mesh.
  2. An octree spatial structure is used for fast nearest-vertex lookup, avoiding the cost of iterating every vertex on every collision.
  3. Vertices within deformationRadius of the contact point are displaced. Damage is stronger at the center and falls off linearly to zero at the edge of the radius.
  4. Original mesh vertex positions are cached at startup so they can be restored during repair.
  5. Meshes must have Read/Write Enabled in their import settings. Non-readable meshes are automatically skipped with a console warning.

Configuration

PropertyTypeDefaultDescription
meshDeformationbooltrueMaster toggle for mesh deformation.
deformationRadiusfloat0.75Radius around the contact point (in meters) within which vertices are affected. Larger values create wider dents.
deformationMultiplierfloat1.0Scales 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.
deformationDirectionModeenum (Legacy, Corrected)LegacyV2.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.
recalculateNormalsbooltrueRecalculates mesh normals after deformation so lighting on deformed panels stays correct. Costs some performance -- switching it off is the single largest saving available on both the legacy path and the XPBD solver, at the cost of dents no longer catching the light.
recalculateBoundsbooltrueRecalculates mesh bounds after deformation, so heavily damaged meshes are not incorrectly frustum-culled. 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

Physically-Simulated Deformation (XPBD)

RCCP_DeformationSolver is an optional component that replaces the instant vertex displacement described above with a small soft-body simulation. Impact energy is injected into the panel as particle velocity, propagates through it, settles over a second or two, and leaves a permanent crease. Repairing the vehicle then springs the panel back instead of snapping it.

The model is XPBD (Extended Position-Based Dynamics). Each body mesh becomes an independent solver island: its vertices are welded into simulation particles, every particle is softly anchored to its own rest position, and neighboring particles are linked by stretch and bend constraints. Because the anchors dominate, the solver never has to be globally stable -- it only relaxes a local dent back onto a strongly-held shell. Displacement past plasticYield is baked into the rest pose, which is what turns an impact into a dent that stays.

Nothing changes until you add it. Soft-Body Deformation is off by default, and no prefab in core RCCP -- including the prototype vehicle -- carries a solver. A project that never adds RCCP_DeformationSolver runs the same legacy deformation code, with the same results, as it did before the solver existed. When the component is present, enabled and has built its islands, RCCP_Damage hands mesh deformation over to it; remove or disable it and every path falls straight back to the legacy behavior.

The one exception is the demo content. The 15 vehicles in the optional Demo Content addon ship with Soft-Body Deformation enabled on the Standard preset, so the demo scenes show the feature working without any setup. That is a deliberate showcase, not the default for your own vehicles, and it is worth knowing about for two reasons: those demo cars pay the one-off island build described in Performance and Platform Notes when they spawn, and if you use a demo vehicle as the starting point for your own, you inherit the solver along with it. Untick the box on your copy if you would rather not.

When to Use the Solver

Legacy deformation (default)RCCP_DeformationSolver
Dent shapeOne direction per hit, linear falloff to the radius edge -- visibly conicalFollows the contact normal and the panel's own structure; edges crease, flat panels bow
TimingApplied on the next frame and static from then onAnimates over a settle window, then sleeps
Repeated hitsEach hit stamps independentlyDamage accumulates continuously across hits
RepairSnaps back in a single frameAnimated spring-back (or single-frame with instantRepair)
Cost when nothing is happeningZeroEffectively zero -- 0.0006 ms measured; settled islands are skipped before any work is done
Cost when the vehicle spawnsZeroOne-off island build; expensive topology runs on bounded background workers by default, while snapshots/finalization stay on the main thread, plus roughly 6-8 MB held for the vehicle's lifetime
Cost while a dent is settlingNegligibleMeasurable; see Performance and Platform Notes
SetupAlready active on every vehicle with RCCP_DamageAdd one component
MobileLong-shipping defaultNot measured -- see the platform notes below

As a rule of thumb: keep legacy deformation for AI traffic, background vehicles, distant LOD levels and large fleets; add the solver to the vehicles the player actually looks at when they crash. The two coexist in the same scene without any special handling -- the solver is per-vehicle, not global.

Adding the Solver

  1. The vehicle needs an RCCP_Damage component with Mesh Deformation enabled. That toggle gates both the legacy path and the solver (see Compatibility and Limitations).
  2. Tick Soft-Body Deformation, nested under Mesh Deformation on the RCCP_Damage inspector. That creates an RCCP_DeformationSolver child GameObject under RCCP_Damage, beside RCCP_DamageMechanics; unticking it destroys that object again. Both edits are undoable. The checkbox is off by default, so no existing project changes until you tick it -- the demo vehicles in the Demo Content addon are the exception and ship with it already ticked.
  3. Enter Play Mode. Islands are built on the first fixed step from the same meshFilters list RCCP_Damage already collected, so there is nothing to assign by hand. Nothing is simulated in edit mode -- a freshly added solver showing no islands is the normal state, not a failure.
  4. The inspector's per-mesh island report then lists each mesh with its particle and constraint counts, and the Play Mode readouts show how many islands are awake, the peak particle velocity, and the last impact energy.

Soft-Body Deformation owns the component's lifecycle, not the behavior. Nothing at runtime branches on the checkbox -- mesh deformation is routed to whichever live solver exists, so adding RCCP_DeformationSolver by hand from Add Component > BoneCracker Games > Realistic Car Controller Pro > Addons > RCCP Deformation Solver still works and still takes over with the checkbox left off. A hand-added solver may sit on the vehicle root or on any child; it registers itself with the RCCP_CarController either way and is reachable afterwards as carController.DeformationSolver. What the checkbox adds is a one-click create/destroy and a serialized record of the intent, so the object is re-created if it is deleted while the box stays ticked.

The same Read/Write Enabled import requirement as legacy deformation applies -- the solver builds from the same meshes, and non-readable ones are skipped the same way.

Body Stiffness Presets

stiffnessPreset is the knob most projects will use. Soft, Standard and Stiff each write the five Material Feel values below; Custom leaves whatever you have hand-authored alone.

PresetCharacterDent depthvs the classic vertex dent
SoftThin-panel feel -- deep, wide dents from moderate hits0.364 m~1.8x deeper
StandardThe shipped default0.212 m~1.0x -- matches
StiffHeavy/reinforced panels -- shallow, tight creases0.097 m~0.5x

Standard is the calibration anchor. impactEnergyScale is tuned so that at Standard, with RCCP_Damage.deformationMultiplier at 1, a soft-body dent comes out as deep as the dent the classic per-vertex path would have made for the same impact -- within 6% across impulses on the demo E46. That is deliberate: switching softBodyDeformation on changes how a dent behaves (it settles, it creases, it can be pulled back out) without changing how deep your existing crashes read. Soft and Stiff then bracket that baseline rather than being miscalibrated against it.

Those depths are one impact at collision impulse 2 on the demo E46's Body panel, hit from above at the front. Treat them as ratios, not absolutes -- a different vehicle, mesh density, impact angle or impulse will land elsewhere, and the ratios themselves drift with impulse because plasticity has a yield threshold (Stiff sits nearer 0.42x of the classic depth on a light hit and nearer 0.56x on a heavy one).

Changing the preset from a script must go through ApplyStiffnessPreset(BodyStiffnessPreset), which sets the selection and writes the five values:


RCCP_DeformationSolver solver = vehicle.DeformationSolver;

// Correct: changes the selection AND the five compliance/plasticity values.
solver.ApplyStiffnessPreset(RCCP_DeformationSolver.BodyStiffnessPreset.Soft);

// Wrong: changes the label only. The body still behaves exactly as before.
// solver.stiffnessPreset = RCCP_DeformationSolver.BodyStiffnessPreset.Soft;

Assigning stiffnessPreset on its own only renames the selection -- the solve reads the five underlying fields, and nothing writes them on a plain field assignment. The component logs a warning if it notices a selected preset whose values were never applied.

Solver Configuration

Simulation

PropertyTypeDefaultDescription
solverIterationsint, 2-126XPBD constraint iterations per substep. Higher settles constraints more accurately at a higher CPU cost.
substepsint, 1-31Physics substeps per fixed update. Raise it if very fast impacts look unstable.
velocityDampingfloat, 0.85-0.9990.94Per-substep velocity multiplier. Lower settles faster but looks less springy.
settleTimefloat0.5Seconds of low particle velocity before an island is considered settled and allowed to sleep.
maxAwakeTimefloat3Hard cap on how long an island may stay awake after an impact, regardless of velocity. A stability backstop, not a tuning knob.
normalRecalcIntervalint, min 13Recalculate an awake island's mesh normals every Nth solve step instead of every step. 1 = every step (highest quality, highest cost). Staggered per island. Ignored entirely when RCCP_Damage.recalculateNormals is off. See Performance and Platform Notes.

Material Feel

PropertyTypeDefaultDescription
stiffnessPresetenum (Soft, Standard, Stiff, Custom)StandardNamed preset for the five values below. Change it from script via ApplyStiffnessPreset(), not by assigning the field.
anchorCompliancefloat2e-3Inverse stiffness of the anchor constraint holding each particle to its rest pose. Lower is stiffer. The dominant term.
stretchCompliancefloat1e-6Inverse stiffness of the distance constraints between neighboring particles.
bendCompliancefloat1e-4Inverse stiffness of the bend constraints. Lower resists folding and creasing more.
plasticYieldfloat, meters0.002Displacement beyond which a constraint's rest length permanently yields instead of springing back. This is what makes a dent stick.
plasticRatefloat, 0-10.9Fraction of the over-yield displacement baked into the rest pose per solve. Higher deforms permanently faster.

Impact

PropertyTypeDefaultDescription
impactRadiusfloat, meters-1Influence radius of an impact. -1 means auto: RCCP_Damage.deformationRadius is used, resolved at build time.
impactEnergyScalefloat1.75Multiplier applied to collision energy before it is injected into the solver. The main "how hard do crashes hit" dial. Calibrated so a soft-body dent is as deep as the classic vertex dent at deformationMultiplier 1 -- within 6% from impulse 1 upward on the demo E46 -- so flipping soft-body on or off changes how a dent behaves without changing how deep it is.

Build

PropertyTypeDefaultDescription
weldEpsilonfloat, meters1e-4Vertices closer together than this are welded into one particle. This is what lets split-vertex meshes (UV seams, smoothing splits) deform without tearing.
maxParticlesPerMeshint6000Hard cap on particles per mesh; denser meshes are downsampled to it. See the platform notes before lowering this -- it costs dent fidelity without reliably buying step time.
asyncIslandBuildbooltrueRuns the expensive managed topology phase on a bounded worker pool. Mesh reads/snapshots and native-buffer finalization remain on Unity's main thread. WebGL players and single-core devices fall back automatically.
islandsPerBuildStepint, 0-322Maximum ready blueprints finalized per fixed step. On the synchronous fallback it limits complete mesh builds instead. 0 drains all ready work, but async mode never waits for an unfinished worker. The scene-wide build budget still caps the total across vehicles.

Repair

PropertyTypeDefaultDescription
instantRepairboolfalseSkip the animated spring-back and restore the rest pose in a single frame, matching legacy repair timing exactly.
repairSettleTimeoutfloat, seconds6Wall-clock safety net (unscaled) before a repair forces itself to complete. A healthy spring-back settles long before this; raise it only if heavy vehicles are being cut short.

Repair and Spring-Back

RCCP.Repair() and every other repair entry point behave exactly as before -- you set the same flag, and wheels, parts, lights and zone health are restored on the same first pass they always were. What changes is that the mesh part of the repair becomes animated: the anchors are moved to the authored pose and the panels are pulled back to it over roughly a second of game time rather than snapping.

Three properties of that are worth knowing:

Set instantRepair = true if you have code that assumes the old same-frame restore.

Performance and Platform Notes

A settled vehicle costs nothing. An island only simulates while it is awake, and the solve loop skips sleeping islands before doing any work at all -- the profiler marker never even opens. Parked, cruising and undamaged vehicles are free; this is a per-impact cost, not a per-frame one.

The island build is the one cost you pay without crashing. Islands are built once after the solver goes live. The old synchronous path measured ~174 ms of total build work on the demo E46 (173.8 and 175.0 across two rebuilds, editor, Burst on), and one high-poly mesh could still put most of that into a single fixed step however low islandsPerBuildStep was. With asyncIslandBuild on (the default), mesh topology and vertex arrays are snapshotted on Unity's main thread, the expensive weld / decimate / constraint / graph-color phase runs on a shared background pool capped at four workers, and only ready native-buffer finalization returns to the main thread. That topology phase was more than 90% of the measured build cost. The vehicle never waits for an unfinished worker.

islandsPerBuildStep now meters ready finalizations on that async route; 0 drains everything already ready but still does not wait. RCCP_Settings's Island Build Budget applies on top and caps finalizations across all vehicles. Until the last island publishes, RCCP_Damage keeps using classic deformation, and a collision, load or repair landing during the build is re-seeded into the finished islands before ownership changes hands. On WebGL, a single-core device, or when asyncIslandBuild is off, RCCP uses the previous synchronous incremental route. That fallback is still useful behind a loading screen; lowering islandsPerBuildStep spreads it between meshes but cannot split one especially heavy mesh.

The async path removes the topology stall, not the work or memory. Background construction still consumes CPU and temporary managed snapshots, and the completed islands retain their persistent native memory for the vehicle's lifetime. Roughly half the footprint on the E46 goes to geometry that never visibly dents -- the engine-bay mesh, the police siren, and 16 inactive customization spoiler variants -- so a vehicle whose RCCP_Damage.meshFilters list is authored by hand (with automaticInstallation off) rather than collected automatically builds proportionally faster and smaller. Note that the same list drives legacy deformation, so narrowing it narrows both paths.

While an island is awake the work is the XPBD solve, the mesh write-back (SetVertices), and -- when RCCP_Damage.recalculateNormals is on -- the normal recalculation.

Measured, desktop only. The figures below are one machine, one vehicle: the demo E46 in the Unity editor at shipped defaults, on a realistic 4-panel front impact with RCCP_Damage.recalculateNormals on -- which is how the demo vehicles ship it. That precondition matters if you reproduce these numbers: a vehicle whose recalculateNormals is switched off lands on the bottom row of the table instead, near 0.867 ms rather than 1.383. The prototype vehicle in core RCCP (Prefabs/Prototype/) is exactly such a vehicle -- it serializes the toggle off, even though the field's own default is on. Read all of this as a shape, not a guarantee.

Readingms per fixed step
Asleep0.0006 -- structurally zero work
Awake, averaged across one normals-recalculation cycle1.383
Awake, the single most expensive step of that cycle2.198
Awake, with RCCP_Damage.recalculateNormals off0.867

Both awake readings are real and they answer different questions. If you care about the average frame cost of a crash, the number is 1.383 ms. If you care about the worst single step -- because you are budgeting against a hard frame ceiling -- the number is 2.198 ms.

The normal recalculation, not the solve, is the expensive part. The solve runs on Unity's C# Job System, one job per island, with zero managed allocation per step; on the E46 that took it from 37.009 ms to 2.825 ms per step, a factor of 13.1x. What it did not speed up is the mesh apply: recalculating normals then measured 63% of the whole step (1.492 ms of 2.359 ms), against roughly 0.65 ms for the solve itself. normalRecalcInterval exists for exactly that inversion -- each awake island recalculates its normals every Nth solve step rather than every step, staggered so different islands fire on different steps. An island always performs one exact recalculation when it settles, bakes, freezes on a part detach, or is baked by an LOD transition, so a resting dent is always lit correctly. Only mid-settle shading is approximated.

Raising normalRecalcInterval lowers the average, never the peak. The most expensive step of any cycle is the one the heaviest island lands on, and every island must recalculate on some step. On the E46 that peak is the no-normals floor plus the heaviest island's normals cost -- 0.842 + 1.356 = 2.198 ms -- and no value of N moves it. A larger N makes that step rarer, not cheaper. If what you need is a hard per-step ceiling rather than a better average, the knob that actually works is turning RCCP_Damage.recalculateNormals off, which takes the same case to 0.867 ms with no code change, at the cost of dents no longer catching the light.

Two knobs that look like performance levers and measurably are not. Both were tried on desktop and both were rejected on the data:

Mobile has not been measured. No device test was run and the figures above are desktop; do not infer a mobile number from them, and do not read the presence of this feature as a claim that it runs well on phones. If you are budgeting for a mobile title, budget the normal recalculation and the per-island mesh apply rather than the XPBD solve -- once the solve moved onto the job system it is the small part. The effective levers in order are: leave the solver off entirely on vehicles that do not need it (legacy deformation remains the default everywhere), turn RCCP_Damage.recalculateNormals off, and raise normalRecalcInterval. Measure on your target device before committing to it.

Burst is optional. When com.unity.burst resolves in the project, RCCP sets the BCG_RCCP_BURST scripting symbol automatically and the solver's jobs compile to native code. Without it, the same jobs run as plain IL on the job system -- everything still works, just slower; roughly 1.9x of the 13.1x above came from the jobs alone and the remainder from Burst. Nothing needs to be installed and no package dependency is added: Burst very often arrives transitively (Universal RP pulls it in), so many projects already have it without ever having asked for it.

Scene-Level Deformation Budget

One soft-body vehicle is cheap. Several crashing at once are not -- the costs in the previous section are per vehicle, and a four-car pile-up pays all four at the same time, in the same fixed step. The deformation budget is the scene-level throttle that stops that from compounding into a frame spike.

It works on measured cost, not on vehicle count -- deliberately, because vehicle count does not predict cost at all. Twenty parked soft-body cars cost essentially nothing (their islands are asleep and skipped before any work happens), while three cars in a pile-up cost real milliseconds. Once per physics step the budget prices every registered solver, ranks them by what the player can actually see -- on screen first, then distance to the camera, then how recently each was hit -- and grants each the highest quality tier that still fits. It is enabled by default, and on a normal scene with one to three vehicles it never fires; most projects will never know it is there.

Your vehicle is never degraded. The active player vehicle is exempt unconditionally, not merely ranked first. If it alone exceeds the whole budget, that is the correct outcome and the budget accepts it: the car the player is looking at stays at full quality and everything else gives way around it. A vehicle in the middle of a repair spring-back is protected too -- it may soften, but it is never dropped further, so a repair in flight can never be stranded.

The four tiers, and what each looks like:

TierWhat the player seesWhat it costs
FullEverything described in this section -- panels flex, overshoot, crease and settle.The measured awake cost above.
SoftenedVisually near-identical. Only the normal-recalculation cadence is throttled, so shading during the half-second a dent is still moving is slightly approximated. A dent that has come to rest is always lit exactly right.Roughly a third of Full.
LegacyThe vehicle falls back to classic per-vertex denting -- dents still appear, in the same place, but they appear instantly instead of settling, and they stop creasing. Depth is close to unchanged on the Standard preset, which is the one calibrated against the classic path; on Soft -- which is what the Demo Content vehicles ship -- the classic dent is roughly half as deep, so the step down is more noticeable there.Effectively zero per step.
DormantIdentical to Legacy on screen. The difference is invisible: the vehicle's solver data has been released.Zero, and the memory comes back.

Transitions are seamless in both directions because both paths write the same vertex arrays -- there is no snap or pop when a vehicle steps down or back up. Stepping up out of Legacy costs about 10 ms once (measured 8.6-9.3 ms on the demo E46); stepping up out of Dormant is the expensive one, because the islands have to be rebuilt from scratch. That is why Dormant is driven only by the memory cap and never by CPU load -- a transient frame spike must never cost a vehicle its islands.

Because that step-up cost is real and paid all at once, recovery is staged: at most one vehicle leaves Legacy per physics step. This matters more than it sounds, because a pile-up demoted together also recovers together -- stepping a vehicle down puts its panels to sleep, so the whole group settles and becomes eligible on the same step. Without the throttle they would all pay their step-up in that one step; measured on nine vehicles that is roughly 81 ms, against a 20 ms step at the default 50 Hz. Staged, the same nine recover over nine physics steps -- under a fifth of a second -- highest visual priority first, and nothing is starved because a vehicle held back keeps its place in the queue.

Settings live on RCCP_Settings (Tools > BoneCracker Games > RCCP > Settings, under Deformation Budget) rather than per scene, so a project authors them once:

SettingDefaultDescription
Enable Deformation BudgettrueMaster switch. Off, every soft-body vehicle runs at full quality with no scene-level limit.
Solve Budget (ms)10Combined per-physics-step allowance for soft-body deformation across every vehicle. Raise it if you would rather drop frames than quality; lower it to hold a tighter frame ceiling.
Island Build Budget (Per Step)4Maximum ready islands finalized per physics step across all vehicles combined. The solver's own islandsPerBuildStep is per vehicle; this global cap composes across simultaneous spawns. 0 disables the global finalization cap. It does not limit background topology workers, which have their own shared cap. This is independent of the tiers and never degrades anything.
Memory Cap (MB)96Total soft-body memory allowance. See the limitation below. 0 disables the cap.

Runtime overrides. A garage scene and a twenty-car pile-up want different answers, and neither should have to edit a shipped asset to get one, so both budgets can be pinned at runtime. Pass a negative value to clear an override and fall back to RCCP_Settings:


RCCP_DeformationBudget.SetBudgetOverride(20f);      // this scene gets 20 ms per step
RCCP_DeformationBudget.SetMemoryCapOverride(256f);  // ...and 256 MB of solver memory
RCCP_DeformationBudget.SetBudgetOverride(-1f);      // back to the RCCP_Settings value

The same static class exposes the whole live state read-only, so a HUD or a telemetry overlay can show it: RegisteredCount, AwakeIslandCount, AwakeParticleCount, LastMeasuredSolveMs, EffectiveBudgetMs, TotalIslandBytes, MemoryCapBytes, TierCount(tier), and GetTier(solver) / GetTierReason(solver) for a single vehicle.

Where to see the status:

Known Limitation: the memory cap is not a hard ceiling

The memory cap only releases solver data from vehicles the budget has already stepped down to Legacy under CPU load, and that are off screen. It is not a general memory limiter, and it will not enforce itself on an idle scene.

The consequence is worth stating plainly, because it is the opposite of what a number labelled "cap" suggests: a scene holding many parked or distant soft-body vehicles can sit above the cap indefinitely and release nothing. Nothing in that scene is under CPU pressure, so nothing was ever demoted, so nothing is eligible. RCCP_Lod pushes in the same unhelpful direction -- it disables RCCP_Damage past its far distance, which puts the islands to sleep, and a sleeping vehicle climbs back out of Legacy. The far-away vehicles, the cheapest to release and the largest holders of memory, are the least likely to stay eligible.

This is a deliberate trade, not an oversight. Widening the rule to release any settled off-screen vehicle would make the cap bind on an idle fleet -- but it would also mean a vehicle that gets hit again while off screen pays a full island rebuild, a pause of roughly the magnitude quoted in the previous section, in a scene that was never struggling in the first place. The rule chosen instead guarantees that no vehicle ever loses its islands unless the scene was already under load, so there is never a surprise rebuild. The RCCP_SceneManager inspector says so in place when the condition is live.

Two practical consequences:

If the cap is being exceeded and it matters, the effective levers are lowering maxParticlesPerMesh, narrowing RCCP_Damage.meshFilters (with automaticInstallation off) so fewer meshes become islands, or simply not enabling the solver on vehicles that do not need it.

Measured behavior

As with every figure in this section, these are measurements on one named configuration, not guarantees: Unity 6000.0.49f1, editor Play Mode, Burst on, on a 32-thread desktop (Intel Core i9-13980HX / RTX 4090 Laptop / Windows 11). Your numbers will differ, which is precisely why the budget calibrates its cost model at runtime rather than trusting a constant.

CaseMeasured
Demo M3_E46, full-body dent settling (16,279 awake particles)2.5 - 3.9 ms per fixed step, 6.0 MB of islands
Demo M5_E30, full-body dent settling (89,000+ awake particles)7.7 - 9.1 ms per fixed step, 26.4 MB of islands
All five crash-camera demo scenes, driven into a wall at 16-28 m/sNo vehicle degraded below Full; the scene stays within budget
Heaviest demo scene (the car-selection line-up, 15 vehicles resident)74.8 MB against the 96 MB cap; nothing reaches Dormant

The M5_E30 row is the interesting one and the reason the exemption exists: that single vehicle can approach the whole default 10 ms budget on its own. Driven as the player it is exempt, so it simply runs at full quality and the scene reports itself as within budget with a note -- which is the intended outcome, not a warning.

The cost model is fitted at runtime, and it is seeded conservatively. Cost is modelled as a fixed per-vehicle term plus a marginal per-awake-particle term, because a single per-particle constant misprices by a factor of four across the real range of vehicle sizes. Both terms are fitted online from the solver's own measured step times, with outlier rejection, so a slower machine or a Burst-less project converges on its own true cost instead of a desktop constant. Until enough samples with enough spread have arrived, the budget holds a deliberately pessimistic seed -- it over-estimates rather than under-estimates, so the first crash of a session is protected rather than optimistic.

Mobile has not been measured. No device test was run for the budget any more than for the solver itself. The default of 10 ms per physics step is a desktop policy number: it is half of a 20 ms fixed step handed to one subsystem, which is defensible on a desktop and probably is not on a phone. If you ship soft-body deformation to mobile, set this from your own measurements.

Compatibility and Limitations

Feature Lab

The Feature Lab's Damage category (ships with the Demo Content addon) carries seven live solver entries, so you can evaluate it without writing any code:

EntryWhat it does
Soft-Body DeformationTurns the solver on and off on the active vehicle mid-drive, creating it on first use. Off by default, exactly as it ships.
Body StiffnessCycles Soft / Standard / Stiff / Custom through ApplyStiffnessPreset().
Dent Front Panel (Test)Injects a fixed impact into the front of the vehicle so you can watch a dent form and settle without crashing.
Dent Rear Panel (Test)The same test impact aimed at the rear face -- useful for checking boot lid and rear quarter behavior.
Dent Left Panel (Test)The same test impact aimed at the left flank, at mid height, so it lands on the door / quarter panel.
Dent Right Panel (Test)The same test impact aimed at the right flank.
Soft-Body StateLive readout: how many islands are still moving, out of how many were built, and how many simulation particles they carry.

Seeing it work. The solve completes in roughly half a second, which at normal speed is too fast to read. The Crash Cinematic exists specifically to show it -- it slows time and frames the impact so the flex, overshoot and permanent crease are all visible.

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

  1. The part must be a separate child GameObject of the vehicle with its own Rigidbody.
  2. A ConfigurableJoint is required (automatically created when adding the component via Reset()).
  3. The part's GameObject and children must be on the RCCP_DetachablePart layer.
  4. The joint's connectedBody should reference the vehicle's main Rigidbody.
  5. BodyTilt is optional. If the vehicle has an RCCP_BodyTilt addon, the part automatically follows it via a ParentConstraint. 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 in Awake).

Part Types

The DetachablePartType enum identifies the role of each part:

Part TypeDamage MultiplierDescription
Bumper_F1.5xFront bumper -- takes the most damage
Bumper_R1.5xRear bumper -- takes the most damage
Trunk1.2xTrunk lid
Hood1.0xEngine hood
Other1.0xAny other body panel
Door0.8xDoors -- 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:

  1. Locked -- The ConfigurableJoint motions are locked. The part is rigidly attached to the vehicle.
  2. Loose (strength <= loosePoint) -- Joint motions are unlocked to their original settings. The part wobbles and can flap in the wind (controlled by addTorqueAfterLoose, applied in FixedUpdate so the effect is framerate-independent).
  3. Detached (strength <= detachPoint) -- The part breaks free from the vehicle, becomes an independent physics object, fires OnRCCPPartDetached, and is deactivated after deactiveAfterSeconds.

Configuration

PropertyTypeDefaultDescription
partTypeDetachablePartTypeHoodIdentifies this part's role for damage weighting.
strengthfloat100Current durability. Decreases on each collision.
lockAtStartbooltrueLock the ConfigurableJoint motions at startup so the part stays firmly attached.
isDetachablebooltrueWhether this part can fully detach. If false, the part can become loose but never falls off.
loosePointint50Strength threshold below which the part becomes loose (joint unlocks).
detachPointint0Strength threshold below which the part fully detaches from the vehicle.
deactiveAfterSecondsfloat5.0Seconds after detachment before the part's GameObject is deactivated.
addTorqueAfterLooseVector3(0,0,0)Torque applied in local space (in FixedUpdate) when the part is loose, scaled by vehicle speed. Creates a flapping effect.
useDamageWeightingbooltrueApply the part-type-based damage multiplier.
onDamagedUnityEvent<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.
COMTransformAuto-createdOptional center of mass override for the part's Rigidbody.
Broken (read-only)boolfalsePublic 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:

PropertyTypeDefaultDescription
partDamagebooltrueMaster toggle for detachable part damage.
partDamageRadiusfloat1.0Radius around the contact point in which parts are checked for damage.
partDamageMultiplierfloat1.0Global 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

PropertyTypeDefaultDescription
wheelDamagebooltrueMaster toggle for wheel damage.
wheelDamageRadiusfloat2.0Radius around the contact point within which wheels are affected.
wheelDamageMultiplierfloat1.0Scales the amount of wheel displacement.
wheelDetachmentbooltrueWhen 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:

PropertyTypeDefaultDescription
isBreakablebooltrueWhether this light can be broken by collisions. Honored since V2.51 -- setting it false genuinely prevents breakage.
strengthfloat100Current durability. Reduced by the light's computed damage * 20 on each nearby collision (see Tuning Reference).
breakPointint35Strength threshold below which the light is considered broken.
brokenboolfalseRead at runtime to check if the light is broken.

RCCP_Damage Light Settings

PropertyTypeDefaultDescription
lightDamagebooltrueMaster toggle for light damage.
lightDamageRadiusfloat0.75Radius around the contact point within which lights are checked.
lightDamageMultiplierfloat1.0Scales 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.

The on/off switch is RCCP_Damage.mechanicalDamage, not the component itself. That flag defaults to off, so vehicles -- including any upgraded from an earlier RCCP version -- keep purely cosmetic damage until you opt in. Ticking it creates the RCCP_DamageMechanics child object for you (in the editor and at runtime); unticking it removes the object again. Deleting the object by hand while the flag is still ticked does not stick, because RCCP_Damage re-creates it.

Changed in V2.63: mechanicalDamage previously defaulted to true, which silently gave every vehicle mechanical consequences on upgrade. It now defaults to false.

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

PropertyTypeDefaultDescription
engineDamagebooltrueMaster toggle. Reduces engine torque as the engine zone takes damage.
engineZoneRCCP_DamageZoneFrontWhich zone hosts the engine. Flip to Rear for rear-engine layouts.
healthToTorqueCurveAnimationCurveLinear (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.
overheatHealthThresholdfloat, 0-10.5Engine-zone health below which the radiator is considered damaged and heat starts building.
overheatBuildTimefloat, min 160Seconds until heat reaches maximum, measured at the reference RPM (where rpmToHeatRateCurve reads 1).
rpmToHeatRateCurveAnimationCurve(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.
overheatCoolTimefloat, min 145Seconds to shed all heat while the engine is off or the radiator is intact again. Independent of build rate.
overheatTorqueFadefloat, 0-10.3Additional 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

PropertyTypeDefaultDescription
steeringMisalignmentbooltrueMaster toggle. Bends steering alignment (toe error) as wheels take collision damage -- the car pulls to one side.
maxToeErrorfloat, 0-103Maximum toe error in degrees at full wheel displacement.
offsetToToeCurveAnimationCurveLinear (0,0) -> (1,1)Toe error over normalized wheel displacement (offset / RCCP_Damage.maximumDamage), as a fraction of maxToeError.

Applied to all wheels, steered or not -- a bent rear wheel makes the car crab slightly, which is correct behavior. Steered wheels take the bias through RCCP_WheelCollider.ApplySteering() on top of the Ackermann angle; non-steered wheels take it in RCCP_WheelCollider's own FixedUpdate (execution order 0, after RCCP_Axle at -2, so a steered wheel is never written twice). Changed in V2.63: the bias was previously written to every wheel but only consumed on steering axles, so rear-wheel damage had no effect at all.

Both the size and the direction of the toe error come from the lateral (local X) part of the wheel's accumulated displacement: the angle is scaled by lateralOffset / totalOffset, so a wheel shoved toward +X toes right, toward -X toes left, and a wheel shoved straight backwards by a head-on impact barely toes at all. Changed in V2.63: the magnitude previously came from the total 3-D displacement while only the sign came from the lateral axis, so a head-on impact -- where the lateral component is numerical noise -- produced a full-strength pull in an essentially arbitrary direction.

Staged Deflation

PropertyTypeDefaultDescription
stagedDeflationbooltrueMaster toggle. Heavily damaged wheels develop a slow leak instead of staying magically inflated.
leakThresholdfloat, 0-10.5Wheel displacement (as a fraction of RCCP_Damage.maximumDamage) that triggers a slow leak.
slowLeakDurationfloat, min 010Seconds for a slow leak to fully deflate the tire, via RCCP_WheelCollider.Deflate(duration).
wobbleAmplitudefloat, 0-101.5Per-revolution visual wobble amplitude (degrees) on deflated wheel models, driven by deflationWobble.
deflatedBrakeDragfloat, min 050Rolling 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. Faded out below ~5 km/h (by absoluteSpeed / 5, unsigned so reverse is gated identically), so a flat tire behaves as rolling resistance and never pins the car at a standstill.

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.

The rising edge is tracked per wheel and only re-arms once that wheel's displacement drops back under leakThreshold, which in practice means a repair. That is what makes a manual RCCP_WheelCollider.Inflate() -- the Feature Lab's Inflate Tires action, a gameplay tire-change, your own script -- actually stick on a still-bent wheel. Changed in V2.63: the trigger used to be a level test, so re-inflating a damaged wheel was undone on the next FixedUpdate.

Fire -> Engine Death

PropertyTypeDefaultDescription
fireDamagebooltrueMaster toggle. Critical engine-zone damage ignites a fire that kills the engine unless repaired.
fireHealthThresholdfloat, 0-10.15Engine-zone health at or below which the fire can ignite.
fireExtinguishHealthThresholdfloat, 0-10.35Engine-zone health the fire must be pulled back above before it self-extinguishes. Keep it above fireHealthThreshold; the gap is the hysteresis band.
fireIgnitionDelayfloat, min 03Seconds of sustained critical damage before the fire actually ignites.
fireToEngineDeathTimefloat, min 120Seconds 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).

Extinguishing is health-driven, in both directions. Repair is still the normal way out, and because collision damage only ever lowers zone health, a burning car in ordinary gameplay stays burning until it is repaired. But the fire also goes out on its own -- restarting a fire-killed engine -- whenever engine-zone health rises back above fireExtinguishHealthThreshold by any route, including RCCP_Damage.Load() and any direct zoneHealth write from your own repair-kit or checkpoint logic. Changed in V2.63: the burning state used to ignore health entirely once lit, so loading a clean save left a mechanically pristine vehicle on fire forever.

Engine death waits for a running engine. The kill is a one-shot per fire, but it is only spent when it actually stops a running engine. If the fire burns through while the ignition is off, the countdown stays armed and kills the engine the moment it is started again. Changed in V2.63: the one-shot used to be consumed even when there was no engine to kill, which made a burning car permanently immune to its own fire after a manual restart.

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 the fire extinguishing, 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

PropertyTypeDefaultDescription
recomputeInertiaOnDetachboolfalseWhen 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.

The RCCP_DamageMechanics inspector in Play Mode — grouped system toggles plus the live Runtime readout block (per-zone health, Engine Heat, Fire status)

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):

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:

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()):

TriggerPrefab slotBehavior
Tire deflates (rising edge of RCCP_WheelCollider.deflated)blowoutBurstPrefabPlays 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.
OnRCCPPartDetacheddetachDebrisPrefabPlays at the detached part's position.
OnRCCPWheelDetacheddetachDebrisPrefab (same pooled instance as part detach)Plays at the wheel's position.
OnRCCPLightBrokenglassShardPrefabPlays at the light's position.

Scrape Ramp

Long, continuous body-grinds (OnCollisionStay) now ramp up over time instead of staying at a constant rate:

PropertyTypeDefaultDescription
scrapeRampDelayfloat, min 01 (second)How long a scrape must continue before the ramp starts.
scrapeRampMaxMultiplierfloat, min 13Maximum 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 fieldRCCP_Settings source fieldUsed for
damageSmokePrefabdamageSmokeParticlesLooping engine-bay smoke.
damageFirePrefabdamageFireParticlesLooping fire (requires RCCP_DamageMechanics).
blowoutBurstPrefabblowoutParticlesOne-shot tire blowout burst.
detachDebrisPrefabdetachDebrisParticlesOne-shot part/wheel detach debris burst.
glassShardPrefabglassShardParticlesOne-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

FieldDefaultMeaning
displayModeShowOnCollisionAlwaysOn keeps the HUD at full alpha permanently. ShowOnCollision fades it in on an event and back out afterwards.
visibilityDuration5Seconds the HUD stays up after a triggering event (ShowOnCollision only).
fadeSpeed3Alpha units per second for the fade in/out.
minCollisionForce2.5Minimum collision.relativeVelocity.magnitude that counts as a HUD-triggering impact.
holdWhileCriticaltrueKeeps the HUD visible for as long as the vehicle stays in a critical state, ignoring the visibility timer (ShowOnCollision only).
criticalHeatThreshold0.5Normalized 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:

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.

SubsystemFormulaFalloff shape
Mesh deformationdisplacement = direction * (normalizedImpulse * (1 - clamp01(dist / deformationRadius))) * (deformationMultiplier / 10)Linear, 100% at the contact point down to 0% at the radius edge.
Wheel displacementdisplacement = (normalizedImpulse * wheelDamageMultiplier / 30) * (1 - 0.5 * clamp01(distSqr / radiusSqr))Squared-distance, up to 50% reduction at the radius edge.
Part strengthdamageToApply = (normalizedImpulse * partDamageMultiplier * (1 - 0.5 * squaredFalloff)) * 5 * typeMultiplierSquared-distance, up to 50% reduction. typeMultiplier is the Part Types table above (0.8x-1.5x).
Light strengthstrengthLoss = (normalizedImpulse * lightDamageMultiplier * (1 - 0.5 * squaredFalloff)) * 20Squared-distance, up to 50% reduction.
Zone healthzoneHealth -= 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:

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:

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:

  1. Meshes -- All deformed vertices are moved back to their original positions.
  2. 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 via Inflate().
  3. 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.
  4. Lights -- Each light's OnRepair() is called: strength is restored and broken is set to false.
  5. Zone health -- All four zones reset to 100 (V2.58).
  6. Mechanics state -- If RCCP_DamageMechanics is present, OnRCCPRepaired resets 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

Detachable parts not falling off

Repair not working

Wheel detachment not triggering

Damage Mechanics has no effect

Damage smoke/fire not appearing

Soft-body deformation has no effect

Performance concerns with deformation

Related Topics


Support: bonecrackergames@gmail.com | www.bonecrackergames.com

Need help? See Troubleshooting