Doing It Once, When the Engine Says It Forty Times
One step onto a platform can fire Touched forty times in a quarter of a second, and now forty copies of your handler are running at once. Add a single boolean and watch thirty-nine of them turn around at the door.
One step onto a platform can fire Touched forty times in a quarter of a second, and now forty copies of your handler are running at once. Add a single boolean and watch thirty-nine of them turn around at the door.
The bug is repetition, not timing
The instinct on seeing forty prints is that something is happening too fast, and that the fix is to slow it down. It is not. Every one of those forty events is real and correct — forty distinct contacts genuinely occurred.
The bug is that your handler assumed it was the only one running. Look at what happens when it is not.
local platform = script.Parent platform.Touched:Connect(function() platform.Transparency = 1 platform.CanCollide = false task.wait(3) platform.Transparency = 0 platform.CanCollide = trueend)Read it as forty simultaneous copies. Copy one hides the platform and starts waiting. Copies two through forty hide it again — harmless — and start their own waits, each three seconds from whenever they began. Copy one finishes and restores the platform. Then copy two finishes and restores it again. Then thirty-eight more.
The player, standing on the restored platform, touches it again. Forty more copies start. The platform now flickers on a schedule nobody designed, and if the player steps off during the wrong window it stays gone.
A flag that says I am already busy
A debounce is one boolean. It records that the handler is already running, so every copy that arrives afterwards checks the flag and returns immediately.
local platform = script.Parentlocal busy = false platform.Touched:Connect(function() if busy then return end busy = true platform.Transparency = 1 platform.CanCollide = false task.wait(3) platform.Transparency = 0 platform.CanCollide = true busy = falseend)Three added lines. The guard, the claim, and the release. The first copy through finds busy false, sets it true, and does the work. The other thirty-nine find it true and return on their first line.
Where the flag has to live
The local busy = false is outside the function on purpose, and moving it inside is the mistake that makes a debounce do nothing at all.
platform.Touched:Connect(function() local busy = false -- created fresh on every single event if busy then return end -- so it is always false, and never guards anything ...end)A variable declared inside the function is created anew each time the function runs. Forty events means forty separate busy variables, each false, each seeing none of the others. Declared outside, there is one variable that all forty calls share — and sharing is the entire mechanism.
One flag per part, or one per player
The version above has one flag for the whole part, which is right for a disappearing platform: the platform is one object, and it is either mid-cycle or it is not.
It is wrong for anything that should happen per player. A coin with a single shared flag means the second player to touch it during the window gets nothing, for reasons they will never work out.
One flag for the part
Right when the part itself has a state: a platform that is currently vanishing, a door that is currently opening, a trap that is currently armed.
One flag per player
Right when the effect belongs to a person: damage cooldowns, coins, checkpoints. Two players must be able to trigger it independently.
The per-player version keeps a table keyed by the player rather than a single boolean.
local Players = game:GetService("Players")local cooldown = {} script.Parent.Touched:Connect(function(otherPart) local character = otherPart.Parent local player = Players:GetPlayerFromCharacter(character) if not player then return end if cooldown[player] then return end cooldown[player] = true -- award the coin, deal the damage, tick the checkpoint task.wait(2) cooldown[player] = nilend)Setting the entry back to nil rather than false is deliberate. It removes the key entirely, so the table does not grow a permanent entry for every player who has ever visited — which on a popular experience is a leak that only shows up after hours of uptime.
A debounce is not a cooldown
The two get used interchangeably and they answer different questions. Being clear about which you want stops you writing one and expecting the other.
A debounce exists because one action produced many events, and it collapses them into one. Its duration should cover the burst — a fraction of a second — and nothing more.
A cooldown is a design decision: this may only happen once every five seconds, because that is the rule of the game. Its duration is whatever the gameplay wants.
Key takeaways
- Every one of the forty Touched events is real. The bug is a handler written as though it were the only copy running.
- A debounce is three lines: guard on the flag, set the flag, clear it when done.
- The flag must be declared outside the handler. Inside, a fresh false is created per event and guards nothing.
- Clear the flag on every path that sets it, including early returns, or the part stays stuck for the round.
- One flag per part is right when the part has a state. One flag per player is right when the effect belongs to a person.
- Key a per-player table by the Player and clear entries to nil, so the table does not grow forever on a long-running server.
- A debounce collapses one action's burst of events; a cooldown is a gameplay rule. Different durations, different purposes.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.