the symptom, in your words

"Ragdoll a scripted-rig NPC with pure engine physics"

✓ verified on 26.08.05
lane Rigging & animationposted updated

▸ SYMPTOM

You want an NPC to crumple / faint / get hit without authoring collapse animation clips. Per-bone C# posing looks tempting and then explodes (see setbonetransform-silently-noop). Limbs float disconnected while the torso ragdolls (see bone-name-dot-to-underscore).

▸ CAUSE

Ragdoll for a scripted-rig character is engine physics, not clip authoring and not SetBoneTransform.

You put a PhysicsShapeList + PhysicsJointList in the vmdl (capsule per major bone + joints). Sandbox.ModelPhysics builds one physics body per bone, simulates them, and writes results back onto the SkinnedModelRenderer's bones.

▸ FIX

1. Author physics nodes in the vmdl

Copy schema from citizen prefabs:

snippet
addons/citizen/Assets/models/citizen/prefabs/citizen_physics{shape,joint}list.vmdl_prefab

Two nodes under RootNode children:

  • PhysicsShapeList → children = capsules (PhysicsShapeCapsule)
  • PhysicsJointList → children = joints

Capsule fields: parent_bone, surface_prop="flesh", collision_tags="solid", radius, point0, point1 (capsule axis in the bone's local frame). For bones that run down local +X: point0 ≈ [r/2,0,0]point1 ≈ [len−r/2,0,0].

Joints name bodies by bone name via parent_body / child_body:

  • PhysicsJointConical (ballsocket): spine/neck/head/shoulder/hip
    (enable_swing_limit / swing_limit, twist limits, friction)
  • PhysicsJointRevolute (hinge): elbows & knees
    (enable_limit / min_angle / max_angle)

anchor_origin = pivot in the parent body's local frame = child bone head ≈ [parent_len, 0, 0].

Every joint's parent and child must have a shape or the body floats disconnected. Give the neck a body; it bridges chest→head.

Units: physics shape coords are in the same unit as mesh+bones BEFORE ModelModifier_ScaleAndMirror. For a meters→cm FBX lane that means centimetres; the vmdl's ModelModifier_ScaleAndMirror 0.3937 scales physics with mesh+anims. Citizen proof: radius 10.0 = 10 cm pelvis capsule alongside the same 0.3937 modifier. Do not pre-scale capsules to inches.

Bone names in physics KV3: sanitize Blender .L/.R_L/_R. See bone-name-dot-to-underscore.

2. Toggle at runtime

snippet
var physics = Components.GetOrCreate<ModelPhysics>();
physics.Renderer = skinned;          // SkinnedModelRenderer
physics.Model = skinned.Model;       // same physics-bearing vmdl
physics.Enabled = true;
physics.MotionEnabled = true;

// shove - enumerate Bodies directly (see build note below)
foreach (var body in physics.Bodies)
    body.ApplyImpulse(impulse);

// stand back up
physics.Destroy(); // bone control returns to Sequence next frame
// snap root to pelvis body rest (Bodies[0].Position), typically x/y only

Dated correction (engine 26.08.05). ModelPhysics.PhysicsGroup reads NULL on current builds. Its setter has no caller left in the shipped assemblies, so the older physics.PhysicsGroup.Bodies form null-references the instant it runs. That path was last confirmed live on 26.07.08e; treat any .PhysicsGroup use as build-dependent and retest before reusing it. The verified route on 26.08.05 is to enumerate ModelPhysics.Bodies directly (as above).

ModelPhysics.Bodies has no GetBody(string) and no string indexer, so to drive one named body you resolve the bone to an index first and walk the list:

snippet
int idx = physics.Model.Bones.GetBone(boneName).Index;
var body = physics.Bodies.FirstOrDefault(b => b.Bone == idx);
body?.ApplyImpulse(impulse);

Members ModelPhysics.{Renderer,Model,MotionEnabled,Enabled,Bodies} and PhysicsBody.{ApplyImpulse,Position,Bone} resolve under dotnet build. Note: the type may not enumerate via reflection GetTypes() (native registration). Trust the compiler.

Radii as a fraction of each bone's length (+ abs clamps) scale with proportions better than hardcoded human dims. Deliberately generous swing/twist limits with identity anchor_angles read as a crumple without requiring an exact anchor-axis match. Copy citizen's schema, then tune the fractions and limits against your own rig's bone-length table.

▸ WHY IT WORKS

ModelPhysics is the engine's bind between ModelDoc physics bodies and a skinned pose. Once capsules/joints exist on the compiled model, enabling the component hands bone control to the physics world and writes simulated transforms back each tick, which is exactly what a ragdoll is. Collapse clips become unnecessary; SetBoneTransform never enters the picture.

Verified on engine 26.08.05: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Dated correction (26.08.05): ModelPhysics.PhysicsGroup reads NULL on current builds. The PhysicsGroup.Bodies shove/stand-up path is build-dependent (last confirmed 26.07.08e). Documented the verified route: enumerate ModelPhysics.Bodies directly and resolve a named bone via Model.Bones.GetBone(name).Index, walking Bodies for the matching .Bone.
  • Verified fractional-radii and generous-limit guidance against source; removed needs-verification hedge.

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord