Animação para Roblox

Por que meu rig está torcendo, esticando e travando em T-pose

Seu rig gira o braço em torno do torso, estica a perna pro outro lado do mapa ou trava em T-pose enquanto a animação roda lisa no Animation Editor. O defeito quase nunca está na animação: está no Motor6D do rig que está no Workspace, e dá pra apontar a junta culpada em dois segundos.

por Gustavo Cantino13 min de leituraIntermediário

Rascunho assistido por IA, revisado, testado e editado por um humano antes de publicar. Ver Política Editorial. · Revisado por Gustavo Cantino, 20/09/2026 Política Editorial

Placa tipográfica com a categoria Animação para Roblox em destaque, o nível intermediário abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

Se o seu rig está "fazendo isso" — o braço girando em torno do torso inteiro, a perna esticando até o outro lado do mapa, a cabeça descolando do pescoço, o personagem plantado em T-pose enquanto a mesma animação roda lisa no Animation Editor —, ele não está bugado. Ele está obedecendo, à risca, a hierarquia de Motor6D que existe no Workspace agora. E essa hierarquia está diferente da que existia quando você gravou a animação.

Seu rig não está bugado, ele está obedecendo

Vale entender o mecanismo antes de sair mexendo em coisa. Uma animação do Roblox não guarda "o braço subiu 30 graus" amarrado a um objeto específico: ela é uma KeyframeSequence que guarda uma pose por nome de junta, dentro de uma hierarquia de nomes. Quando a track toca, o Animator procura no rig uma junta chamada LeftShoulder, aplica a transformação naquele Motor6D e segue em frente. Se essa junta tiver outro nome, apontar para outra parte, estiver com Part0 e Part1 invertidos ou simplesmente não existir, o Animator não solta erro nenhum no Output. Ele aplica onde encontra e ignora o resto. O espaguete que você está vendo é o rig fazendo o que foi mandado.

A parte cara é o que a maioria faz em seguida: assume que a animação está torta e volta pro Blender. O ciclo completo tem seis etapas — ajustar no Blender, exportar FBX, importar no Animation Editor, exportar a animação, publicar, esperar a moderação e trocar o asset ID nos scripts. Você repete isso três vezes, queima uma tarde inteira, gera três asset IDs novos que ficam pendurados no seu jogo pra sempre, e o rig continua torcendo. Porque o defeito nunca esteve no arquivo: estava no modelo que está dentro do Workspace.

O contorno numérico é simples e serve de primeiro teste. Um rig R15 padrão tem 16 partes e exatamente 15 Motor6D: Root, Waist, Neck, os três de cada braço (Shoulder, Elbow, Wrist) e os três de cada perna (Hip, Knee, Ankle). Um rig R6 tem 7 partes e 6 juntas: RootJoint, Neck, Left Shoulder, Right Shoulder, Left Hip e Right Hip — com espaço no nome, e todas parenteadas no Torso. Se o seu rig não bate com esses números, você já tem por onde começar antes de abrir qualquer software 3D.

Os cinco diagnósticos, na ordem certa

1. Conte as juntas e confira Part0 e Part1. Selecione o rig no Explorer, olhe quantos Motor6D existem e abra cada um dos suspeitos. Part0 é o pai (fica parado), Part1 é o filho (é o que gira). No LeftShoulder de um R15, Part0 tem que ser UpperTorso e Part1 tem que ser LeftUpperArm. Se estiver assim, o braço gira em torno do ombro. Se estiver ao contrário, o torso inteiro é que passa a girar em torno do braço — que é exatamente o "meu rig gira todo quando eu movo o braço" que você está vendo. O jeito errado de consertar, e que gente faz direto em tempo de execução:

Script em ServerScriptService (o jeito ERRADO)
local m = npc.UpperTorso.LeftShoulder
m.Part0 = npc.LeftUpperArm -- filho virou pai
m.Part1 = npc.UpperTorso
-- agora a animação inteira gravada nesse rig está deslocada

Inverter Part0 e Part1 no runtime não "desfaz" nada: o C0 e o C1 continuam calculados pro sentido antigo, e você troca um bug por outro mais difícil de enxergar. Conserta no rig-fonte, no Studio, com o jogo parado.

2. Confira nome por nome, inclusive espaços e maiúsculas. Esse é o campeão silencioso. Você renomeou UpperTorso pra Torso_01 no Blender, ou o artista exportou com sufixo .001, ou alguém duplicou um membro e o Studio criou LeftUpperArm e LeftUpperArm com o mesmo nome. A animação continua procurando o nome antigo, não acha, e aquele membro fica congelado na pose neutra — daí o efeito "metade do corpo anima, metade fica em T-pose". Em R6 o erro clássico é gravar Left Shoulder sem o espaço. Nome de junta é string, e string não perdoa.

3. Anchored, Weld sobrando e RootPart. Se qualquer parte do rig estiver com Anchored marcado, o Motor6D não consegue mover ela, e todo o resto passa a orbitar aquela parte travada. Isso acontece direto quando você arrasta o rig pelo mapa e, pra ele não cair, ancorou um membro em vez do HumanoidRootPart. Na mesma linha: WeldConstraint ou Weld antigo entre duas partes que já têm um Motor6D ligando as duas. Os dois tentam resolver a mesma posição, o resultado é o membro tremendo ou colado no lugar errado. E confira se Model.PrimaryPart aponta pro HumanoidRootPart — sem isso, PivotTo e MoveTo arrastam o rig pelo pivô errado e você jura que a animação empurrou o personagem.

4. Prioridade e peso das tracks. Se a animação toca mas fica "piscando" com a idle, ou some depois de meio segundo, o problema é briga de prioridade. A ordem do enum AnimationPriority é Core, Idle, Movement, Action, Action2, Action3, Action4 — do mais fraco pro mais forte. O script Animate padrão do personagem toca idle e caminhada o tempo todo; se a sua animação de ataque foi exportada como Idle, ela empata com a idle padrão e o resultado é o personagem tremendo entre as duas poses. Animação de ação tem que ser Action pra cima. Dá pra forçar em código sem reexportar:

Script em ServerScriptService
local animator = humanoid:FindFirstChildOfClass("Animator")
local track = animator:LoadAnimation(anim)
track.Priority = Enum.AnimationPriority.Action
track:Play(0.1) -- fade de 0.1s, o padrão

O outro lado dessa moeda é track esquecida tocando. Uma track que você nunca deu Stop continua somando peso em cima das outras. Se o personagem parece estar em duas poses ao mesmo tempo, conte quantas tracks estão tocando antes de culpar o rig.

5. Animator, e não Humanoid:LoadAnimation. Carregar a track direto no Humanoid está deprecado há anos. Ainda funciona em muitos casos, o que é justamente a armadilha: funciona no Studio em modo solo e falha ou replica errado no servidor de verdade. Carregue sempre pelo Animator que fica dentro do Humanoid. Para o personagem do próprio jogador, carregue no cliente e a replicação sai de graça; para NPC, carregue no servidor. Se não existe Animator dentro do Humanoid, nenhuma track vai tocar e o Output não vai te avisar.

Um sexto item que vale a checagem quando o membro aparece "descolado" do corpo: escala. Os valores de escala do Humanoid (altura, largura, profundidade, tamanho da cabeça) reescrevem os C0 das juntas quando o personagem nasce. Animação gravada num rig escalado e aplicada num rig padrão vai bater em offsets diferentes, e o resultado é mão flutuando a meio stud da manga.

O script que aponta a junta culpada em dois segundos

O artefato abaixo é um ModuleScript que faz essa varredura inteira sozinho. Ele compara o rig contra o mapa oficial de juntas R15 ou R6 (escolhido pelo RigType do Humanoid), acusa junta ausente, Part0 e Part1 invertidos, ligação apontando pra parte errada, nome duplicado, parte Anchored, Weld disputando com Motor6D, PrimaryPart vazio e Animator faltando. Cada linha devolvida traz gravidade, nome da junta e o que está errado — então você lê o Output e vai direto no Explorer consertar aquela junta específica.

Coloque como ModuleScript em ServerScriptService com o nome RigDoctor e chame assim, apontando pro modelo que está torcendo:

Script em ServerScriptService
local RigDoctor = require(script.Parent.RigDoctor)
for _, p in RigDoctor.diagnosticar(workspace.NPC) do
	print(p.gravidade, p.junta, p.mensagem)
end
for _, linha in RigDoctor.listarTracks(workspace.NPC) do
	print(linha)
end

A segunda função lista as tracks tocando agora com prioridade, peso e velocidade — é ela que resolve o caso "a animação toca mas fica brigando com a idle". O segundo artefato, o JSON, é o mesmo mapa de juntas R15 em formato de arquivo: use como referência de nomenclatura de bones no Blender antes de exportar, e o problema do item 2 deixa de existir na origem.

O erro mais caro: reexportar em vez de consertar o rig

O prejuízo não é o tempo do reexport, é o rastro que ele deixa. Cada animação publicada vira um asset ID novo e permanente, passa por moderação, e vai parar espalhada em quatro, cinco, dez scripts diferentes. Depois de três tentativas você tem três IDs quase iguais no jogo e ninguém lembra qual é o bom. Quando alguém troca o rig de novo daqui a dois meses, o ciclo recomeça.

A regra é: animação reexportada só resolve problema de pose; rig quebrado só se resolve no rig. Antes de abrir o Blender, rode o diagnóstico. Se ele acusar junta invertida, nome errado ou parte ancorada, o Blender não tem nada a ver com isso.

E tem o agravante clássico: "consertar" o C0 em tempo de execução pra compensar um Part0 invertido. Você acha o offset que faz aquela pose parecer certa, comita, e todas as outras animações daquele rig passam a ficar tortas — porque o C0 é comum a todas elas. Se você já fez isso, o caminho de volta é apagar as compensações, restaurar o Motor6D no sentido certo no rig-fonte e testar as animações uma por uma.

Mantenha um rig de referência guardado no ReplicatedStorage, intocado, e sempre grave animação nele. E centralize os asset IDs numa tabela única de módulo em vez de espalhar número solto pelos scripts: quando precisar trocar uma animação, você muda em um lugar.

O que medir depois

Rode o RigDoctor em todos os NPCs no boot do servidor e mande as linhas de gravidade ERRO pro Output. Se aparecer defeito em rig que já está publicado, você descobre antes do jogador — e descobre em qual NPC, com o nome da junta, sem abrir o Explorer de cada um.

Depois meça duas coisas. A primeira: quantas AnimationTracks estão tocando por personagem em pico de partida, usando a listagem de tracks. Track esquecida tocando é acúmulo silencioso e aparece primeiro como "animação estranha", depois como queda de fps. A segunda: o tempo do seu ciclo de reexport, cronometrado. Se ele passa de dez minutos, o diagnóstico antes do Blender se paga na primeira vez que você não precisa abrir o Blender.

Por último, pare de gravar animação em rig baixado do Toolbox. Grave sempre no rig que está no jogo — é a única forma de garantir que os nomes de junta batem antes de você exportar qualquer coisa.

Artefato pronto pra usar

ModuleScript em ServerScriptService (nome: RigDoctor)
lua
--!strict
-- RigDoctor: varre um rig R6/R15 e devolve a lista de defeitos que fazem
-- a animacao torcer, esticar ou travar em T-pose.
-- Nao conserta nada: aponta a junta culpada e o que esta errado nela.

export type Problema = {
	gravidade: string, -- "ERRO", "AVISO" ou "OK"
	junta: string,
	mensagem: string,
}

-- Mapa oficial: [nome da junta] = { Part0 esperado, Part1 esperado }
local JUNTAS_R15: { [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 JUNTAS_R6: { [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 novo(gravidade: string, junta: string, mensagem: string): Problema
	return { gravidade = gravidade, junta = junta, mensagem = mensagem }
end

-- Varre o rig e devolve todos os defeitos encontrados.
function RigDoctor.diagnosticar(modelo: Model): { Problema }
	local problemas: { Problema } = {}

	local humanoid = modelo:FindFirstChildOfClass("Humanoid") :: Humanoid?
	if humanoid == nil then
		table.insert(problemas, novo("ERRO", "-", "Modelo sem Humanoid: nao existe Animator e nenhuma track vai tocar."))
		return problemas
	end

	if humanoid:FindFirstChildOfClass("Animator") == nil then
		table.insert(problemas, novo("ERRO", "-", "Humanoid sem Animator. Crie um Animator dentro do Humanoid."))
	end

	if modelo.PrimaryPart == nil then
		table.insert(problemas, novo("AVISO", "-", "PrimaryPart vazio: PivotTo/MoveTo vao arrastar o rig pelo pivo errado."))
	end

	local esperado = if humanoid.RigType == Enum.HumanoidRigType.R15 then JUNTAS_R15 else JUNTAS_R6

	-- Indexa Motor6D por nome, conta duplicados e acusa Anchored/Weld no caminho.
	local motores: { [string]: Motor6D } = {}
	local contagem: { [string]: number } = {}
	for _, d in modelo:GetDescendants() do
		if d:IsA("Motor6D") then
			contagem[d.Name] = (contagem[d.Name] or 0) + 1
			motores[d.Name] = d
		elseif d:IsA("BasePart") and d.Anchored then
			table.insert(problemas, novo("ERRO", d.Name, "Parte Anchored: a animacao nao consegue mover essa parte."))
		elseif d:IsA("Weld") or d:IsA("WeldConstraint") then
			table.insert(problemas, novo("AVISO", d.Name, "Weld/WeldConstraint dentro do rig disputando posicao com o Motor6D."))
		end
	end

	for nome, quantidade in contagem do
		if quantidade > 1 then
			table.insert(problemas, novo("ERRO", nome, string.format("%d Motor6D com o mesmo nome: a animacao so enxerga um deles.", quantidade)))
		end
	end

	local conferidas = 0
	for nome, par in esperado do
		conferidas += 1
		local m = motores[nome]
		if m == nil then
			table.insert(problemas, novo("ERRO", nome, "Junta ausente. A pose e gravada por nome de junta, entao esse membro fica em T-pose."))
			continue
		end

		local p0 = m.Part0
		local p1 = m.Part1
		if p0 == nil or p1 == nil then
			table.insert(problemas, novo("ERRO", nome, "Part0 ou Part1 vazio."))
			continue
		end

		if p0.Name == par[2] and p1.Name == par[1] then
			table.insert(problemas, novo("ERRO", nome, string.format("Part0/Part1 invertidos. Esperado %s -> %s.", par[1], par[2])))
		elseif p0.Name ~= par[1] or p1.Name ~= par[2] then
			table.insert(problemas, novo("ERRO", nome, string.format("Ligacao errada: %s -> %s. Esperado %s -> %s.", p0.Name, p1.Name, par[1], par[2])))
		elseif m.Parent ~= p0 then
			table.insert(problemas, novo("AVISO", nome, "Motor6D nao esta parenteado no Part0 (funciona, mas foge do padrao do rig)."))
		end
	end

	for nome in motores do
		if esperado[nome] == nil then
			table.insert(problemas, novo("AVISO", nome, "Junta fora do padrao: confira se a animacao foi gravada com esse nome exato."))
		end
	end

	if #problemas == 0 then
		table.insert(problemas, novo("OK", "-", string.format("Rig %s integro: %d juntas conferidas.", humanoid.RigType.Name, conferidas)))
	end

	return problemas
end

-- Lista as tracks tocando agora: resolve o caso "a animacao briga com a idle".
function RigDoctor.listarTracks(modelo: Model): { string }
	local linhas: { string } = {}
	local humanoid = modelo:FindFirstChildOfClass("Humanoid") :: Humanoid?
	local animator = if humanoid ~= nil then humanoid:FindFirstChildOfClass("Animator") :: Animator? else nil

	if animator == nil then
		table.insert(linhas, "Sem Animator: nenhuma track pode tocar.")
		return linhas
	end

	for _, track in animator:GetPlayingAnimationTracks() do
		table.insert(linhas, string.format(
			"%s | prioridade=%s | peso=%.2f | velocidade=%.2f | loop=%s",
			track.Name, track.Priority.Name, track.WeightCurrent, track.Speed, tostring(track.Looped)
		))
	end

	if #linhas == 0 then
		table.insert(linhas, "Nenhuma track tocando neste instante.")
	end

	return linhas
end

return RigDoctor
Arquivo rig-r15.json na pasta do projeto (referência de nomes para o export do Blender)
json
{
  "rigType": "R15",
  "partCount": 16,
  "motor6dCount": 15,
  "note": "Part0 = pai (fica parado), Part1 = filho (gira). O Motor6D mora dentro do 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" }
  ]
}
Neste artigo

Continue por aqui