This is a tutorial on event counters or event IDs. This concept is related to debounce or cooldowns. In this post, I’m going to give some examples of event counting and break the idea down.
Event counting is keeping track of how many times an event fires.
It’s often useful whenever there’s an intentional delay before some action triggered by an event.
Visualization [link to working demo]
Motivation
A great example is an automatically-closing door.
However, in this example, we still want players to be able to open and close the door rapidly.
Wait, can't I just...?
Wrong method: It may seem easy enough to write something like the following (incomplete) code.
local function onMouseClick()
-- open/shut door
if closed then
openDoor()
else
shutDoor()
end
closed = not closed
-- wait some time and close the door
task.wait(4)
if not closed then
shutDoor()
end
end
detector.MouseClick:Connect(onMouseClick)
But this wouldn’t work! When players click too quickly, it displays unstable behavior. In the GIF below, the last three clicks should only open the door, but the door opens and closes rapidly.
Example solution [link]
I give my full solution to the example problem below. There’s also a working model here.
-- Services
local TweenService = game:GetService("TweenService")
-- Objects
local doorModel: Model = workspace.DoorModel
local sounds: Folder = doorModel.Sounds
local door: BasePart = doorModel.Door
local hinge: BasePart = doorModel.Hinge
local detector: ClickDetector = door.ClickDetector
-- Constants
local hingeClosedRotation = hinge.CFrame
local hingeOpenRotation = hinge.CFrame * CFrame.Angles(0, math.rad(90), 0)
local infoIn = TweenInfo.new(sounds.Open.TimeLength, Enum.EasingStyle.Cubic, Enum.EasingDirection.In)
local infoOut = TweenInfo.new(sounds.Close.TimeLength, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local tweens = {
Open = TweenService:Create(hinge, infoIn, { CFrame = hingeOpenRotation }),
Close = TweenService:Create(hinge, infoOut, { CFrame = hingeClosedRotation }),
}
local DEBOUNCE_DELAY = 0.2
local DOOR_CLOSE_DELAY = 4
-- Variables
local doorAction = "Open"
local currentEventId = 0 -- event counter
local debounce = false
-- Functions
local function toggleDoor()
tweens[doorAction]:Play()
sounds[doorAction]:Play()
doorAction = (doorAction == "Open") and "Close" or "Open"
end
local function autoShutDoor(eventId: number)
if eventId == currentEventId and doorAction == "Close" then
toggleDoor()
end
end
local function onMouseClick()
if debounce then return end
debounce = true
-- open/shut door
toggleDoor()
-- stamp the event with an id
currentEventId += 1
-- start shut door timer
task.delay(DOOR_CLOSE_DELAY, autoShutDoor, currentEventId)
task.wait(DEBOUNCE_DELAY)
debounce = false
end
-- Events
detector.MouseClick:Connect(onMouseClick)
Why it works
Before getting into event IDs, it’s helpful to first understand debounce. Debounce essentially means the same thing as cooldown. There are a lot of great resources out there on that topic already, but I’ll give a quick refresher here.
What is debounce?
Debounce
To “debounce” something is to make sure it doesn’t happen “too much.” This is common when an event may fire many times though we only needed one, or in general when an event is faster than wanted.
Example
Let’s say the goal is to make a part get a little bit more invisible every time it’s touched.
Try putting this code in a Script inside a Part:
script.Parent.Touched:Connect(function()
script.Parent.Transparency += 0.1
end)
But when the Halloween Cat touches the brick, it disappears a bit too quickly.
The solution is to add a debounce!
local debounce = false -- create a debounce variable
script.Parent.Touched:Connect(function()
if debounce then return end -- if debounced, then don't continue!
debounce = true -- otherwise, start debouncing
script.Parent.Transparency += 0.1
task.wait(0.2) -- wait 0.2 seconds
debounce = false -- stop debouncing
end)
Well, it’s better anyway.
Debounce in terms of state machines
Note that debounce involves both a single global “state” and the events that request that state. Though an event may trigger many times, there’s just one state. The machine uses its state to figure out what needs to happen with the events.
In debounce, the behavior of the machine is like this:
State: The state reads either on or off .
Procedure when an event triggers:
- If the state is on , do nothing.
- If the state is off , turn the state on and do the action!
After event action:
- Wait some time.
- Turn the state back off .
You could interchange the “on” and “off” states and it would make no difference.
Event counting
In event counting, the behavior of the machine is a bit different. There’s typically something that happens after a delay.
The key idea is that each thread gets its own “ID” and only the current thread ID allows the final action to occur.
Here’s an outline:
State: The state can take on an unlimited number of values (0, 1, 2, 3, …)!
Procedure when an event triggers:
- Go to the next state (so 1 goes to 2 and 2 goes to 3, etc.).
- Remember the new state by storing it in a local variable
eventState
.- Do main action.
After event action:
- Wait some time.
- Check if the current state equals
eventState
.- If they are equal , do another action!
- If they are not equal , do nothing.
So rather than being a machine with two configurations like debounce, event counters have potentially infinitely many configurations.
Step-by-step
Here’s how to count your events properly, step by step.
- Connect to any event.
For example, create a Part. Then insert a ClickDetector into it, and also put this code in a Script inside the Part.
local part = script.Parent
local detector = part.ClickDetector
detector.MouseClick:Connect(function()
print("Click detected!")
task.wait(4)
print("It's been four seconds since I detected some click.")
end)
- Create a state variable outside of the event function.
local state = 0
- Rewrite the event with the following additions:
Add one to the state and store the result in a variable.
Then after waiting, check if the state is equal to the variable.
(Everything else can stay the same.)
Here’s the end result:
local part = script.Parent
local detector = part.ClickDetector
local state = 0
detector.MouseClick:Connect(function()
state += 1 -- update state
local eventState = state -- store state
print("Click detected!")
print("Number of clicks: " .. state)
task.wait(4)
-- check if states are equal before continuing
if state ~= eventState then return end
-- states were equal, so we can do an action:
print("It's been four seconds since the *very last* click.")
end)
Voila! That’s an event counter.
I hope this helps! Feel free to ask for clarification on anything.