Debounce is a technique that prevents a function or event from firing again until a set amount of time passes. Think of it like a cooldown. If a player clicks a button, debounce ensures they can't trigger it again for 0.5 seconds (or however long you set). Without debounce, a single click might register multiple times accidentally, causing bugs or unfair behavior.
Debounce is one of the most important patterns you'll use in Roblox scripting. Whether you're building combat, handling button clicks, or processing pickups, debounce keeps your code stable and fair.
What Is Debounce?
Debounce uses a boolean flag and time delays to control how often code runs. Here's the core idea:
- A variable tracks whether the action is currently on cooldown.
- When the event fires, check the flag first.
- If cooldown is active, ignore the event.
- If cooldown is off, run the code and start the cooldown.
- After the cooldown duration, reset the flag.
The word "debounce" comes from electronics: a switch bounces when pressed, triggering multiple signals. Debouncing filters out those unwanted repeats.
The Simple Pattern
Here's the basic structure you'll use in almost every debounce scenario:
local debounce = false
local cooldownTime = 0.5
local function doSomething()
if debounce then
return -- Exit early if on cooldown
end
debounce = true -- Lock the action
-- Your code here
print("Action executed!")
wait(cooldownTime) -- Wait for cooldown duration
debounce = false -- Unlock the action
end
-- Call your function
doSomething()The flow is simple: check the flag, lock it, do work, wait, unlock. Every time someone tries to trigger the action during the wait, the first if statement blocks them.
debounce = true before the work happens. If you wait first and then set the flag, two calls might both pass the check.Why Debounce Matters
Without debounce, players can spam actions and break your game:
- Button clicks: A player rapidly clicks a buy button and gets charged multiple times or receives multiple rewards.
- Combat: A player clicks to attack once, but the script triggers damage 5 times in one frame.
- Pickups: A player collides with a coin, and both the player and coin's touch event fire, applying the effect twice.
- Teleports: A touch event teleports the player, then they're still touching it and get teleported again.
Debounce solves all these by enforcing a minimum time between triggers.
Debounce With a Cooldown Timer
For more control, use a numeric variable instead of just a boolean. This tracks the exact time you can act again:
local lastActionTime = 0
local cooldownDuration = 1 -- 1 second cooldown
local function takeAction()
local currentTime = tick() -- Get the current time in seconds
if currentTime < lastActionTime + cooldownDuration then
return -- Still on cooldown
end
-- Action is allowed
print("Action executed at " .. currentTime)
lastActionTime = currentTime
-- Your code here
end
-- Test it
takeAction()
wait(0.5)
takeAction() -- Blocked (still cooling down)
wait(0.6)
takeAction() -- Allowed (1.1 seconds have passed)Using tick() is more precise than a boolean. You don't need to explicitly wait; the next call just checks whether enough time has passed. This pattern is great for games where you need to show cooldown UI or track exact timings.
Common Mistakes
Mistake 1: Setting debounce after the wait.
if debounce then return end
print("Action!")
wait(0.5)
debounce = false -- Too late! Requests during wait() slip throughMistake 2: Not unlocking the debounce.
if debounce then return end
debounce = true
print("Action!")
-- Missing: debounce = false
-- Now the action is locked foreverMistake 3: Using a shared debounce for multiple players.
local sharedDebounce = false
local function giveReward(player)
if sharedDebounce then return end
sharedDebounce = true
player:WaitForChild("leaderstats").Gold.Value += 10
wait(1)
sharedDebounce = false -- Blocks ALL players from getting rewards during this wait!
endIf you're handling multiple players, each player needs their own debounce. Store them in a table keyed by player ID or use a debounce inside the player's script.
local playerDebounces = {} and check playerDebounces[player.UserId].Real-World Example: Click to Damage
Here's how debounce works in a combat scenario. Say you have a humanoid and a click detector:
local part = script.Parent -- The part this script is in
local clickDetector = part:WaitForChild("ClickDetector")
local humanoid = part.Parent:WaitForChild("Humanoid") -- Assumes this part is in a model with a humanoid
local debounce = false
local damageAmount = 10
local cooldown = 0.5
local function takeDamage()
if debounce then
return
end
debounce = true
humanoid:TakeDamage(damageAmount)
print("Hit! Health is now " .. humanoid.Health)
wait(cooldown)
debounce = false
end
clickDetector.MouseClick:Connect(takeDamage)Now when a player clicks the part, they trigger damage once per 0.5 seconds maximum. Rapid clicks are ignored. The humanoid can't be damaged again until the cooldown finishes.
If you want this to work for a player's weapon instead, store the debounce on the player or in a module:
local player = game.Players:GetPlayerFromCharacter(script.Parent)
local lastAttackTime = 0
local attackCooldown = 0.6
local UserInputService = game:GetService("UserInputService")
local function attack()
if tick() - lastAttackTime < attackCooldown then
return -- Still on cooldown
end
lastAttackTime = tick()
print(player.Name .. " attacked!")
-- Fire a remote or play an animation here
end
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.KeyCode == Enum.KeyCode.E then
attack()
end
end)Every time the player presses E, the code checks if 0.6 seconds have passed since the last attack. If not, the attack is blocked. This prevents spam and makes combat feel fair.