Debounce: por que a sua armadilha tira a vida inteira do jogador de uma vez
O evento Touched dispara várias vezes em sequência por causa de colisões físicas sutis. Sem debounce, uma armadilha de 10 de dano tira muito mais que 10 — e o mesmo vale para som, coletável e recarga.
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

Você faz uma armadilha que tira 10 de vida. O jogador pisa nela e morre na hora, com a barra inteira indo a zero. O código está certo, o número está certo, e o resultado está errado.
É o primeiro encontro de quase todo mundo com o problema que o debounce resolve, e a causa não está no seu script — está na física.
O problema
A definição oficial é direta: debounce é a técnica que impede uma função de rodar vezes demais ou uma entrada de disparar várias vezes.
O exemplo que a documentação usa é exatamente o da armadilha: uma peça que causa 10 de dano ao ser tocada. A implementação ingênua conecta uma função ao Touched e subtrai 10 da vida do Humanoid. Lê bem, parece certo.
E aí vem a frase que explica o mistério: o teste mostra que o evento Touched dispara várias vezes em rápida sucessão, por causa de colisões físicas sutis.
Isso não é bug do motor. Um personagem em cima de uma peça não fica parado — ele oscila em frações de stud, cada perna toca em momentos diferentes, a colisão se resolve e se refaz. Cada um desses micro-contatos é um Touched. O seu dano de 10 vira 10 vezes o número de contatos, e o número de contatos não é previsível.
A saída, na própria documentação, é um sistema de debounce que impõe um período de recarga no dano, por meio de um atributo da instância.
Passo a passo
O padrão tem sempre a mesma forma, em três passos: conferir se já está em recarga, marcar que está, fazer o trabalho, e desmarcar depois do tempo.
1. Escolha onde guardar o estado. A documentação usa um atributo da instância, e a escolha tem uma vantagem que não é óbvia: atributo aparece no Explorer durante o teste. Dá para ver o estado do debounce mudando na tela, sem print nenhum.
2. Defina o tempo. O exemplo da armadilha usa um tempo de reinício de um segundo. Não há número universal — o tempo certo é o que faz o dano acontecer uma vez por "pisada", e isso depende do seu jogo.
3. Aplique a três famílias de caso. A documentação percorre três, e elas cobrem quase tudo:
Colisão e dano. O caso da armadilha. O atributo entra em true, o dano acontece, espera-se o tempo, o atributo volta a false.
Som. Debounce também serve para efeito sonoro — som ao dois objetos colidirem, som ao clicar num botão. O motivo é mecânico: chamar o Play() do som recomeça a reprodução do início da faixa. Sem debounce, o som reinicia a cada disparo e vira um estalo.
E aqui a documentação dá uma solução mais elegante que um atributo: para evitar sobreposição, o debounce usa a propriedade IsPlaying do objeto Sound. O estado já existe — não é preciso inventar outro.
Coletável com recarga. Jogos costumam ter coletáveis no mundo 3D, como kits médicos e pacotes de munição. Se o coletável deve permanecer no mundo para ser pego de novo, é preciso um tempo de recarga antes de ele reativar.
Assim como na detecção de colisão, o estado é gerenciado por um atributo da instância. O exemplo do coletável de vida usa cinco segundos de recarga e devolve 25 de vida. E acrescenta uma coisa que vale copiar: durante a recarga a peça fica semitransparente, o que torna o estado visível para o jogador.
4. Deixe o estado visível. O exemplo do coletável faz algo que vale copiar em quase todo caso: além de bloquear, ele mostra. A peça semitransparente durante a recarga responde, sem texto nenhum, a pergunta que o jogador faria — "por que não funcionou?".
Isso separa dois problemas que se parecem na tela. Sem retorno visual, um coletável em recarga e um coletável quebrado são indistinguíveis para quem está jogando, e o jogador conclui a segunda hipótese. Com retorno, ele espera.
O mesmo princípio vale para recarga de arma, porta que está fechando e botão que já foi apertado: o estado precisa ser legível de fora, não só verdadeiro por dentro.
Artefato
O módulo abaixo separa duas coisas que o exemplo oficial mantém juntas, e a separação é uma decisão de jogo, não de implementação.
O exemplo guarda o estado num atributo da peça. Isso resolve o caso do coletável perfeitamente: o objeto é que fica indisponível. Mas aplicado a uma armadilha, produz um comportamento errado assim que duas pessoas pisam nela — enquanto um jogador está em recarga, o outro não toma dano nenhum, porque a recarga é da peça.
Então: porPeca para quando o objeto fica indisponível — coletável, alavanca, porta. porJogador para quando o que se limita é a pessoa — dano, tiro, ação.
O terceiro uso do exemplo, o som, não usa o módulo de propósito: o IsPlaying já é a fonte da verdade, e criar um atributo ao lado seria manter dois estados para a mesma coisa.
Vale registrar um detalhe de vocabulário que confunde quem procura por isso: "debounce" vem da eletrônica, do ruído mecânico de um contato que oscila ao fechar. O paralelo é exato — a colisão física no Roblox produz o mesmo tipo de ruído, e a solução é a mesma: ignorar o que vier dentro de uma janela de tempo depois do primeiro sinal válido.
Erro comum
O debounce de peça numa armadilha. É o erro descrito acima, e ele é traiçoeiro porque o teste de uma pessoa só passa. Você testa sozinho no Studio, funciona, publica. Com dois jogadores, a armadilha vira loteria.
O task.wait bloqueando dentro do handler. O padrão do exemplo usa espera dentro da função conectada, e isso é aceitável ali. Mas em código que faz mais coisas, esperar dentro do handler segura aquela execução inteira — e como cada Touched roda em sua própria thread, o resultado é um acúmulo de threads esperando. O artefato usa task.delay para desmarcar, o que não segura nada.
Esquecer de desmarcar quando a peça some. Se a peça for destruída durante a recarga, a função que ia desmarcar ainda roda. Com atributo, escrever numa peça destruída é problema; com tabela indexada por jogador, o estado fica pendurado para sempre. As duas correções estão no artefato — conferir o Parent antes de escrever, e limpar no PlayerRemoving.
Usar debounce onde o problema é outro. Se o coletável deve sumir quando pego, não há debounce nenhum: o objeto é destruído, e o Touched não dispara mais porque não existe mais peça. Debounce é para o que permanece.
Uma variável única para tudo. Um local debounce = false no topo de um script que atende vários objetos faz todos compartilharem o mesmo estado. É o mesmo erro do escopo, com outra roupa — e é mais difícil de ver, porque a variável parece local.
Há ainda uma decisão de arquitetura que vale tomar cedo: onde o debounce vive. Espalhado pelos scripts, cada objeto tem a própria variante, e a terceira delas já estará errada. Num módulo, como o do artefato, a regra é uma só e a escolha de escopo fica explícita na chamada — quem lê o código vê se aquilo limita a peça ou a pessoa, sem precisar descobrir.
Próximo passo
O conceito de recarga se estende além do que a documentação usa como exemplo, e ela mesma diz isso: o mesmo conceito serve para outros casos, como limitar a frequência com que o jogador dispara foguetes.
Essa frase é a ponte para o uso mais importante do padrão: ele deixa de ser um conserto de física e vira regra de jogo. Tempo de recarga de arma, intervalo entre habilidades, limite de quantas vezes se abre um baú — tudo é debounce com outro nome, e tudo é decisão de balanceamento.
Quando o debounce vira regra de jogo, duas coisas mudam. O tempo passa a ser um número que se ajusta com frequência, então ele deve morar num lugar fácil de mexer — um atributo, um módulo de configuração — e não escrito dentro da função. E o estado precisa ser visível ao jogador: recarga que o jogador não vê é recarga que ele interpreta como o jogo não estar respondendo. A peça semitransparente do exemplo oficial é a versão mais simples disso, e funciona.
O exercício para hoje: procure no seu código todo Touched que mexe em vida, moeda ou inventário. Cada um sem debounce é um multiplicador aleatório esperando dois jogadores no mesmo lugar.
Fontes
- Debounce patterns | Documentation - Roblox Creator Hub — consultada em
Artefato pronto pra usar
-- ModuleScript "Debounce"
--
-- O exemplo oficial guarda o estado num ATRIBUTO da peça, e isso resolve
-- o caso de uma armadilha. Ele para de resolver no momento em que duas
-- pessoas pisam na mesma armadilha: o atributo é da PEÇA, então enquanto
-- um jogador está em recarga, o outro não toma dano nenhum.
--
-- Este módulo separa os dois escopos, porque a escolha entre eles é uma
-- decisão de JOGO, não de implementação:
--
-- porPeca -> a peça inteira entra em recarga. Coletável, alavanca,
-- porta: o objeto é que fica indisponível.
-- porJogador -> cada jogador tem a própria recarga. Armadilha, dano,
-- tiro: o que se limita é a pessoa.
local Debounce = {}
-- ── escopo POR PEÇA (o do exemplo oficial) ──────────────────────────
-- Usa atributo mesmo: fica visível no Explorer durante o Play, o que
-- torna o estado inspecionável sem print nenhum.
function Debounce.porPeca(peca, chave, segundos)
if peca:GetAttribute(chave) then
return false
end
peca:SetAttribute(chave, true)
task.delay(segundos, function()
if peca.Parent then
peca:SetAttribute(chave, false)
end
end)
return true
end
-- ── escopo POR JOGADOR ──────────────────────────────────────────────
local ultimo = {}
function Debounce.porJogador(player, chave, segundos)
local agora = os.clock()
ultimo[player] = ultimo[player] or {}
local anterior = ultimo[player][chave]
if anterior and (agora - anterior) < segundos then
return false
end
ultimo[player][chave] = agora
return true
end
game:GetService("Players").PlayerRemoving:Connect(function(player)
ultimo[player] = nil
end)
return Debounce
--[[
═══════════════════════════════════════════════════════════════════════
OS TRÊS USOS, com o escopo certo em cada um
═══════════════════════════════════════════════════════════════════════
-- 1. ARMADILHA: por JOGADOR. Dois jogadores em cima dela tomam dano
-- independentemente, que é o comportamento que o jogador espera.
local Players = game:GetService("Players")
local armadilha = script.Parent
armadilha.Touched:Connect(function(outraParte)
local humanoid = outraParte.Parent:FindFirstChildWhichIsA("Humanoid")
if not humanoid or humanoid.Health <= 0 then return end
local player = Players:GetPlayerFromCharacter(outraParte.Parent)
if not player then return end
if Debounce.porJogador(player, "armadilha_" .. armadilha:GetDebugId(), 1) then
humanoid.Health -= 10
end
end)
-- 2. COLETÁVEL COM RECARGA: por PEÇA, com o estado VISÍVEL.
local kit = script.Parent
kit.Anchored = true
kit.CanCollide = false
kit.Touched:Connect(function(outraParte)
local humanoid = outraParte.Parent:FindFirstChildWhichIsA("Humanoid")
if not humanoid then return end
if Debounce.porPeca(kit, "CoolingDown", 5) then
humanoid.Health += 25
kit.Transparency = 0.75
task.delay(5, function()
if kit.Parent then kit.Transparency = 0 end
end)
end
end)
-- 3. SOM: não precisa deste módulo. O próprio objeto já carrega o
-- estado, e IsPlaying é a fonte da verdade -- inventar um atributo
-- ao lado criaria dois estados para a mesma coisa.
local projetil = script.Parent
projetil.Touched:Connect(function()
local som = projetil:FindFirstChild("Impact")
if som and not som.IsPlaying then
som:Play()
end
end)
]]


