Skip to content

Ragdoll

Ragdoll turns any jointed model into a physics body and puts it back exactly as it found it. The defining decision is build-once, toggle-forever: the constraints a ragdoll needs are constructed one time per model and merely flipped on and off for every ragdoll after — so a re-ragdoll replicates zero new instances, and the physics itself rides the engine’s ordinary replication. Nothing about a falling body costs custom wire traffic.

_build walks the model’s descendants once and classifies joints:

Classic rigs (R6/R15, Motor6D joints). Every enabled motor — except the root joint — gets:

  • Two attachments (CKRagdoll0/CKRagdoll1) placed at exactly C0/C1, so the socket pivots precisely where the motor held the joint (spec-verified).
  • A BallSocketConstraint with per-joint swing/twist limits and MaxFrictionTorque set to FrictionTorque (default 10 — friction damps free joint spin; 0 reads as a seizure, high values as a mannequin).
  • A NoCollisionConstraint for the jointed pair.

New-format rigs (AnimationConstraint joints paired with native BallSocketConstraints). Nothing new is built except the pair no-collide: cutting the animated joint lets the rig’s own socket take over. But those native sockets ship frictionless and often limitless — free-spinning joints spasm — so enable retunes them (friction to FrictionTorque, default limits if none were enabled) and release restores every original value (spec-verified).

Everything created starts disabled. enable and release only toggle Enabled flags and properties; a model with no motors and no animation joints refuses (enable returns false) instead of half-ragdolling.

Cones are wide by design — limits exist to block anatomically impossible poses, not to pose the body. Tight cones read as a stiff mannequin. The built-in table (override any entry via Options.Limits):

Joint Upper (swing) Twist (±)
Neck 45° 60°
Waist / RootJoint 40° 40°
Shoulders (R6 Left Shoulder, R15 LeftShoulder, …) 120° 60°
Hips 90° 45°
Elbows / Knees 80° 15°
Wrists 60° 30°
Ankles 50° 20°
(any unlisted joint) 80° 45°

Jointed-pair no-collides alone miss hand-vs-hip and arm-vs-leg contacts, whose depenetration jitter is the classic residual ragdoll spazz. So all limbs join a collision group (default CKRagdollLimbs, auto-registered) that is set non-collidable with itself: limbs collide with the world, never with each other. While down, limbs gain CanCollide; the root loses it and goes massless. Every part’s original CanCollide/Massless/CollisionGroup is recorded and restored on release.

Who owns the flop: the replicate-for-free trick

Section titled “Who owns the flop: the replicate-for-free trick”

What actually rides the wire:

Data Mechanism Cost
Joint/socket/no-collide toggles, collision flags Ordinary property replication of the server’s writes A handful of property changes per ragdoll — no new instances after the first build
The falling body itself Engine physics replication of the simulated assemblies What any moving assembly already costs — nothing extra
Humanoid state flip (player characters only) One CKRagdoll state push: Boolean8 + Vector3F24 impulse 10 bytes per enable/release

The one thing property replication cannot do is the humanoid state machine: it is client-authoritative on player characters, and the kernel never takes ownership of them. So for a player character the server pushes CKRagdoll over the NetDriver state channel and the owning client answers (below). Server-simulated rigs (NPCs) have no owner to ask — they get PlatformStand = true and a Physics state change directly on the server.

enable(model, options?) returns false if the model is already down or has no joints. Otherwise it:

  1. Turns off AutoRotate and RequiresNeck (socketed necks separate slightly; RequiresNeck would kill the humanoid).
  2. Retunes native sockets (new-format rigs), records restore state.
  3. Cuts animated joints and motors; enables sockets and no-collides.
  4. Applies the collision-group/massless/CanCollide swap.
  5. Delivers the Impulse: for player characters it rides the CKRagdoll push and is applied on the owning machine; for server rigs it applies server-side. Either way it applies per part, scaled by mass — with joints cut, every limb is its own assembly, so a single root impulse would move nothing (and the ghost root is massless anyway).
  6. Enforces MaxActive (default 16): the oldest live ragdoll is released when the cap is exceeded — a pile-up cannot accumulate unbounded simulation cost.
  7. Schedules the Duration auto-release if given, publishes Ragdoll.Started.

There is no automatic fling: gravity collapses the body the moment the joints cut. Impulse is opt-in for hit reactions.

release(model) restores everything in reverse: toggles off, motors and animated joints back on, every part’s collision state back, every native socket’s original tune back. Then recovery splits by ownership:

  • Server-simulated rigs recover server-side: zero the assembly velocities, stand the root upright with its yaw kept (a tilted or moving root falls straight over again), lifted +2 studs because the root sits at torso height and standing needs leg room. PlatformStand restores and the humanoid gets GettingUp.
  • Player characters receive CKRagdoll(false) and recover owner-side (below).

A model that is Destroyed while down releases and tears down automatically (a Destroying connection); destroy() on the service releases everyone.

Player characters must run RagdollClient.listen(netClient) in the client Bootstrap — without it the owner never enters the Physics state and the server-side joint cuts fight the client’s character controller.

Down (CKRagdoll(true, impulse)):

  • Enters Enum.HumanoidStateType.Physics and latches it: GettingUp is disabled and a StateChanged connection re-forces Physics — ground impacts knock the state machine into Landed/GettingUp, whose standing forces wrestle the ball sockets. That fight is the classic ragdoll seizure.
  • Disables the Animate script and stops every playing track — frozen tracks are not enough; Animate keeps starting new ones.
  • Applies the impulse per part, scaled by Part.Mass, so every limb gets the same velocity change.

Release (CKRagdoll(false)):

  • Waits for the server’s joint re-enables to replicate (finds a cut joint, polls its Enabled up to a 0.5 s deadline). Standing up while joints are still cut is where broken recoveries come from. A generation counter aborts the wait if a fresh ragdoll push lands mid-wait (spec: re-ragdoll during recovery must win).
  • Zeroes assembly velocities before the re-enabled joints yank limbs back into place, stands the root upright with yaw kept (+2 studs of leg room), re-enables Animate (a fresh enable restarts it into idle), restores AutoRotate and GettingUp, and gets up.

No slanted-walk hangover, no snap, no permanent Physics latch.

-- src/Server/Bootstrap.luau
local Root = game:GetService("ServerScriptService").ChloeKernelServer
local Ragdoll = require(Root.Ragdoll)
return function(kernel)
local Ragdolls = Ragdoll.attach(kernel, {
MaxActive = 16,
AutoDeath = true, -- every player death ragdolls the character
})
-- Hit reactions: ragdoll for 4 seconds with a fling along the shot
kernel.Bus:subscribe("Projectile.Hit", function(_, ownerSession, victimPlayer, defId, position)
local Character = victimPlayer and victimPlayer.Character
if Character then
local RootPart = Character.PrimaryPart
local Direction = if RootPart then (RootPart.Position - position).Unit else Vector3.yAxis
Ragdolls:enable(Character, { Duration = 4, Impulse = Direction * 40 })
end
end)
-- Manual control works the same on NPC rigs:
-- Ragdolls:enable(NpcModel, { Impulse = Vector3.new(0, 30, -50) })
-- Ragdolls:release(NpcModel)
end

AutoDeath = true wires Humanoid.Died on every session’s characters, so kill events need no extra plumbing. For custom kill flows (rounds, executions, environmental deaths), call enable from your own Bus subscribers as above.

Ragdoll.attach(kernel, options?) options:

Option Default Description
MaxActive 16 Live-ragdoll cap; exceeding it releases the oldest
AutoDeath false Ragdoll player characters on Humanoid.Died
CollisionGroup "CKRagdollLimbs" Limb group while down; the default is auto-registered self-non-collidable
FrictionTorque 10 Socket MaxFrictionTorque while down; 0 = free-spinning, high = stiff
Limits built-in table Per-joint { Upper, Twist } overrides merged over the defaults
Member Description
ragdolls:enable(model, options?) → boolean Ragdoll the model; options = { Duration?, Impulse? }. False when jointless or already down
ragdolls:release(model) Restore joints, sockets, collision flags, humanoid state — exactly as found
ragdolls:isRagdolled(model) → boolean Is the model currently down
ragdolls:destroy() Release every active ragdoll and tear down built components
RagdollClient.listen(netClient) → connection Owner-side state handler; wire once in the client Bootstrap

EnableOptions:

Field Description
Duration Seconds until auto-release (a re-enable during the window is not clobbered — the release checks state identity)
Impulse Fling velocity, applied per part × mass on the owning machine
Topic Payload When
Ragdoll.Started model A ragdoll engaged
Ragdoll.Ended model Released and restored (including cap evictions and destroy)

Wire: the CKRagdoll state channel, schema Boolean8, Vector3F24 (enabled flag + impulse), sent only to the owning player of player characters. See Packet & Wire Types.

  • Restore is exact, and the spec holds it to that. Parts deliberately start with mixed CanCollide values in the spec rig, and release must put back the mixed values — not “true for everything”. If your game changes part collision while a body is down, those writes are overwritten on release.
  • Player characters and PlatformStand don’t mix. The server sets PlatformStand only on unowned rigs; on player characters it would fight the owner-side controller — jitter while down, sliding after. If you see either symptom, something else is setting PlatformStand server-side.
  • MaxActive is a simulation budget, not a gameplay rule. Sixteen simulated bodies is already generous; pick the value with DeviceBench-style tiers if your server also simulates heavy NPC crowds.
  • StreamingEnabled: RagdollClient operates on wire data and the player’s own character — no dependency on streamed world geometry. See StreamingEnabled.
  • AnimationConstraint availability varies by engine build. The spec skips new-format construction where the class isn’t creatable; live new-format rigs exercise it in play testing.