Animação para Roblox

Evento de animação: sincronizar som, dano e efeito com o quadro certo

Passo que faz barulho antes do pé encostar no chão é o sintoma de som disparado por temporizador. Marcador de evento na linha do tempo resolve isso, e o parâmetro dele transforma um marcador em vários.

por Gustavo Cantino9 min de leituraIntermediário

Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. 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.

Existe um defeito que todo jogo com animação própria passa: o som do passo sai um pouco antes do pé encostar no chão. Ou o dano acontece quando a espada ainda está subindo. Ou a poeira aparece depois do impacto.

A causa é quase sempre a mesma, e ela não está na animação: o efeito foi disparado por tempo, não por quadro. Alguém mediu que o pé encosta mais ou menos meio segundo depois do início, escreveu uma espera no script, e ficou. Funciona uma vez e desalinha em todas as outras.

A ferramenta certa para isso existe dentro do próprio editor de animação, e é o marcador de evento.

O problema

Disparar efeito por temporizador quebra por três motivos independentes, e basta um.

O primeiro é a velocidade. Animação tocada com AdjustSpeed diferente de um — correr mais rápido com um buff, atacar mais devagar com uma arma pesada — muda quando cada quadro acontece. O temporizador continua esperando o mesmo tanto.

O segundo é a edição. Bastou você mover um keyframe dois quadros para frente e o número que está escrito no script virou mentira. E nada avisa: o script continua rodando, o som continua saindo, só que na hora errada.

O terceiro é o mais chato de rastrear: com dois efeitos no mesmo movimento, o ajuste de um desalinha o outro, e você fica ajustando os dois em turnos.

O marcador de evento resolve os três de uma vez porque muda a pergunta. Em vez de "quanto tempo depois do início", ele responde "quando este quadro passar". Na definição oficial: dá para definir marcadores de evento ao longo da linha do tempo e detectá-los com GetMarkerReachedSignal() enquanto a animação roda.

A mudança de fundo é de quem manda. Com temporizador, quem decide o momento é o script, e a animação é um vídeo tocando ao lado — duas linhas do tempo paralelas que alguém tenta manter alinhadas à mão. Com marcador, quem decide é a animação, e o script só reage. Passa a existir uma fonte única da verdade sobre quando o pé encosta, e ela é o mesmo arquivo que desenha o pé encostando.

Isso tem um efeito prático de organização que só aparece depois: quem edita a animação deixa de precisar avisar quem escreve o script. O animador move o impacto dois quadros, salva, e o jogo continua certo. Sem marcador, essa combinação vira mensagem, reunião ou bug — e normalmente as três, nessa ordem.

Passo a passo

1. Mostre a faixa de eventos. Ela não vem visível. Vá até a direita da linha do tempo, clique no ícone de engrenagem e marque Show Animation Events. A barra de eventos abre logo abaixo dos controles de reprodução.

2. Posicione o scrubber. Arraste-o até o quadro exato onde o evento deve acontecer. Este passo vem primeiro de propósito, e é ele que dá precisão ao resto.

3. Crie o marcador. Na faixa de eventos, clique em Edit Animation Events. No diálogo, clique em + Add Event e digite o nome do evento. O nome é o que o script vai usar, então vale escolher algo estável — FootStep, HitFrame, Landed — e não algo descritivo demais que você vá querer mudar depois.

4. Preencha o parâmetro, se fizer sentido. O campo Parameter recebe uma string, e é opcional. A próxima seção mostra o que ele destrava.

5. Salve. Aparece um símbolo de marcador naquele quadro. Marcador de evento é exatamente isso — o indicador visual de onde o evento começa. E depois de criado, ele pode ser movido para qualquer quadro da linha do tempo, o que é a saída quando o ajuste fino muda o momento do impacto.

6. Escute no script. A detecção em LocalScript se faz conectando uma função ao GetMarkerReachedSignal() do AnimationTrack. A conexão é por nome de marcador: cada nome é um sinal diferente.

Artefato

O roteador abaixo resolve o problema que aparece no terceiro ou quarto evento: conexões espalhadas pelo código, cada uma feita num lugar, sem ninguém saber quantas existem.

Ele mantém uma tabela de ações por nome de marcador, liga todas de uma vez num track, e devolve as conexões para quem quiser desligar depois. Acrescentar um evento novo passa a ser acrescentar uma linha na tabela.

A leitura do parâmetro é a parte que vale copiar mesmo que o resto não sirva: o marcador entrega uma string, e a documentação diz que ela pode ser interpretada ou convertida e usada para a ação que se quiser. Ler no formato chave=valor separado por vírgula transforma um campo de texto num pequeno formato de configuração.

E repare no HitFrame: o cliente não aplica dano. Ele avisa o servidor de que o quadro de acerto passou, e quem decide é o servidor. Marcador de animação é informação de tempo, nunca autoridade.

Erro comum

O erro comum é criar um marcador por ocorrência. É natural: o pé esquerdo encosta, você cria FootStepLeft; o direito encosta, você cria FootStepRight. Em pouco tempo há oito nomes na animação e oito conexões no script.

A documentação aponta o caminho contrário em duas frases. A primeira: evento criado fica disponível na animação inteira, não só no quadro onde nasceu. A segunda, no exemplo oficial: dá para criar um marcador FootStep no ponto em que o pé encosta, e usar o mesmo evento quando o outro pé encosta.

O que distingue as duas ocorrências não é o nome, é o parâmetro. Qualquer marcador pode ter um valor de parâmetro definido no editor, e ele permite passar uma string própria ao GetMarkerReachedSignal() — inclusive separada por vírgula. Um marcador FootStep com parâmetro pe=esquerdo e outro com pe=direito fazem o trabalho dos oito, com uma conexão só.

Para duplicar sem redigitar: clique com o botão direito no marcador e escolha Copy Selected, depois botão direito em outro ponto da linha do tempo e Paste Events. Se o parâmetro da cópia precisa ser diferente, o caminho é Edit Animation Event no menu de contexto do marcador duplicado.

E aqui mora uma armadilha que é a mesma dos keyframes, e que pega pela segunda vez quem já foi pego uma: o evento copiado entra na posição de tempo mais próxima de onde você clicou — não onde está o scrubber. Você posiciona o scrubber com cuidado no quadro do impacto, clica com o botão direito num lugar conveniente da barra, e o marcador nasce no lugar conveniente. A diferença é de poucos quadros, o som sai quase certo, e ninguém desconfia do clique.

Próximo passo

Com os marcadores no lugar, vale fazer uma varredura no código atrás de espera fixa perto de animação. Qualquer task.wait com número cravado logo antes ou depois de um :Play() é candidato a virar marcador — e cada um que vira deixa de quebrar quando a animação for editada.

O passo seguinte é padronizar os nomes entre animações. Se toda animação de ataque tiver um HitFrame e toda animação de locomoção tiver um FootStep, o mesmo roteador atende o jogo inteiro, e trocar uma animação por outra deixa de exigir mexer no script.

É um contrato pequeno, escrito em nomes de marcador, e é o que separa animação que é arte de animação que é peça de software.

Uma conferência que vale entrar na rotina: depois de mexer em qualquer animação que tenha marcador, rode-a uma vez com um print no roteador antes de qualquer efeito. Se o marcador chegar, ele aparece no output; se não chegar, não aparece nada — e "não aparece nada" é justamente o resultado que um marcador renomeado, apagado ou colado no lugar errado produz.

Esse detalhe merece atenção porque a falha aqui é silenciosa por natureza. GetMarkerReachedSignal() conecta por nome, e um nome que não existe na animação não é erro: é uma conexão válida que nunca dispara. O jogo roda, a animação toca, e o som simplesmente não sai. Não há mensagem no console, não há exceção, não há nada a depurar — só a ausência. Escrever FootStep no editor e Footstep no script é o bastante, e a diferença de uma letra maiúscula não aparece em lugar nenhum.

Por isso vale manter os nomes de marcador numa tabela do lado do código, como a ACOES do artefato faz, em vez de digitados soltos em cada conexão. Um nome errado numa tabela continua sendo um nome errado — mas pelo menos está tudo numa tela só, e comparar com o editor vira questão de olhar.

Fontes

Como corrigimos erros

Artefato pronto pra usar

Roteador de eventos de animação com parâmetro separado por vírgula (LocalScript em StarterPlayerScripts)
lua
-- StarterPlayerScripts (LocalScript): um lugar só que escuta os marcadores
-- e roteia por NOME do evento, lendo o parâmetro como "chave=valor".
--
-- Por que rotear por nome em vez de conectar caso a caso: cada conexão do
-- GetMarkerReachedSignal é por NOME de marcador. Sem um roteador, cada som
-- novo vira uma conexão nova espalhada pelo código, e ninguém sabe mais
-- quantas existem numa faixa.

local Players = game:GetService("Players")
local SoundService = game:GetService("SoundService")

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")

-- "som=passo_pedra,volume=0.4" -> { som = "passo_pedra", volume = "0.4" }
local function lerParametro(texto)
	local campos = {}
	if type(texto) ~= "string" or texto == "" then
		return campos
	end
	for pedaco in string.gmatch(texto, "[^,]+") do
		local chave, valor = string.match(pedaco, "^%s*(.-)%s*=%s*(.-)%s*$")
		if chave then
			campos[chave] = valor
		else
			-- parâmetro sem "=" vale como valor solto, na ordem em que veio
			table.insert(campos, (string.gsub(pedaco, "^%s*(.-)%s*$", "%1")))
		end
	end
	return campos
end

-- nome do marcador -> o que fazer. Adicionar evento é adicionar linha AQUI.
local ACOES = {}

function ACOES.FootStep(campos)
	local raiz = character:FindFirstChild("HumanoidRootPart")
	if not raiz then
		return
	end

	local modelo = SoundService:FindFirstChild(campos.som or "passo_padrao")
	if not modelo or not modelo:IsA("Sound") then
		warn("marcador FootStep pediu som inexistente: " .. tostring(campos.som))
		return
	end

	local som = modelo:Clone()
	som.Volume = tonumber(campos.volume) or modelo.Volume
	som.Parent = raiz
	som:Play()
	som.Ended:Once(function()
		som:Destroy()
	end)
end

function ACOES.HitFrame(campos)
	-- O CLIENTE NÃO APLICA DANO. Ele só avisa o servidor de que o quadro
	-- de acerto passou; quem valida alcance, tempo e alvo é o servidor.
	local remote = game:GetService("ReplicatedStorage"):FindFirstChild("QuadroDeAcerto")
	if remote and remote:IsA("RemoteEvent") then
		remote:FireServer(campos.parte or "mao_direita")
	end
end

-- Liga um track a todos os marcadores conhecidos, e devolve as conexões
-- para quem carregou poder desligar depois.
local function ligar(track)
	local conexoes = {}
	for nome, acao in ACOES do
		table.insert(
			conexoes,
			track:GetMarkerReachedSignal(nome):Connect(function(parametro)
				local ok, erro = pcall(acao, lerParametro(parametro))
				if not ok then
					warn("evento " .. nome .. " falhou: " .. tostring(erro))
				end
			end)
		)
	end
	return conexoes
end

-- exemplo de uso
local caminhada = Instance.new("Animation")
caminhada.AnimationId = "rbxassetid://5432167890"

local track = animator:LoadAnimation(caminhada)
ligar(track)
track:Play()
Neste artigo

Continue por aqui