Verification: Static review complete; Studio/device tests unverifiedLast verified: 2026-10-03Prerequisites: Learn Luau through engine code

Use events without duplicate effects

Outcome and prerequisites

Build one touch-responsive pad and learn why an event can fire more often than your game action should occur. You need the data-model and Luau lessons. Use a disposable Baseplate; this experiment is not part of the obby.

Event occurrence is not game intent

A Touched event describes physical contact with another part. An avatar is made of several parts, so walking onto a pad can produce several contacts. It does not mean “the player deliberately activated the pad once.” Your rules must translate low-level events into meaningful actions.

Likewise, a Player has a longer lifetime than its Character. The same player may get a new character after dying. Holding a Humanoid reference forever is therefore fragile. Whenever a mechanic affects a character, consider what happens when that character is replaced.

Set up the pad

In edit mode, insert a Part directly under Workspace named TouchPad. Set Anchored=true, CanCollide=true, CanTouch=true, Size=(10,1,10), Position=(0,1,-12). Give it a red Color. Keep the default Baseplate and SpawnLocation for this experiment.

Insert a Script at ServerScriptService.TouchLab, replacing the default body with the complete code:

-- Place: ServerScriptService.TouchLab (Script)
local Players = game:GetService("Players")
local pad = workspace:WaitForChild("TouchPad")
assert(pad:IsA("BasePart"), "TouchPad must be a Part")
local nextAllowed: {[Player]: number} = {}

pad.Touched:Connect(function(hit)
    local character = hit:FindFirstAncestorOfClass("Model")
    if not character then return end
    local player = Players:GetPlayerFromCharacter(character)
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not player or not humanoid or humanoid.Health <= 0 then return end
    local now = os.clock()
    if now < (nextAllowed[player] or 0) then return end
    nextAllowed[player] = now + 1
    print("TouchPad", player.Name, "time", math.floor(now))
    pad.Color = Color3.fromRGB(80, 210, 150)
end)
Players.PlayerRemoving:Connect(function(player)
    nextAllowed[player] = nil
end)

Download: touch-lab.luau

The callback rejects contacts that are not a live player character. The per-player table then allows at most one message per second for that player. A single global boolean would let one player's touch suppress another player's action, which is often an accidental multiplayer bug.

This is a cooldown, not proof of trust. It reduces duplicate effects. Later mechanics also check server-owned eligibility, distance and state. A cooldown alone does not make a remote request safe.

Verify the behavior

Run Test, walk onto the pad and watch Output. The pad turns green. Repeated contacts within the one-second window are ignored. Step away, wait, and return: another message is allowed. Jump on the pad and notice that several body contacts still produce at most one accepted action in that window.

Stop and inspect the pad. The runtime color change should have reset to the edit-time red. That reset is expected. Do not “fix” it by changing the script unless you want a different runtime behavior.

For an additional check, remove the cooldown guard temporarily in your disposable file. Watch the repeated messages, then restore it. Comparing observed behavior with and without a guard is more instructive than adding a variable named debounce without understanding its scope.

Build a debugging habit

Use a short, specific log that identifies the mechanic, player and relevant state. Avoid logging on every frame. Find the first error in Output before investigating dozens of downstream symptoms. Clicking an error can take you to its script and line; confirm you are looking at the current version of that script.

A useful bug report has an expected result, an actual result and the smallest repeatable sequence that demonstrates the difference. “The pad does not work” is ambiguous. “In a two-client test, client two touches TouchPad and no server log appears; client one succeeds” gives you a testable boundary.

When connecting an event on a persistent object, consider whether you might connect it repeatedly after every respawn or round. Repeated connections lead to duplicate callbacks. Keep the connection once, or retain and disconnect it when its owner no longer needs it. This script connects each event once during startup and removes the departing player's cooldown entry.

Troubleshooting

Nothing happens: inspect CanTouch, the exact name and path, and whether the avatar physically contacts the pad. It works once for everyone together: check for a shared global cooldown instead of a table keyed by Player. It prints after Stop: you may be looking at retained Output history. A wait warning appears: TouchPad is likely misspelled or nested somewhere else.

Exercise

Change the cooldown to two seconds. Predict which of three touches at 0, 1 and 3 seconds will be accepted, then test it. Remove TouchLab and TouchPad when finished. Proceed with a clean local obby file.

Sources and navigation

Verified 2026-10-03: events, BasePart.Touched, Players, Output.

Previous: Luau · Next: Build an obby · Related: Debugging

Progress and help notes

Nothing is sent automatically. Copying happens only when you press the button.

Sources