Advanced Suspension Maker SDK

Build custom, fully-animated suspension for CarX Drift Racing Online

Hub Arm Shock Axle

Get started Browse tutorials View on GitHub

Custom Suspension β€” Full Tutorial (Advanced Suspension Maker)

🐣 New to this? Read GETTING_STARTED.md first β€” it’s the simple, click-by-click version. This file is the deep reference (how it all works under the hood).

End-to-end guide to building a custom, articulated, per-car suspension for CarX Drift Online: study the basegame rig, author your own in the Kino content SDK, and have the mod drive it in-game.

Note (toolset trimmed). The active SDK tools are: KSusp Bone (+ custom Inspector), Build Rig Hierarchy, Attach Parts to Bones, Bake Suspension Descriptor. The Auto-Skin Suspension, Export Suspension Bundle, and Import Suspension Dump editor tools were removed (they belonged to the single-mesh / standalone-bundle / dump-study paths, which the current rigid-parts + chassis-embedded workflow doesn’t use). Sections below that mention them are kept for reference / git history. The in-game F9 dump and the mod’s dormant swap mode still exist; only the matching editor tools are gone.


0. The big picture β€” how the SDK half and the mod half align

There are two halves, and they connect through three shared artifacts (not through the SDK’s built-in part categories β€” see Β§9, there is no β€œSuspension” category):

  SDK / Unity (authoring + build)                 MOD / in-game (runtime)
  ───────────────────────────────                 ───────────────────────
  mesh + bone GameObjects                          GameBindings.GetWheelTransforms()
  + KSusp* components                              (reads car's live wheel data)
        β”‚                                                   β”‚
        β”‚ bake                                              β”‚
        β–Ό                                                   β–Ό
  β‘  <name>.suspension.json  ───────────────────▢  SuspensionAnimator (copy/aim/span)
        β”‚ build                                             β–²
        β–Ό                                                   β”‚ instantiates + drives
  β‘‘ <name>.bundle  ────────────────────────────▢  mod loads it + swaps stock suspension
        β”‚                                                   β”‚
        └──────── β‘’ bone NAME convention β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The three contracts that keep the halves in sync:

# Artifact Produced by (SDK) Consumed by (mod) The rule
β‘  <name>.suspension.json Bake Suspension Descriptor SuspensionAnimator.Load describes how to animate (copy/aim/span)
β‘‘ <name>.bundle Export Suspension Bundle mod AssetBundle.LoadFromFile + swap one prefab; mod loads & instantiates it
β‘’ bone GameObject names your prefab GameBindings.FindDeep names in β‘  must match the bones in β‘‘

So the mod never depends on the SDK directly β€” it depends on those three things. Change a bone name in Unity β†’ re-bake β‘  so the names still match. That’s the whole alignment.


1. Where everything lives

Kino content SDK (Unity project): …\kino_content_sdk (wherever you unpacked the Kino content SDK) (HDRP project. Note: OneDrive may mark some files read-only; β€œalways keep on this device”.)

Game: your Steam library, e.g. …\steamapps\common\CarX Drift Racing Online (Steam β–Έ right-click CarX β–Έ Manage β–Έ Browse local files, if you’re not sure which drive.)

This repo (mod source):

Deploy the SDK-side tools into the SDK project

Copy these into the SDK Unity project:

File Copy to Why
src/Authoring/SuspensionBoneComponents.cs Assets/AdvancedSuspensionMaker/ (NOT an Editor folder) runtime components you put on bones
src/Editor/KSuspensionDumpImporter.cs Assets/Editor/ rebuild a dump as a prefab to study
src/Editor/SuspensionDescriptorBaker.cs Assets/Editor/ bake artifact β‘ 
src/Editor/KSuspensionBundleExporter.cs Assets/Editor/ build artifact β‘‘

After copying, Unity compiles them and you get a Tools β–Έ KSL menu.


2. Prerequisites


3. Part 1 β€” Study the basegame suspension (mod side, already working)

  1. Load into a car in-game.
  2. Press F9 (or the menu button in the KSL panel) β†’ a dump is written to kino\dumps\<timestamp>\targets\Mesh_Suspension\.
  3. Study it:
    • Open geometry.glb in Blender (rig comes in posed/skinned), or
    • In the SDK: Tools β–Έ KSL β–Έ Import Suspension Dump β†’ pick the dump folder β†’ it rebuilds a prefab under Assets/KSL_DumpImports/ you can inspect.

The basegame rig (front MacPherson + rear multilink, 49 bones) is your reference for what a working rig looks like: Root β†’ Front/Rear β†’ FR/FL/RR/RL β†’ {Lower_Arm, Upper_Arm, Lower_ShA, Upper_ShA, SteeringRod, Wheel}.


4. Part 2 β€” Author your suspension in the SDK

  1. Create a folder, e.g. Assets/Content/CarParts/Examples/MySuspension/.
  2. Bring in / model your suspension mesh (one SkinnedMeshRenderer, name it Mesh_Suspension to match the basegame convention).
  3. Build a bone hierarchy of empty GameObjects as children under the mesh’s root. You need, per corner, the bones your design uses: a wheel-mount, control arms/links, a shock.
  4. Assign the material (see Β§2). Paint the colormask so the zones you want recolourable map to R/G/B.

Per-car: because the mod loads and swaps the suspension itself (suspension isn’t a selectable category and the game hardcodes it β€” see Β§9), you make one bundle per car and place each corner at that car’s geometry. The mod still auto-fits width/wheelbase live (the wheel bones follow the car’s wheel transforms), so you mostly need the inboard/chassis points right for each car.


5. Part 3 β€” Annotate the bones (component-based, no JSON by hand)

In the Inspector, add the KSusp* components:

Hub rotation modes (front vs rear)

The Hub’s Wheel Rotation picks how much of the game wheel’s orientation the upright copies. It mirrors the wheel β€” it never generates motion the wheel doesn’t have:

Mode Copies Use
SteerCamber steer + camber, no spin the right default for any upright (front and rear)
SteerOnly flat yaw only β€” no camber rare; only if camber misbehaves
Full steer + camber + spin only an actual spinning wheel mesh
None nothing (locked to authored pose) a fixed/decorative hub

SteerCamber on the rear β€” it will not steer left/right, because the game doesn’t steer the rear wheels, so there’s no steering input to copy. You still get camber + toe (static toe, plus dynamic toe / bump-steer through travel), which is exactly what a rear hub should do. SteerCamber doesn’t add fake steering β€” it only reflects what the real wheel does. So keep the rear on SteerCamber; the front swings because the front wheels are steered, the rear won’t.

Shocks (body + spring + tophat)

A coilover is three parts; put one KSuspShock on the assembly and wire:

Field Drag in Behaviour
tophat the fixed upper-mount bone NOT animated β€” stays at its authored chassis position. Do not parent it under the wheel bone.
lowerMount a locator on the moving side (child of wheel bone / lower arm) follows the wheel
bodyBone the rigid damper-tube bone sits at the lower mount, points up at the tophat (telescopes)
springBone the spring bone stretches between tophat and lower mount β†’ visual compression
axis the long axis of the body/spring meshes (default Z) Β 

The baker expands this into a rigid span (body) + a scaling span (spring). The tophat is β€œfixed based on its position in the car” simply by being authored there and left un-driven.


Grouping reference β€” what to put on each bone, per suspension type

Legend: ← Component means add that component; (child) means parent it under the line above so it inherits motion; (fixed) means a chassis bone with no component.

Diagrams (in docs/visuals/): MacPherson Β· double wishbone Β· multilink Β· cantilever.

Components unified. There is now ONE component β€” KSuspBone β€” with a Role dropdown. The trees below still show the old per-role component names; map them like this: KSuspWheelBone β†’ Role Hub Β· KSuspAimBone β†’ Role Arm Β· KSuspShock β†’ Role Shock Β· KSuspWheelChild β†’ Role RidesWithHub. KSuspCornerGroup is gone (corner is a dropdown on each KSuspBone). Marker names are free β€” just keep them unique.

MacPherson (strut rigid with the hub) β€” the strut rides and steers with the hub, so parent it under the wheel bone (no aim needed); only the spring compresses:

FR_Wheel                 ← KSuspWheelBone            (copy: travel + steer)
β”œβ”€β”€ FR_StrutBody         (child, no component)       rides + steers with the hub
β”‚   └── FR_LowerPerch    (child locator, no comp.)   spring lower seat (moves)
β”œβ”€β”€ FR_LowerArm_End      (child locator, no comp.)   lower-arm aim target
└── FR_Steer_End         (child locator, no comp.)   steering aim target
FR_Tophat                (fixed)                      strut top mount
FR_StrutSpring           ← KSuspShock                tophat=FR_Tophat, lowerMount=FR_LowerPerch,
                                                      springBone=FR_StrutSpring, bodyBone=EMPTY
FR_LowerArm              ← KSuspAimBone              target = FR_LowerArm_End
FR_SteeringRod           ← KSuspAimBone              target = FR_Steer_End

Double wishbone β€” the shock mounts to an arm (not the hub), so the shock body aims (use bodyBone); arms aim at ball-joint locators under the upright:

XX_Upright               ← KSuspWheelBone            (copy)
β”œβ”€β”€ XX_UpperBall         (child locator)             upper-arm aim target
β”œβ”€β”€ XX_LowerBall         (child locator)             lower-arm aim target
XX_UpperArm              ← KSuspAimBone              target = XX_UpperBall
XX_LowerArm              ← KSuspAimBone              target = XX_LowerBall
β”‚   └── XX_ShockLower    (child locator on lower arm) shock bottom mount
XX_Tophat                (fixed)                      shock top mount
XX_ShockBody             ← KSuspShock                tophat=XX_Tophat, lowerMount=XX_ShockLower,
                                                      bodyBone=XX_ShockBody, springBone=XX_Spring

Multilink β€” same as wishbone but N links, each its own aim:

RX_Upright               ← KSuspWheelBone            (copy)
β”œβ”€β”€ RX_Link1_End … RX_LinkN_End   (child locators)   one aim target per link
RX_Link1 … RX_LinkN      ← KSuspAimBone (each)       target = its *_End locator
RX_Tophat                (fixed)
RX_ShockBody             ← KSuspShock                top=RX_Tophat, lowerMount on the lower link

Cantilever / pushrod — pushrod drives a rocker; the shock spans rocker→chassis:

XX_Upright               ← KSuspWheelBone            (copy)
β”‚   └── XX_Pushrod_End   (child locator)             pushrod outboard target
XX_Rocker                (pivot bone)
β”‚   β”œβ”€β”€ XX_Rocker_PushEnd  (child locator)           where the pushrod points
β”‚   └── XX_Rocker_ShockEnd (child locator)           shock bottom mount
XX_Pushrod               ← KSuspAimBone              target = XX_Rocker_PushEnd
XX_Rocker                ← KSuspAimBone              target = XX_Pushrod_End  (rocker tracks the pushrod)
XX_ChassisShockMount     (fixed)
XX_ShockBody             ← KSuspShock                tophat=XX_ChassisShockMount,
                                                      lowerMount=XX_Rocker_ShockEnd, body+spring

Rule of thumb: rigid-with-hub parts get parented under the wheel bone; everything that pivots gets a KSuspAimBone; every spring gets a KSuspShock; chassis mounts stay fixed.

Deployment note (this build): the suspension is imported as part of the CustomChassis prefab of the KNPP part β€” i.e. it ships inside the chassis, built via the SDK’s CarParts Chassis category, not as a standalone .bundle. The custom chassis brings its own suspension mesh + bones, so the mod’s job is reduced to find the suspension bones on the loaded car + load the descriptor + animate (no bundle load / no stock swap needed; for a fully-custom chassis the game already skips loading the stock suspension). The descriptor (artifact β‘ ) and bone-name contract (β‘’) are unchanged. See Β§10.

6. Part 4 β€” Skin the mesh

The mesh must be a SkinnedMeshRenderer weighted to the bones so it deforms when they move. Two ways:

Then save the whole thing as a prefab.


7. Part 5 β€” Bake the descriptor (artifact β‘ )

Tools β–Έ KSL β–Έ Bake Suspension Descriptor β†’ assign the rig root β†’ Bake β†’ saves <name>.suspension.json. The report warns about: missing corners, unresolved aim targets, a tophat accidentally under a wheel bone, and duplicate bone names (which would make the mod’s name lookup ambiguous β€” keep names unique).

Example baked corner (MacPherson FL):

{ "corner":"FL", "wheelBone":"FLWheel", "copyRotation":true,
  "aims":  [{ "bone":"FL_Lower_Arm", "target":"FL_Lower_Arm_1", "aimAxis":[0,0,1] }],
  "spans": [
    { "bone":"FL_ShockBody",   "top":"FL_LowerMount", "bottom":"FL_Tophat",     "scaleAxis":"z", "scale":false, "restLength":0 },
    { "bone":"FL_ShockSpring", "top":"FL_Tophat",     "bottom":"FL_LowerMount", "scaleAxis":"z", "scale":true,  "restLength":0 }
  ] }

8. Part 6 β€” Build the bundle (artifact β‘‘)

Tools β–Έ KSL β–Έ Export Suspension Bundle β†’ assign the prefab β†’ Validate (fix errors) β†’ Validate + Build β†’ produces <name>.bundle.

Notes:

Chosen path β€” embed in the chassis: instead of a standalone .bundle, drop the suspension prefab (mesh + bones + KSusp* components) into your CustomChassis prefab and build it through the SDK’s CarParts β–Έ Chassis tool β†’ .knpp. The suspension then ships inside the chassis and installs like any chassis part. You still Bake the descriptor (artifact β‘ ) and deploy it alongside; the mod reads it and animates the bones that came in with the chassis. (The Export Suspension Bundle tool stays as the standalone alternative.)


9. Part 7 β€” Deploy + per-car config

  1. Put both artifacts where the mod can read them, e.g.: E:\...\CarX Drift Racing Online\kino\suspensions\ridge_gt\ridge.bundle E:\...\CarX Drift Racing Online\kino\suspensions\ridge_gt\ridge.suspension.json
  2. Easiest (no config): the baker writes the carId (and a name) inside a custom-named .ksusp file. Drop any number of them in kino\suspensions\ β€” e.g. drift.ksusp, grip.ksusp. The mod scans them, keeps those whose carId matches RaceCar.carId, and when several share a carId it picks the one whose suspensionRoot bones resolve on the loaded chassis (so multiple setups per car don’t clash). A .ksusp may also carry a "bundle" field (relative to the file) for swap mode. The mod logs (carId N) for the current car.

    Or use kino\suspensions\config.json for explicit control (wins over the scan). Match by carId (precise) or car name (substring, either direction β€” Ridge_GT matches Ridge_GT(Clone)); point descriptor at a .ksusp:

    {
      "suspensions": [
        { "car": "Ridge_GT",
          "descriptor": "kino/suspensions/ridge_gt/ridge.suspension.json" },
    
        { "car": "SomeOtherCar",
          "bundle": "kino/suspensions/other/other.bundle",
          "descriptor": "kino/suspensions/other/other.suspension.json" }
      ]
    }
    

    The first entry (no bundle) is find + animate β€” for a suspension that ships inside the CustomChassis. The second (with bundle) is swap β€” a standalone bundle the mod loads and swaps in. In the KSL panel: Enable swap toggles the whole system; Reload config re-reads this file after edits.

Why the mod loads it itself (not a CarParts pack, not a path override): suspension is not customizable content in this game. The SDK CarParts categories are Wheel/Bumper/Skirt/…/Chassis/BrakeRotor/Tire β€” there is no Suspension. In CarVisualManager, any SuspensionId entry is removed from the visual-parts dictionary (line ~9697) and suspension is then loaded by hardcoded name: LoadPart(SuspensionId, flag2 ? null : "suspensionmacpherson") β€” "suspensionmacpherson" for stock cars, or null for a fully-custom car (whose own model carries its suspension). So there is no category and no path-override hook. The mod injects a custom suspension by loading the bundle itself and swapping the stock one at runtime (Β§10).


10. Part 8 β€” In-game (mod runtime)

This is built (SuspensionSwap, driven from the extension’s OnUpdate). Each ~0.5 s it checks the player car; when it changes it:

  1. Finds the stock Mesh_Suspension SkinnedMeshRenderer and hides its renderer (bones stay, so the game’s own suspension targets don’t error).
  2. AssetBundle.LoadFromFile(<bundle>) (reflective), loads the prefab, and instantiates it under the car’s Model at the stock suspension’s local transform.
  3. Loads <name>.suspension.json β†’ SuspensionAnimator.Load(...).
  4. Every frame: animator.Tick(wheelTransforms) β†’ wheel bones follow the wheel transforms (width/wheelbase/travel/steer), arms aim, shocks compress.

When the car changes (or swap is disabled) it restores the stock renderer and destroys the spawned suspension. (0Harmony is referenced if we ever prefer patching the suspension LoadPart instead of the runtime swap β€” but the swap keeps us off fragile game internals.)

Chassis-embedded mode (built): if the suspension ships inside the CustomChassis (the chosen path, Β§8), there’s nothing to load or swap β€” the bones are already on the car. SuspensionSwap picks the mode per config entry:

Until that activation is wired, you can still author, bake, build, and validate everything.


11. Reference

Descriptor schema (artifact β‘ )

{
  "type": "MacPherson",            // label only
  "suspensionRoot": "Root",        // bone the mod searches under (FindDeep)
  "corners": [
    {
      "corner": "FL",              // FL | FR | RL | RR  β†’ wheel index 0..3
      "wheelBone": "FLWheel",      // copies the wheel transform
      "rotation": "steercamber",   // steercamber (steer+camber, no spin) | steeronly (flat yaw) | full (+spin) | none
      "aims":  [ { "bone": "...", "target": "...", "aimAxis": [0,0,1] } ],
      "spans": [ { "bone": "...", "top": "...", "bottom": "...",
                   "scaleAxis": "z", "scale": true, "restLength": 0 } ]
    }
  ]
}

Primitives: copy (wheel bone ← wheel transform), aim (bone LookAt target), span (bone stretches topβ†’bottom; scale:false = rigid). Any suspension type = a combination of these (MacPherson = 1 aim + 2 spans; double wishbone = 2 aims; multilink = N aims; cantilever = rocker aim + span).

Bone roles (component β†’ primitive)

| Component | Primitive | Drives | |———–|———–|——–| | KSuspWheelBone | copy | wheel mount (width/wheelbase/travel/steer) | | KSuspAimBone | aim | control arms, links, steering rods, rockers | | KSuspShock | span Γ—2 | rigid body + compressing spring (tophat fixed) | | KSuspSpanBone | span | anything exotic / manual |

Suspension data the mod reads (per wheel, reflective via GameBindings)


12. Status

Piece State
Dumper + GLB + Unity importer βœ… working
Authoring components (KSusp*) βœ… built (deploy to SDK)
Descriptor baker (artifact β‘ ) βœ… built (deploy to SDK)
Bundle exporter (artifact β‘‘) βœ… built (deploy to SDK)
Generic animator (copy/aim/span) βœ… built into the mod, compiles
Runtime activation (find/hide stock β†’ load bundle β†’ swap β†’ per-frame Tick) βœ… built (SuspensionSwap), deployed
Auto-skinning helper (locators β†’ weights) ⏳ optional, not built

Things to verify in-engine (can’t be tested outside the game)


13. Troubleshooting