"Ragdoll a scripted-rig NPC with pure engine physics"
▸ 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:
addons/citizen/Assets/models/citizen/prefabs/citizen_physics{shape,joint}list.vmdl_prefabTwo 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
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 onlyDated correction (engine 26.08.05).
ModelPhysics.PhysicsGroupreads NULL on current builds. Its setter has no caller left in the shipped assemblies, so the olderphysics.PhysicsGroup.Bodiesform null-references the instant it runs. That path was last confirmed live on26.07.08e; treat any.PhysicsGroupuse as build-dependent and retest before reusing it. The verified route on26.08.05is to enumerateModelPhysics.Bodiesdirectly (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:
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.
- 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.