Why my rig is twisting, stretching and freezing in a T-pose
Your rig spins the arm around the torso, stretches the leg to the other side of the map or freezes in a T-pose while the animation plays smoothly in the Animation Editor. The defect is almost never in the animation: it is in the Motor6D of the rig sitting in the Workspace, and you can point at the guilty joint in two seconds.
Produced with AI and checked by automated verification. This text carries no source list. No person read this text before publication. Editorial Policy

If your rig is "doing that thing" — the arm rotating around the whole torso, the leg stretching to the other side of the map, the head detaching from the neck, the character planted in a T-pose while the same animation plays smoothly in the Animation Editor — it is not bugged. It is obeying, to the letter, the Motor6D hierarchy that exists in the Workspace right now. And that hierarchy is different from the one that existed when you recorded the animation.
Your rig is not bugged, it is obeying
It is worth understanding the mechanism before touching anything. A Roblox animation does not store "the arm went up 30 degrees" tied to a specific object: it is a KeyframeSequence that stores a pose per joint name, inside a hierarchy of names. When the track plays, the Animator looks in the rig for a joint called LeftShoulder, applies the transformation to that Motor6D and moves on. If that joint has another name, points at another part, has Part0 and Part1 inverted or simply does not exist, the Animator raises no error in the Output at all. It applies what it finds and ignores the rest. The spaghetti you are seeing is the rig doing what it was told.
The expensive part is what most people do next: they assume the animation is crooked and go back to Blender. The full cycle has six stages — adjust in Blender, export FBX, import into the Animation Editor, export the animation, publish, wait for moderation and swap the asset ID in the scripts. You repeat that three times, burn a whole afternoon, generate three new asset IDs that hang around your game forever, and the rig still twists. Because the defect was never in the file: it was in the model inside the Workspace.
The numeric shortcut is simple and works as a first test. A standard R15 rig has 16 parts and exactly 15 Motor6D: Root, Waist, Neck, three per arm (Shoulder, Elbow, Wrist) and three per leg (Hip, Knee, Ankle). An R6 rig has 7 parts and 6 joints: RootJoint, Neck, Left Shoulder, Right Shoulder, Left Hip and Right Hip — with the space in the name, and all parented to the Torso. If your rig does not match those numbers, you already have somewhere to start before opening any 3D software.
The five diagnoses, in the right order
1. Count the joints and check Part0 and Part1. Select the rig in the Explorer, look at how many Motor6D exist and open each of the suspects. Part0 is the parent (it stays still), Part1 is the child (it is what rotates). In an R15's LeftShoulder, Part0 has to be UpperTorso and Part1 has to be LeftUpperArm. Like that, the arm rotates around the shoulder. The other way round, the whole torso starts rotating around the arm — which is exactly the "my whole rig spins when I move the arm" you are seeing. The wrong way to fix it, which people do constantly at run time:
local m = npc.UpperTorso.LeftShoulder
m.Part0 = npc.LeftUpperArm -- the child became the parent
m.Part1 = npc.UpperTorso
-- now every animation recorded on this rig is offsetInverting Part0 and Part1 at run time "undoes" nothing: C0 and C1 are still calculated for the old direction, and you swap one bug for another that is harder to see. Fix it in the source rig, in Studio, with the game stopped.
2. Check name by name, including spaces and capitals. This is the silent champion. You renamed UpperTorso to Torso_01 in Blender, or the artist exported with a .001 suffix, or somebody duplicated a limb and Studio created LeftUpperArm and LeftUpperArm with the same name. The animation keeps looking for the old name, does not find it, and that limb freezes in the neutral pose — hence the "half the body animates, half stays in a T-pose" effect. In R6 the classic error is recording Left Shoulder without the space. A joint name is a string, and strings do not forgive.
3. Anchored parts, leftover Welds and the RootPart. If any part of the rig has Anchored ticked, the Motor6D cannot move it, and everything else starts orbiting that frozen part. That happens constantly when you drag the rig around the map and, to stop it falling, anchor a limb instead of the HumanoidRootPart. Along the same lines: a WeldConstraint or an old Weld between two parts that already have a Motor6D linking them. Both try to resolve the same position, and the result is a limb shaking or stuck in the wrong place. And check that Model.PrimaryPart points at the HumanoidRootPart — without it, PivotTo and MoveTo drag the rig by the wrong pivot and you swear the animation pushed the character.
4. Track priority and weight. If the animation plays but "flickers" with the idle, or disappears after half a second, the problem is a priority fight. The AnimationPriority enum's order is Core, Idle, Movement, Action, Action2, Action3, Action4 — from weakest to strongest. The character's default Animate script plays idle and walking constantly; if your attack animation was exported as Idle, it ties with the default idle and the result is a character shaking between the two poses. An action animation has to be Action or above. You can force it in code without re-exporting:
local animator = humanoid:FindFirstChildOfClass("Animator")
local track = animator:LoadAnimation(anim)
track.Priority = Enum.AnimationPriority.Action
track:Play(0.1) -- a 0.1s fade, the defaultThe other side of that coin is a forgotten track still playing. A track you never stopped keeps adding weight on top of the others. If the character seems to be in two poses at once, count how many tracks are playing before blaming the rig.
5. The Animator, not Humanoid:LoadAnimation. Loading the track directly on the Humanoid has been deprecated for years. It still works in many cases, which is precisely the trap: it works in Studio in solo mode and fails or replicates wrongly on a real server. Always load through the Animator inside the Humanoid. For the player's own character, load on the client and replication comes for free; for an NPC, load on the server. If there is no Animator inside the Humanoid, no track will play and the Output will not warn you.
A sixth item worth checking when a limb appears "detached" from the body: scale. The Humanoid's scale values (height, width, depth, head size) rewrite the joints' C0 when the character spawns. An animation recorded on a scaled rig and applied to a standard rig will land on different offsets, and the result is a hand floating half a stud from the sleeve.
The script that points at the guilty joint in two seconds
The artifact below is a ModuleScript that does that whole sweep by itself. It compares the rig against the official R15 or R6 joint map (chosen by the Humanoid's RigType), flags a missing joint, inverted Part0 and Part1, a link pointing at the wrong part, a duplicate name, an Anchored part, a Weld fighting the Motor6D, an empty PrimaryPart and a missing Animator. Every line returned carries a severity, the joint's name and what is wrong — so you read the Output and go straight into the Explorer to fix that specific joint.
Put it as a ModuleScript in ServerScriptService named RigDoctor and call it like this, pointing at the model that is twisting:
local RigDoctor = require(script.Parent.RigDoctor)
for _, p in RigDoctor.diagnose(workspace.NPC) do
print(p.severity, p.joint, p.message)
end
for _, line in RigDoctor.listTracks(workspace.NPC) do
print(line)
endThe second function lists the tracks playing now with priority, weight and speed — it is the one that solves the "the animation plays but fights the idle" case. The second artifact, the JSON, is the same R15 joint map as a file: use it as a bone naming reference in Blender before exporting, and item 2's problem stops existing at the source.
The most expensive mistake: re-exporting instead of fixing the rig
The loss is not the re-export's time, it is the trail it leaves. Every published animation becomes a new, permanent asset ID, goes through moderation, and ends up scattered across four, five, ten different scripts. After three attempts you have three nearly identical IDs in the game and nobody remembers which is the good one. When somebody changes the rig again two months later, the cycle starts over.
The rule is: a re-exported animation only solves a pose problem; a broken rig is only solved in the rig. Before opening Blender, run the diagnosis. If it flags an inverted joint, a wrong name or an anchored part, Blender has nothing to do with it.
And there is the classic aggravating factor: "fixing" C0 at run time to compensate for an inverted Part0. You find the offset that makes that pose look right, commit it, and every other animation on that rig starts coming out crooked — because C0 is common to all of them. If you have already done that, the way back is deleting the compensations, restoring the Motor6D in the right direction in the source rig, and testing the animations one by one.
Keep a reference rig stored in ReplicatedStorage, untouched, and always record animations on it. And centralise the asset IDs in a single module table instead of scattering loose numbers through the scripts: when you need to swap an animation, you change it in one place.
What to measure afterwards
Run RigDoctor on every NPC at server boot and send the ERROR severity lines to the Output. If a defect appears in a rig that is already published, you find out before the player — and you find out on which NPC, with the joint's name, without opening each one's Explorer.
Then measure two things. The first: how many AnimationTracks are playing per character at match peak, using the track listing. A forgotten track playing is silent accumulation and it appears first as "a strange animation", then as an fps drop. The second: your re-export cycle's time, actually timed. If it goes past ten minutes, diagnosing before Blender pays for itself the first time you do not have to open Blender.
Finally, stop recording animations on a rig downloaded from the Toolbox. Always record on the rig that is in the game — it is the only way to guarantee the joint names match before you export anything.
Ready-to-use artifact
--!strict
-- RigDoctor: sweeps an R6/R15 rig and returns the list of defects that make
-- the animation twist, stretch or freeze in a T-pose.
-- It fixes nothing: it points at the guilty joint and what is wrong with it.
export type Problem = {
severity: string, -- "ERROR", "WARNING" or "OK"
joint: string,
message: string,
}
-- Official map: [joint name] = { expected Part0, expected Part1 }
local R15_JOINTS: { [string]: { string } } = {
Root = { "HumanoidRootPart", "LowerTorso" },
Waist = { "LowerTorso", "UpperTorso" },
Neck = { "UpperTorso", "Head" },
LeftShoulder = { "UpperTorso", "LeftUpperArm" },
LeftElbow = { "LeftUpperArm", "LeftLowerArm" },
LeftWrist = { "LeftLowerArm", "LeftHand" },
RightShoulder = { "UpperTorso", "RightUpperArm" },
RightElbow = { "RightUpperArm", "RightLowerArm" },
RightWrist = { "RightLowerArm", "RightHand" },
LeftHip = { "LowerTorso", "LeftUpperLeg" },
LeftKnee = { "LeftUpperLeg", "LeftLowerLeg" },
LeftAnkle = { "LeftLowerLeg", "LeftFoot" },
RightHip = { "LowerTorso", "RightUpperLeg" },
RightKnee = { "RightUpperLeg", "RightLowerLeg" },
RightAnkle = { "RightLowerLeg", "RightFoot" },
}
local R6_JOINTS: { [string]: { string } } = {
RootJoint = { "HumanoidRootPart", "Torso" },
Neck = { "Torso", "Head" },
["Left Shoulder"] = { "Torso", "Left Arm" },
["Right Shoulder"] = { "Torso", "Right Arm" },
["Left Hip"] = { "Torso", "Left Leg" },
["Right Hip"] = { "Torso", "Right Leg" },
}
local RigDoctor = {}
local function make(severity: string, joint: string, message: string): Problem
return { severity = severity, joint = joint, message = message }
end
-- Sweeps the rig and returns every defect found.
function RigDoctor.diagnose(model: Model): { Problem }
local problems: { Problem } = {}
local humanoid = model:FindFirstChildOfClass("Humanoid") :: Humanoid?
if humanoid == nil then
table.insert(problems, make("ERROR", "-", "Model with no Humanoid: there is no Animator and no track will play."))
return problems
end
if humanoid:FindFirstChildOfClass("Animator") == nil then
table.insert(problems, make("ERROR", "-", "Humanoid with no Animator. Create an Animator inside the Humanoid."))
end
if model.PrimaryPart == nil then
table.insert(problems, make("WARNING", "-", "PrimaryPart empty: PivotTo/MoveTo will drag the rig by the wrong pivot."))
end
local expected = if humanoid.RigType == Enum.HumanoidRigType.R15 then R15_JOINTS else R6_JOINTS
-- Indexes Motor6D by name, counts duplicates and flags Anchored/Weld along the way.
local motors: { [string]: Motor6D } = {}
local counts: { [string]: number } = {}
for _, d in model:GetDescendants() do
if d:IsA("Motor6D") then
counts[d.Name] = (counts[d.Name] or 0) + 1
motors[d.Name] = d
elseif d:IsA("BasePart") and d.Anchored then
table.insert(problems, make("ERROR", d.Name, "Anchored part: the animation cannot move that part."))
elseif d:IsA("Weld") or d:IsA("WeldConstraint") then
table.insert(problems, make("WARNING", d.Name, "Weld/WeldConstraint inside the rig fighting the Motor6D for position."))
end
end
for name, quantity in counts do
if quantity > 1 then
table.insert(problems, make("ERROR", name, string.format("%d Motor6D with the same name: the animation only sees one of them.", quantity)))
end
end
local checked = 0
for name, pair in expected do
checked += 1
local m = motors[name]
if m == nil then
table.insert(problems, make("ERROR", name, "Joint missing. The pose is recorded by joint name, so that limb stays in a T-pose."))
continue
end
local p0 = m.Part0
local p1 = m.Part1
if p0 == nil or p1 == nil then
table.insert(problems, make("ERROR", name, "Part0 or Part1 empty."))
continue
end
if p0.Name == pair[2] and p1.Name == pair[1] then
table.insert(problems, make("ERROR", name, string.format("Part0/Part1 inverted. Expected %s -> %s.", pair[1], pair[2])))
elseif p0.Name ~= pair[1] or p1.Name ~= pair[2] then
table.insert(problems, make("ERROR", name, string.format("Wrong link: %s -> %s. Expected %s -> %s.", p0.Name, p1.Name, pair[1], pair[2])))
elseif m.Parent ~= p0 then
table.insert(problems, make("WARNING", name, "Motor6D is not parented to Part0 (it works, but it departs from the rig standard)."))
end
end
for name in motors do
if expected[name] == nil then
table.insert(problems, make("WARNING", name, "Non-standard joint: check whether the animation was recorded with that exact name."))
end
end
if #problems == 0 then
table.insert(problems, make("OK", "-", string.format("Rig %s intact: %d joints checked.", humanoid.RigType.Name, checked)))
end
return problems
end
-- Lists the tracks playing now: solves the "the animation fights the idle" case.
function RigDoctor.listTracks(model: Model): { string }
local lines: { string } = {}
local humanoid = model:FindFirstChildOfClass("Humanoid") :: Humanoid?
local animator = if humanoid ~= nil then humanoid:FindFirstChildOfClass("Animator") :: Animator? else nil
if animator == nil then
table.insert(lines, "No Animator: no track can play.")
return lines
end
for _, track in animator:GetPlayingAnimationTracks() do
table.insert(lines, string.format(
"%s | priority=%s | weight=%.2f | speed=%.2f | loop=%s",
track.Name, track.Priority.Name, track.WeightCurrent, track.Speed, tostring(track.Looped)
))
end
if #lines == 0 then
table.insert(lines, "No track playing at this instant.")
end
return lines
end
return RigDoctor
{
"rigType": "R15",
"partCount": 16,
"motor6dCount": 15,
"note": "Part0 = parent (stays still), Part1 = child (rotates). The Motor6D lives inside Part0.",
"joints": [
{ "name": "Root", "part0": "HumanoidRootPart", "part1": "LowerTorso" },
{ "name": "Waist", "part0": "LowerTorso", "part1": "UpperTorso" },
{ "name": "Neck", "part0": "UpperTorso", "part1": "Head" },
{ "name": "LeftShoulder", "part0": "UpperTorso", "part1": "LeftUpperArm" },
{ "name": "LeftElbow", "part0": "LeftUpperArm", "part1": "LeftLowerArm" },
{ "name": "LeftWrist", "part0": "LeftLowerArm", "part1": "LeftHand" },
{ "name": "RightShoulder", "part0": "UpperTorso", "part1": "RightUpperArm" },
{ "name": "RightElbow", "part0": "RightUpperArm", "part1": "RightLowerArm" },
{ "name": "RightWrist", "part0": "RightLowerArm", "part1": "RightHand" },
{ "name": "LeftHip", "part0": "LowerTorso", "part1": "LeftUpperLeg" },
{ "name": "LeftKnee", "part0": "LeftUpperLeg", "part1": "LeftLowerLeg" },
{ "name": "LeftAnkle", "part0": "LeftLowerLeg", "part1": "LeftFoot" },
{ "name": "RightHip", "part0": "LowerTorso", "part1": "RightUpperLeg" },
{ "name": "RightKnee", "part0": "RightUpperLeg", "part1": "RightLowerLeg" },
{ "name": "RightAnkle", "part0": "RightLowerLeg", "part1": "RightFoot" }
]
}


