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.
Mental model
Section titled “Mental model”Construction: two rig formats, one swap
Section titled “Construction: two rig formats, one swap”_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 exactlyC0/C1, so the socket pivots precisely where the motor held the joint (spec-verified). - A
BallSocketConstraintwith per-joint swing/twist limits andMaxFrictionTorqueset toFrictionTorque(default 10 — friction damps free joint spin; 0 reads as a seizure, high values as a mannequin). - A
NoCollisionConstraintfor 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.
Joint limits
Section titled “Joint limits”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° |
Collision: the self-blind group
Section titled “Collision: the self-blind group”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
Section titled “Enable”enable(model, options?) returns false if the model is already down or has no joints. Otherwise it:
- Turns off
AutoRotateandRequiresNeck(socketed necks separate slightly;RequiresNeckwould kill the humanoid). - Retunes native sockets (new-format rigs), records restore state.
- Cuts animated joints and motors; enables sockets and no-collides.
- Applies the collision-group/massless/CanCollide swap.
- Delivers the
Impulse: for player characters it rides theCKRagdollpush 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). - Enforces
MaxActive(default 16): the oldest live ragdoll is released when the cap is exceeded — a pile-up cannot accumulate unbounded simulation cost. - Schedules the
Durationauto-release if given, publishesRagdoll.Started.
There is no automatic fling: gravity collapses the body the moment the joints cut. Impulse is opt-in for hit reactions.
Release and recovery
Section titled “Release and recovery”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.
PlatformStandrestores and the humanoid getsGettingUp. - 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.
The client half: RagdollClient
Section titled “The client half: RagdollClient”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.Physicsand latches it:GettingUpis disabled and aStateChangedconnection re-forcesPhysics— ground impacts knock the state machine intoLanded/GettingUp, whose standing forces wrestle the ball sockets. That fight is the classic ragdoll seizure. - Disables the
Animatescript and stops every playing track — frozen tracks are not enough;Animatekeeps 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
Enabledup 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), restoresAutoRotateandGettingUp, and gets up.
No slanted-walk hangover, no snap, no permanent Physics latch.
-- src/Server/Bootstrap.luaulocal Root = game:GetService("ServerScriptService").ChloeKernelServerlocal 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-- src/Client/Bootstrap.luau — the owner-side half; player characters need thislocal ReplicatedStorage = game:GetService("ReplicatedStorage")local NetClient = require(ReplicatedStorage.ChloeKernel.Net.Client)local RagdollClient = require(ReplicatedStorage.ChloeKernel.Net.RagdollClient)
return function(kernel) local Net = NetClient.new() RagdollClient.listen(Net) -- returns the connection; keep it for the sessionendAutoDeath = 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.
API reference
Section titled “API reference”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 |
Bus topics
Section titled “Bus topics”| 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.
Design notes
Section titled “Design notes”- Restore is exact, and the spec holds it to that. Parts deliberately start with mixed
CanCollidevalues 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
PlatformStanddon’t mix. The server setsPlatformStandonly 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 settingPlatformStandserver-side. MaxActiveis 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.
AnimationConstraintavailability 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.