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
- Physically-Simulated Deformation (XPBD)
- When to Use the Solver
- Adding the Solver
- Body Stiffness Presets
- Solver Configuration
- Repair and Spring-Back
- Performance and Platform Notes
- Scene-Level Deformation Budget
- Compatibility and Limitations
- Feature Lab
- 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
- Soft-body deformation has no effect
- 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. |
mechanicalDamage | bool | false | Opt-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. |
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.
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
- 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 | true | Recalculates 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. |
recalculateBounds | bool | true | Recalculates 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 shape | One direction per hit, linear falloff to the radius edge -- visibly conical | Follows the contact normal and the panel's own structure; edges crease, flat panels bow |
| Timing | Applied on the next frame and static from then on | Animates over a settle window, then sleeps |
| Repeated hits | Each hit stamps independently | Damage accumulates continuously across hits |
| Repair | Snaps back in a single frame | Animated spring-back (or single-frame with instantRepair) |
| Cost when nothing is happening | Zero | Effectively zero -- 0.0006 ms measured; settled islands are skipped before any work is done |
| Cost when the vehicle spawns | Zero | One-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 settling | Negligible | Measurable; see Performance and Platform Notes |
| Setup | Already active on every vehicle with RCCP_Damage | Add one component |
| Mobile | Long-shipping default | Not 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
- The vehicle needs an
RCCP_Damagecomponent with Mesh Deformation enabled. That toggle gates both the legacy path and the solver (see Compatibility and Limitations). - Tick Soft-Body Deformation, nested under Mesh Deformation on the
RCCP_Damageinspector. That creates anRCCP_DeformationSolverchild GameObject underRCCP_Damage, besideRCCP_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. - Enter Play Mode. Islands are built on the first fixed step from the same
meshFilterslistRCCP_Damagealready 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. - 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.
| Preset | Character | Dent depth | vs the classic vertex dent |
|---|---|---|---|
| Soft | Thin-panel feel -- deep, wide dents from moderate hits | 0.364 m | ~1.8x deeper |
| Standard | The shipped default | 0.212 m | ~1.0x -- matches |
| Stiff | Heavy/reinforced panels -- shallow, tight creases | 0.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
| Property | Type | Default | Description |
|---|---|---|---|
solverIterations | int, 2-12 | 6 | XPBD constraint iterations per substep. Higher settles constraints more accurately at a higher CPU cost. |
substeps | int, 1-3 | 1 | Physics substeps per fixed update. Raise it if very fast impacts look unstable. |
velocityDamping | float, 0.85-0.999 | 0.94 | Per-substep velocity multiplier. Lower settles faster but looks less springy. |
settleTime | float | 0.5 | Seconds of low particle velocity before an island is considered settled and allowed to sleep. |
maxAwakeTime | float | 3 | Hard cap on how long an island may stay awake after an impact, regardless of velocity. A stability backstop, not a tuning knob. |
normalRecalcInterval | int, min 1 | 3 | Recalculate 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
| Property | Type | Default | Description |
|---|---|---|---|
stiffnessPreset | enum (Soft, Standard, Stiff, Custom) | Standard | Named preset for the five values below. Change it from script via ApplyStiffnessPreset(), not by assigning the field. |
anchorCompliance | float | 2e-3 | Inverse stiffness of the anchor constraint holding each particle to its rest pose. Lower is stiffer. The dominant term. |
stretchCompliance | float | 1e-6 | Inverse stiffness of the distance constraints between neighboring particles. |
bendCompliance | float | 1e-4 | Inverse stiffness of the bend constraints. Lower resists folding and creasing more. |
plasticYield | float, meters | 0.002 | Displacement beyond which a constraint's rest length permanently yields instead of springing back. This is what makes a dent stick. |
plasticRate | float, 0-1 | 0.9 | Fraction of the over-yield displacement baked into the rest pose per solve. Higher deforms permanently faster. |
Impact
| Property | Type | Default | Description |
|---|---|---|---|
impactRadius | float, meters | -1 | Influence radius of an impact. -1 means auto: RCCP_Damage.deformationRadius is used, resolved at build time. |
impactEnergyScale | float | 1.75 | Multiplier 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
| Property | Type | Default | Description |
|---|---|---|---|
weldEpsilon | float, meters | 1e-4 | Vertices closer together than this are welded into one particle. This is what lets split-vertex meshes (UV seams, smoothing splits) deform without tearing. |
maxParticlesPerMesh | int | 6000 | Hard 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. |
asyncIslandBuild | bool | true | Runs 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. |
islandsPerBuildStep | int, 0-32 | 2 | Maximum 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
| Property | Type | Default | Description |
|---|---|---|---|
instantRepair | bool | false | Skip the animated spring-back and restore the rest pose in a single frame, matching legacy repair timing exactly. |
repairSettleTimeout | float, seconds | 6 | Wall-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:
- The repaired event still fires exactly once, when the spring-back actually completes -- not on the frame the repair was requested.
RCCP_Events.OnRCCPRepairedand the zone-health reset both wait for it. - A crash during a spring-back cancels it. The vehicle is left deformed, exactly as it was before the repair started; the repair does not quietly finish underneath the new damage.
repairSettleTimeoutruns on the unscaled clock, so a repair requested from a pause menu, a garage screen or photo mode (Time.timeScale = 0) still completes rather than hanging. Worst case it degrades from an animation to a snap.
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.
| Reading | ms per fixed step |
|---|---|
| Asleep | 0.0006 -- structurally zero work |
| Awake, averaged across one normals-recalculation cycle | 1.383 |
| Awake, the single most expensive step of that cycle | 2.198 |
Awake, with RCCP_Damage.recalculateNormals off | 0.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:
- Lowering
maxParticlesPerMeshfrom6000to2000left the step time flat and then made it worse (2.13 -> 2.42 ms), while costing about 17% of dent depth. It shrinks the solve, which was not the bottleneck, and does not touch the mesh apply, which is. - Lowering
solverIterationsfrom6to2bought about 16% of the step -- real, but small, and it is traded directly against constraint accuracy, which is what keeps hard impacts stable.
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:
| Tier | What the player sees | What it costs |
|---|---|---|
| Full | Everything described in this section -- panels flex, overshoot, crease and settle. | The measured awake cost above. |
| Softened | Visually 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. |
| Legacy | The 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. |
| Dormant | Identical 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:
| Setting | Default | Description |
|---|---|---|
| Enable Deformation Budget | true | Master switch. Off, every soft-body vehicle runs at full quality with no scene-level limit. |
| Solve Budget (ms) | 10 | Combined 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) | 4 | Maximum 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) | 96 | Total 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:
RCCP_SceneManagerinspector, in Play Mode -- the scene-wide banner (within budget / N of M vehicles degraded / over budget with nothing left to demote), plus live counts, measured milliseconds against the budget, the calibrated cost model and a tier histogram.RCCP_DeformationSolverinspector -- that one vehicle's Budget Tier, with the reason beside it, e.g.Softened (normals throttled) — rank 3 of 7, off screen.- Feature Lab, Systems category -- Deformation Budget (toggle), Deformation Budget (ms) (slider; drag it down to watch vehicles degrade live) and Budget Status (readout).
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:
- Budget your soft-body memory by vehicle count, not by the cap. Roughly 6-8 MB per built vehicle at the shipped mesh density is the figure to plan against; the cap is a backstop for the loaded case, not a promise about the parked one.
- The reported total only counts vehicles whose solver is currently enabled. A vehicle deactivated while keeping its solver data -- a car-selection line-up is the obvious case -- still holds its memory but does not appear in the total and cannot be released by the cap. If you keep a fleet of deactivated soft-body vehicles resident, account for them yourself.
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.
| Case | Measured |
|---|---|
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/s | No 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
- A project without the component is unaffected.
RCCP_DeformationSolveris on no prefab in core RCCP, andsoftBodyDeformationdefaults to off. Without it,RCCP_Damageruns exactly the code it ran before, and every vehicle you have already set up behaves identically. The 15 vehicles in the optional Demo Content addon are the deliberate exception -- they ship with it enabled so the demo scenes showcase it. RCCP_Damage.meshDeformationmust stay enabled. The solver receives impacts throughRCCP_Damage.DamageMesh(), which is only called while that toggle is on. With it off, the solver can be present, enabled and perfectly configured and still never receive a single impact. This is deliberate -- a live solver is mesh deformation, so a vehicle whose owner switched deformation off should not deform. To keep the case visible rather than silent, the component logs one warning, and the inspector shows the condition with a one-click Enable 'Mesh Deformation' On RCCP_Damage button.- Save data is unchanged and loads in both directions. The solver bakes its output into the same
damagedMeshDatathe legacy path writes, so theDamageDataformat described in Saving and Loading Damage is untouched. A save written with the solver loads on a vehicle without it, and a save written without it loads on a vehicle with it -- it is only vertex data either way. - Deformation is local-only in multiplayer, and always has been. Neither the shipped Photon PUN 2 integration nor the Mirror integration replicates damage of any kind: they synchronize transform, velocity, inputs, drivetrain state, lights and wheel RPMs (see Photon PUN 2 and Mirror). Every client dents its own copy of a vehicle from the collisions it simulates locally, so dents will not match across clients. This applies equally to the legacy deformation path -- the solver inherits the limitation rather than introducing it. If matching damage across clients matters to your game, you have to replicate it yourself.
- Body meshes only. The solver writes render meshes and nothing else. Chassis physics -- the rigidbody, the
WheelColliders, colliders, mass and inertia -- are completely unaffected. Glass shatter, cloth and antenna dynamics, particle-versus-world collision and cross-mesh seam stitching are all out of scope. - A detached part stops simulating. When a part detaches, its island bakes its current shape and freezes, so the panel keeps the dent it had at the moment it came off. Repairing the vehicle reattaches the part and unfreezes its island, and it springs back with the rest of the body.
- LOD is handled. When
RCCP_LoddisablesRCCP_Damageat distance, solver islands bake and sleep, so a vehicle that goes out of and back into range is consistent rather than mid-simulation. - The other three damage subsystems are untouched. Wheel damage, detachable parts, light breakage, zone health,
RCCP_DamageMechanics, the VFX pack and every damage event work identically with or without the solver -- it only changes how body vertices move.
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:
| Entry | What it does |
|---|---|
| Soft-Body Deformation | Turns the solver on and off on the active vehicle mid-drive, creating it on first use. Off by default, exactly as it ships. |
| Body Stiffness | Cycles 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 State | Live 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
- 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.
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 theRCCP_DamageMechanicschild 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, becauseRCCP_Damagere-creates it.Changed in V2.63:
mechanicalDamagepreviously defaulted totrue, which silently gave every vehicle mechanical consequences on upgrade. It now defaults tofalse.
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, 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
| 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 | Rolling 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
| 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. |
fireExtinguishHealthThreshold | float, 0-1 | 0.35 | Engine-zone health the fire must be pulled back above before it self-extinguishes. Keep it above fireHealthThreshold; the gap is the hysteresis band. |
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).
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
| 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
- Check that
RCCP_Damage.mechanicalDamageis ticked -- it defaults to off as of V2.63, and it is what creates the component in the first place. RCCP_DamageMechanicsrequires anRCCP_Damagecomponent on the same vehicle. Without one it logs a single warning, clears anything it had applied, and idles everyFixedUpdate.trackZoneHealthis required by engine damage and fire only; with it off those two see a permanently healthy engine zone and the component logs one warning naming them. Steering misalignment and staged deflation read wheel displacement and keep working regardless.- Steering misalignment and staged deflation both normalize wheel displacement against
RCCP_Damage.maximumDamage, so amaximumDamageof0switches both off (the component warns once). Set a positive value -- the default is0.75. - 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.
Soft-body deformation has no effect
RCCP_Damage.meshDeformationmust be enabled. It gates the solver exactly as it gates legacy denting, so with it off the solver never receives an impact at all. The component logs one warning, and its inspector offers a one-click Enable 'Mesh Deformation' On RCCP_Damage button.- Islands are built in Play Mode, on the first fixed step. An empty island report in edit mode is the normal state, not a failure.
- The meshes still need Read/Write Enabled -- the solver builds from the same
meshFiltersarray and skips non-readable meshes for the same reason the legacy path does. - If panels flex but the dent does not stay,
plasticYieldis above the displacement your impacts actually produce. RaiseimpactEnergyScale, or pick a softerstiffnessPreset. RCCP_Damage.deformationMultiplierscales the solver too, not just legacy denting -- at0nothing dents at all, which is the intended meaning of the Feature Lab's Deformation Strength slider. If a vehicle dents far less than the presets suggest, check that field before retuningimpactEnergyScale. Note that the solver is calibrated at multiplier 1, and because plasticity has a hard yield threshold the permanent dent is monotonic in the multiplier but not proportional to it -- it falls away faster than proportionally as the multiplier shrinks, because a shallower push spends more of itself inside the yield threshold. Measured on the demo E46 at impulse 2, Standard:0.50leaves 47% of the dent (not 50%),0.25leaves 20% (not 25%),0.10leaves 6% (not 10%), and below about0.05the permanent dent collapses toward nothing. Tune withimpactEnergyScaleandstiffnessPresetfor feel; usedeformationMultiplierfor per-vehicle scaling relative to the rest of your fleet.- If changing
stiffnessPresetfrom a script appears to do nothing, callApplyStiffnessPreset()instead -- a bare field assignment changes the label but not the five values the solve reads.
Performance concerns with deformation
- Reduce
deformationRadiusto limit the number of vertices processed per collision. - Disable
recalculateNormalsandrecalculateBoundsif they are not visually necessary. - Legacy 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 that path. The optional XPBD solver does run its solve on Unity's C# Job System; its cost profile is different and is documented under Performance and Platform Notes.
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