Ragdoll
A character's skeleton handed to physics: switched on for a fall (a death, a knockdown, an
explosion) and off again to get up; powered, it does hit reactions on a character that stays in its
animation. An aspect on a skinned Model, built on 3D physics; springy
hair and tails are DynamicBone, a walking capsule the CharacterController.
- make a character fall limp when it dies or is knocked down
- throw a body back with an explosion or a hit
- make a character flinch when shot without falling over
- let a shot character hold its pose for a moment, then collapse
- get a fallen character back up with an animation
- know when a fallen body has come to rest and which way it lies
- choose which bones get a body and how far the joints bend
Platforms: iOS and Android (the full engine variant), desktop, the web player and the le.codes viewer app; not on the web (Web Lite) or in headless tests — check Ragdoll.supported, and activate() (false = the host refused the body).
At a glance
Physics.configure({ gravity: [0, -9.81, 0] })
const scene = new Scene()
scene.add(Mesh.box({ material: Material.lit({ color: '#6b8f5a' }), size: [40, 1, 40], position: [0, -0.5, 0] })
.aspect(Shape, {}).aspect(Physics, { motion: 'static' }))
scene.open()
let rag: Ragdoll | null = null
Model.load(asset('./hero.glb')).then((hero) => {
scene.add(hero)
rag = hero.aspect(Ragdoll, { mass: 75 }).ragdoll // 'humanoid': the parts found by bone name
rag.activate({ velocity: [0, 0, -2] }) // falls from the pose it is in, thrown back at 2 m/s
})
class Fallen extends System<'fallen'> {
private told = false
update() {
if (this.told || !rag?.settled) return // every part at rest — frozen where it lies
this.told = true
console.log(`down: ${rag.facing}, head toward ${rag.heading.toFixed(0)}°, hips at y ${rag.root!.worldPosition.y.toFixed(2)}`)
}
}
scene.system(Fallen)The ragdoll is built once, at attach, from the pose the model is in — that pose is the joints'
neutral for the limits. activate() drops the body from the pose it shows now, every bone keeping
the motion it had over the last two frames (a runner keeps travelling, a swinging arm keeps
swinging); velocity is a launch added on top.
Which bones get a body: parts
One capsule body per listed bone, joined to its parent part by a swing-twist joint (a cone plus a twist range, or a one-way hinge for knees and elbows) that carries a motor.
node.aspect(Ragdoll, { parts: 'humanoid' }) // the default
node.aspect(Ragdoll, { parts: [
{ bone: 'Hips', to: 'Spine', radius: 0.12, mass: 15 },
{ bone: 'Spine', to: 'Head', radius: 0.11, mass: 25, swing: 25 }, // parent: the previous part
{ bone: 'Head', radius: 0.1, length: 0.2, mass: 6 }, // a leaf: 0.2 m on from its parent's line
{ bone: 'LeftUpLeg', to: 'LeftLeg', parent: 'Hips', radius: 0.08, mass: 10 },
{ bone: 'LeftLeg', to: 'LeftFoot', radius: 0.06, mass: 5, hinge: { axis: [1, 0, 0], min: 0, max: 140 } },
] })
interface RagdollPart {
bone: string // the bone the body sits on — its origin is the joint
to?: string // the bone the capsule points at; omitted = a leaf, `length` long
parent?: string // the part it hangs from; default the previous part (the root has none)
radius?: number // m, default 0.05
length?: number // m — a leaf's length, or an override of the distance to `to`
mass?: number // this part's SHARE of the ragdoll's mass (relative), default 1
swing?: number // cone half-angle, degrees, default 30 (ignored with `hinge`)
twist?: number // twist half-angle, degrees, default 20
hinge?: RagdollHinge // { axis: Vec3Like, min, max } — a one-way joint: the axis in the MODEL's rest space, degrees
}'humanoid' finds the standard bones by name — Mixamo, MotusMan and Unity-style names first, then
Unreal-style (pelvis, thigh_l, …), with a mixamorig: style prefix ignored — and lays out a
~1.8 m human: hips, one spine capsule up to the neck, head, two-segment arms and legs, hands and feet
where the rig has them, knees and elbows as hinges that fold the natural way for a model standing in
a T- or A-pose facing +Z. A rig without a hips bone throws; name the parts yourself. A list lists
parents before children, and an unknown bone throws at attach.
Mass, collisions, motors and joint limits: Ragdoll options
node.aspect(Ragdoll, {
mass?: number // kg, default 70 — split between the parts by their `mass` shares
friction?: number // every part, default 0.6
linearDamping?: number // default 0 = Jolt's own 0.05
angularDamping?: number // default 0 = Jolt's own 0.05; more settles a flailing body sooner
collide?: 'static' | 'all' // default 'static' (below)
freeze?: boolean // default true: a limp body at rest turns static where it lies
freezeAfter?: number // s limp before it freezes regardless; default 0 = no cap
driveFrequency?: number // Hz, the motors' spring, default 6
driveDamping?: number // ratio, default 1 (critical)
driveTorque?: number // N·m per kg of the part at strength 1, default 12
jointFriction?: number // N·m per kg of the part, default 0.3 — a limp body folds instead of flopping
limits?: 'clips' | 'table' // default 'clips' (below)
limitsMargin?: number // degrees added on each side of a learned range, default 10
})Everything here is read once, at attach — re-configuring a live ragdoll changes nothing but
strength / anchored; remove the aspect and attach it again to rebuild.
collide: 'static'— a fallen body is scenery, not an obstacle: only the static world and other such ragdolls stop it; dynamic bodies, character controllers and vehicles pass through, so it never shoves or blocks anything.'all'is a regular dynamic body that bumps into everything (and gets kicked awake by everything).freeze— once every part of a limp body has come to rest (or afterfreezeAfterseconds limp), the parts turn static where they lie: the pose holds, nothing can wake them, the solver skips them;settledreadstrue. A driven or anchored body never freezes.activate()makes it dynamic again.limits: 'clips'— the joint limits are learned from the animation: at the first activation that finds clips on the model's animator, each joint's swing and twist range over every bound clip (+limitsMargin) becomes its limit; until then, and with'table', the parts' cones and hinges apply.
Knock it down: activate() and reading the fallen body
rag.activate(opts?: RagdollActivateOptions): boolean // { velocity?: Vec3Like (m/s launch), strength?, anchored? }
rag.active // physics owns the bones
rag.settled // active, and every part asleep or frozen
rag.root // the root part's bone node (the hips): where the body is
rag.facing // RagdollFacing: 'up' (on its back) | 'down' (prone) | 'side'
rag.heading // degrees: the yaw that faces where the head points — for the get-up take
rag.bones // the bone names that carry a part, in order
rag.bodyOf(bone): number // a part's rigid-body id (0 = none)
rag.impulse(bone, v, at?): boolean // N·s on one part, at a world point or through its centreactivate() returns false when there is no ragdoll to activate (see where a ragdoll runs).
While active, the engine writes the bodies' poses onto the bones every frame after the animator,
so the listed bones hide whatever plays underneath; bones without a part (fingers, toes, the spine
bones between two parts) follow the animation while the body is driven and hold their pose as it
goes limp. Anything mounted on a bone rides along. Physics.raycast hits the parts and reports the
bone node, so a shot knows which limb it struck. impulse and hit take the part's name as
listed in bones (the humanoid table's name, e.g. 'Head', even on a mixamorig:-prefixed rig).
A character's CharacterController is yours to switch off for the fall
(node.removeAspect(CharacterController)): a capsule left standing would keep the body from settling.
Hit reactions and a collapsing death: hit, goLimp, strength
rag.strength = 1 // 0..1: how hard every joint is pulled toward the pose the animator shows
rag.anchored = true // the hips follow the animation kinematically (a stagger, not a fall)
rag.hit(bone, v, at?, opts?: RagdollHitOptions): boolean // { duration?, blend?, strength? }
rag.goLimp(seconds = 0.4) // the joints give way: strength eases to 0, the anchor comes offhit is a flinch in one call: a body that is not active powers up anchored to its animation (at
strength, default 1), the struck part takes the impulse (N·s, at a world point), and after
duration (s, default 0.6) the bones go back to the animator through blend (s, default 0.25). On a
limp active body (a corpse) it is just the impulse. strength and anchored persist across
activations. goLimp is a death that does not switch off: a body shot mid-stride keeps its pose for
a moment and then collapses. Call it after activate(), which cancels a running goLimp.
// a shot: the struck limb flinches and the character keeps its animation; the last one kills
const shot = (rag: Ragdoll, bone: string, push: Vec3, at: Vec3, fatal: boolean) => {
if (!fatal) { rag.hit(bone, push, at); return } // powers up anchored, back to the animator after 0.6 s
rag.activate({ strength: 1, anchored: false }) // a free body that still holds the pose it shows…
rag.impulse(bone, push, at)
rag.goLimp(0.4) // …and gives way over 0.4 s
}Getting up after a fall: deactivate
Stand a fallen character back up: hand the bones back to the animator, then play a get-up take from where the body lies.
rag.deactivate(opts?: RagdollDeactivateOptions) // { blend?: s the animator takes to come out of the fallen pose; default 0 = a cut }The animator's pose shows again from the next frame — through blend when asked: play the get-up
take right after, and it starts from where the body lies (facing picks the take, heading its
direction). Move the model node under the hips before this call: the bones are handed back
against its new transform.
// back on its feet: read how it lies, put the model node under the hips, then hand the bones back
const standUp = (hero: Model, rag: Ragdoll) => {
const hips = rag.root!.worldPosition, heading = rag.heading
const take = rag.facing === 'down' ? 'GetUpFront' : 'GetUpBack' // prone, or on its back / side
hero.position = [hips.x, hero.position.y, hips.z]
hero.eulerAngles = [0, heading, 0] // facing where the head points
rag.deactivate({ blend: 0.2 })
hero.anim.play(take, { fade: 0.2 })
}Where a ragdoll runs: Ragdoll.supported
Needs 3D physics with ragdoll parts — guard with Ragdoll.supported (false, never a throw, without
physics). Web Lite and the headless renderer report it but refuse the parts: the aspect
attaches (with a warning), activate() returns false and the body never drops. Test a ragdoll with
lecodes render --sim or --desktop (physics.md).