Skip to content

Ragdoll

Ragdolls classic and new-format rigs by toggling joints built once per model, restores them exactly on release, and flips humanoid state on the owning client.

Realm
Server + Client
Require
ServerScriptService.ChloeKernelServer.Ragdoll
Since
v0.3.0
Source
Ragdoll.luauNet/RagdollClient.luau

Ragdoll turns a jointed model into a physics body and puts it back as it found it. The defining decision is build once, toggle after: the sockets and no-collide constraints a ragdoll needs are created once per model and only switched on and off after that, so a second ragdoll of the same model replicates no new instances, and the falling body rides the engine’s ordinary physics replication. Humanoid state is the exception, because it is client-authoritative on player characters: a small state push flips it on the owning client, handled by RagdollClient. Projectiles and Sweeps hits are the usual triggers, and NPCKit ragdolls its dead through this module with RagdollOnDeath.

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

Classic rigs (Motor6D joints). Every enabled motor except the root joint gets two attachments (CKRagdoll0, CKRagdoll1) placed exactly at C0 and C1, so the socket pivots where the motor held the joint; a BallSocketConstraint with that joint’s swing and twist limits and MaxFrictionTorque = FrictionTorque (default 10; 0 spins freely, high values read as a mannequin); and a NoCollisionConstraint for the jointed pair.

New-format rigs (AnimationConstraint joints paired with native BallSocketConstraints, the default since the Avatar Joint Upgrade). Nothing is created except the pair no-collide: cutting the animated joint hands the limb to the rig’s own socket. Disabled AnimationConstraints count as joints too. With BreakJointsOnDeath off, the engine disables every AnimationConstraint before Humanoid.Died fires, and before 0.9.0 that left Ragdoll with no joints to find, so death ragdolls on new-format rigs never engaged. The rig’s native sockets ship frictionless and often without limits, so enable retunes them (friction to FrictionTorque, the default limits when none are on) and release restores every original value.

Everything created starts disabled. A model with neither motors nor animation joints is refused (enable returns false) instead of half-ragdolling.

Cones are wide on purpose. A limit exists to block an anatomically impossible pose, and a tight cone makes the body look stiff. Override any entry with Options.Limits:

Joint Upper (swing) Twist (either way)
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 other joint 80 45

The limit table applies to the sockets Ragdoll builds on classic rigs. A new-format rig’s native sockets keep their own limits when they have them and get the default 80 and 45 when they have none.

Pair no-collides miss hand-against-hip and arm-against-leg contacts, and their depenetration jitter is the classic ragdoll spasm. So every limb joins one collision group (default CKRagdollLimbs) that does not collide with itself: limbs hit the world, never each other. The group is shared, so two ragdolls pass through each other too.

attach registers the default group once through the Workspace (workspace:RegisterCollisionGroup, then CollisionGroupSetCollidable(name, name, false)), skipping registration when the group already exists, for example one registered in Studio. There is no PhysicsService fallback. If registration fails (a place holds at most 32 groups), Ragdoll logs a warning and limbs keep their own groups; the pair no-collides still hold. A custom CollisionGroup must already be registered, and an unregistered one logs a warning at attach.

While down, limbs gain CanCollide, and the root loses it and goes massless. Every part’s original CanCollide, Massless and CollisionGroup are recorded and restored on release.

Data Mechanism Cost
Joint, socket and no-collide toggles, collision flags Property replication of the server’s writes A few property changes per ragdoll; no new instances after the first build
The falling body Engine physics replication What any moving assembly already costs
Humanoid state (player characters) One CKRagdoll state push: Boolean8 plus a Vector3F24 impulse 10 payload bytes per enable or release

Humanoid state on a player character is client-authoritative, and the kernel never takes ownership of player characters. The server pushes CKRagdoll over the NetDriver state channel to the owning player, and RagdollClient answers. Rigs without a player owner (NPCs) are handled on the server: PlatformStand = true and a change to the Physics state.

Rigs without a player owner are also pinned to server network ownership. Before the state change, Ragdoll pins the root (while jointed, the body is one assembly, so that pins every limb); after the joints are cut, it pins every part, since each limb is now its own assembly and a nearby client would otherwise own it. Parts that cannot take network ownership (anchored, or welded to something anchored) are skipped. The pin is not undone on release, so an NPC stays server-owned afterwards, including one whose archetype sets ClientPhysics = true.

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

  1. For a rig without a player owner, pins the root to the server.
  2. Turns off AutoRotate and RequiresNeck (socketed necks separate slightly, and RequiresNeck would kill the humanoid). For a rig without an owner, also sets PlatformStand and changes the state to Physics.
  3. Retunes native sockets and records their values.
  4. Records each AnimationConstraint’s Enabled and disables it; disables the motors; enables the sockets and no-collides.
  5. For a rig without an owner, carries the animation’s momentum into the fall: Animator:ApplyJointVelocities(motors) gives the cut Motor6Ds the joint velocities of the playing tracks, so a swing or stride continues instead of freezing mid-pose. (The engine documents Motor6D input only.) Player characters do this on the owning client.
  6. Swaps collision group, Massless and CanCollide, then pins every part of an unowned rig.
  7. Delivers the Impulse. For a player character it rides the CKRagdoll push and applies on the owning client; for other rigs it applies on the server. Either way it applies once per assembly at the assembly’s mass (ApplyImpulse(impulse * AssemblyMass) on each assembly root), so every limb gets the same velocity change and massless accessories ride their limb instead of taking a share.
  8. Enforces MaxActive (default 16): the oldest live ragdoll is released when the cap is exceeded.
  9. Schedules the Duration release on the kernel Scheduler, and publishes Ragdoll.Started.

There is no automatic fling. Gravity collapses the body once the joints cut; Impulse is for hit reactions.

release(model) restores everything: sockets and no-collides off, motors back on, each AnimationConstraint back to the Enabled it had before the cut, every part’s collision state back, every native socket’s tune back. A pending Duration deadline is cancelled. The restore of AnimationConstraints matters on death: joints the engine disabled at death stay disabled, so a released corpse stays down instead of snapping upright.

Recovery then splits by owner:

  • Rigs without a player owner recover on the server: assembly velocities zeroed, the root stood upright with its yaw kept (a tilted or moving root falls straight over again) and lifted 2 studs, because the root sits at torso height and standing needs leg room. PlatformStand is restored and the humanoid changes to GettingUp.
  • Player characters receive CKRagdoll(false) and recover on the client.

A model that is destroyed while down, or that leaves the DataModel without Destroy() (Parent = nil, pooling, legacy character removal), is released and its built components are torn down: Destroying and AncestryChanged connections cover both. destroy() on the service releases everyone.

AutoDeath = true ragdolls player characters on death. For every session it watches the current character: it sets Humanoid.BreakJointsOnDeath = false, so the engine does not destroy the joints before Died fires, and connects Died to enable. The watch is replaced when the character changes and disconnected at session end and at destroy(). It works on classic and new-format rigs.

A manual on-death enable (your own Died handler, a round system) needs BreakJointsOnDeath = false on that humanoid too; with the default true, the joints are gone by the time your handler runs.

Player characters need RagdollClient.listen(netClient) running on the client. Without it the owner never enters the Physics state, and the server’s joint cuts fight the client’s character controller.

Going down (CKRagdoll(true, impulse)):

  • Records the humanoid’s AutoRotate and whether each state it is about to block was enabled (a push while already down keeps the first record), then turns AutoRotate off.
  • Disables the Animate script. Stopping tracks alone is not enough, because Animate keeps starting new ones.
  • Applies the playing tracks’ joint velocities to the joints the server cut (Animator:ApplyJointVelocities, Motor6Ds only), then stops every playing track.
  • Disables every competing humanoid state: Running, RunningNoPhysics, StrafingNoPhysics, Landed, Freefall, Jumping, FallingDown, GettingUp, Climbing, Swimming, Seated, PlatformStanding, Flying and Ragdoll. Ground impacts knock a humanoid into Landed or GettingUp, whose standing forces push against the sockets; that fight is the classic ragdoll seizure. A disabled state cannot be entered, even by the engine’s own transitions. SetStateEnabled does not replicate, so this has to run on the owner.
  • Changes to Physics and connects a StateChanged latch that forces Physics back if anything but Physics or Dead is entered, as a backstop.
  • Applies the impulse once per assembly at its mass.

Release (CKRagdoll(false)), on its own thread:

  • Waits for the server’s joint re-enables to replicate. It finds every Motor6D and AnimationConstraint in the character that is still disabled and waits on each one’s Enabled property-changed signal, with one 0.5 s deadline for the whole set, whichever comes first. Nothing polls. Standing up while joints are still cut is where broken recoveries come from.
  • Aborts if another push arrived meanwhile (a generation counter; a re-ragdoll during recovery wins) or the character left the game.
  • Zeroes the root’s assembly velocities before the re-enabled joints pull limbs back, stands the root upright with its yaw kept and 2 studs of leg room, re-enables Animate (which restarts into idle), restores AutoRotate and exactly the state flags it recorded, and changes to GettingUp.

A corpse’s joints stay disabled after release (see above), so a release on a dead character waits out the full 0.5 s before standing it up. The Dead state keeps it down.

src/Server/Game/Services/KnockdownService.luau
--!strict
-- KnockdownService: death ragdolls, and a 4-second knockdown when a bolt lands.
local ServerScriptService = game:GetService("ServerScriptService")
local Ragdoll = require(ServerScriptService.ChloeKernelServer.Ragdoll)
local KnockdownService = {
Name = "KnockdownService",
}
function KnockdownService.init(self: any, kernel: any)
self.Kernel = kernel
self.Ragdolls = Ragdoll.attach(kernel, {
MaxActive = 16,
AutoDeath = true, -- every player death ragdolls; sets BreakJointsOnDeath = false
})
end
function KnockdownService.start(self: any)
self.HitConnection = self.Kernel.Bus:subscribe(
"Projectile.Hit",
function(_topic: string, _owner: any, victim: Player?, _defId: number, position: Vector3)
local Character = victim and victim.Character
local Root = Character and Character:FindFirstChild("HumanoidRootPart")
if Character and Root then
local Away = (Root :: BasePart).Position - position
local Direction = if Away.Magnitude > 1e-3 then Away.Unit else Vector3.yAxis
self.Ragdolls:enable(Character, { Duration = 4, Impulse = Direction * 40 })
end
end
)
end
function KnockdownService.stop(self: any)
if self.HitConnection then
self.HitConnection:disconnect()
end
self.Ragdolls:destroy()
end
return KnockdownService

NPC rigs use the same calls: Ragdolls:enable(NpcModel, { Impulse = Vector3.new(0, 30, -50) }) and Ragdolls:release(NpcModel). NPCKit’s RagdollOnDeath sets BreakJointsOnDeath = false and enables the kit’s Ragdoll before Npc.Died.

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; sets BreakJointsOnDeath = false on their humanoids
CollisionGroup "CKRagdollLimbs" Limb group while down. The default is registered and made self-non-colliding; a custom group must already be registered
FrictionTorque 10 Socket MaxFrictionTorque while down
Limits built-in table Per-joint { Upper, Twist } overrides merged over the defaults
Member Description
ragdolls:enable(model, options?) -> boolean Ragdolls the model. options: { Duration?, Impulse? }. False when it has no joints or is already down
ragdolls:release(model) Restores joints, sockets, collision flags and humanoid state as found; no-op when not down
ragdolls:isRagdolled(model) -> boolean Whether the model is down
ragdolls:destroy() Disconnects AutoDeath watches, releases every ragdoll and tears down built components
RagdollClient.listen(netClient, options?) -> connection The owner-side handler. options: { Player? } (default Players.LocalPlayer)

EnableOptions:

Field Description
Duration Seconds until release, on a kernel Scheduler deadline that an earlier release cancels
Impulse Fling velocity, applied once per assembly at its mass on the owning machine
Topic Payload When
Ragdoll.Started model A ragdoll engaged
Ragdoll.Ended model Released and restored, including cap evictions, removal from the DataModel and destroy()

Wire: the CKRagdoll state channel, schema Boolean8, Vector3F24 (down flag and impulse), sent only to the owner of a player character. See Packet and wire types.

  • Restore is exact. The spec rig starts with mixed CanCollide values, and release must put the mixed values back. Collision changes your game makes to a body while it is down are overwritten on release.
  • PlatformStand is only for rigs without a player owner. On a player character it fights the owner’s controller: jitter while down, sliding after. If you see either, something else sets PlatformStand on the server.
  • MaxActive is a simulation budget. Sixteen simulated bodies is already generous on a server that also runs NPC crowds.
  • StreamingEnabled: RagdollClient works on wire data and the player’s own character, which is always streamed to its owner.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.