How to make timers and clocks in Roblox scripts
How to make timers and clocks in Roblox scripts has many uses. You can use timers to limit how long a game round lasts, a delay for an explosion, or control how long a player has to complete a task.
We can also add a clock or countdown GUI either in game or within your UI.
What you’ll learn
- How to use BindableEvent to trigger code into other parts
- Rotate a part or model on a pivot
- Transform a real world concept, such as a clock, into code
How to make a countdown timer
Countdown node hierarchy
Our countdown consists of a Part, SurfaceGui, TextLabel, and its server script. We’ll also demonstrate how to reset the timer using a separate part.
Steps
- Create a Part and scale it into a wall
- Add a SurfaceGui as a child to your part
- Update the SurfaceGui size to match your Part’s size
- Add a TextLabel as a child to the SurfaceGui
- Add a server Script as a child to your TextLabel
Add your BindableEvent to ServerStorage so it’s available to your other server scripts. Make sure to name the event as CountdownResetEvent.
Here’s an example of what your countdown can look like in game. Notice how it blinks red and black once it’s done.
With the CountdownResetEvent BindableEvent we can add a reset button to restart the countdown. Of course you can use different triggers to reset.
Code example
local label = script.Parent
label.Text = "Countdown"
local countdown = 10
local blink = false
local loopInterval = 1
function calculateRemainingMinutes(seconds)
return math.floor(seconds / 60)
end
function calculateRemainingSeconds(seconds)
local minutesElapsed = calculateRemainingMinutes(seconds)
local remaining = seconds - minutesElapsed * 60
return remaining
end
-- format time so it’s always two digits
function formatTime(time)
if time == 0 then
return "00"
elseif time < 10 then
local formatted = "0" .. time
return formatted
else
return time
end
end
local serverStorage = game:GetService("ServerStorage")
local resetCountdownEvent = serverStorage:WaitForChild("CountdownResetEvent")
function resetCountdown()
blink = false
loopInterval = 1
countdown = 10
label.TextColor3 = Color3.new(0, 0, 0)
end
function onResetCountdown()
if countdown <= 0 then
print("resetting countdown")
resetCountdown()
end
end
resetCountdownEvent.Event:Connect(onResetCountdown)
while true do
wait(loopInterval)
if countdown >= 0 then
local minutes = calculateRemainingMinutes(countdown)
local seconds = calculateRemainingSeconds(countdown)
minutes = formatTime(minutes)
seconds = formatTime(seconds)
-- join the formatted minutes and seconds with a colon
local countdownText = minutes .. ":" .. seconds
label.Text = countdownText
countdown = countdown - 1
else
-- blink
loopInterval = 0.25
if blink then
label.TextColor3 = Color3.new(1, 0, 0)
blink = false
else
label.TextColor3 = Color3.new(0, 0, 0)
blink = true
end
end
end
Effects when countdown finishes
As you can see in the code example, the countdown will start blinking 4 times a second. It flashes from black to red text every 250ms (quarter of a second). Once the countdown finishes by reaching 0 seconds, the script changes the wait interval so it can update the TextLabel faster.
We could update this to do anything we imagine such as having the TextLabel scale up for 250ms and scale back down so it looks like it’s zooming in and out.
Add a reset button
local button = script.Parent
-- BindableEvent for when button is pressed
local serverStorage = game:GetService("ServerStorage")
local resetCountdownEvent = serverStorage:WaitForChild("CountdownResetEvent")
local pressed = false
function onButtonTouched()
if not pressed then
pressed = true
print('touched button:', resetCountdownEvent)
resetCountdownEvent:Fire()
wait(1)
pressed = false
end
end
button.Touched:Connect(onButtonTouched)
This button uses debouncing and fires off the CountdownResetEvent so the TextLabel can react when triggered. With debouncing we prevent the countdown from resetting every time it’s touched.
How to make a clock
Here’s a time-lapse of the clock running in real time.
Clock node hierarchy
With this Part structure, the hour and minute hands follow the main clock part. Our script can then reference the clock hand parts from its parent.
Rotating from a pivot point
When using CFrame, a model rotates from its center by default. For the clock hands to rotate from a point we’ll need to understand a bit of Linear Algebra and how Matrix mathematics work.
There’s a lot more to learn about CFrame that I’ll go into in another post. A Roblox CFrame is basically a data structure that consists of these matrices. Each matrix represents a model’s position and orientation in the world.
For example a part’s position is represented by its x, y, and z coordinate. So the position vector or matrix consists of 3 numbers in a 3×1 matrix.
Think of CFrame as a coordinate system that let’s us manipulate models in a 3D world.
The important thing to remember with Matrix mathematics is that order matters. If we have a matrix A and matrix B then A * B is different from B * A.
As you’ll see in the script below, we create a pivot offset for each hand by dropping it down half its length on the y axis.
To rotate a model from a given point, we modify the model’s current CFrame using this formula.
pivotOffsetCFrame * rotationCFrame * pivotOffsetCFrameReset
This formula first moves the model with the offset, applies the rotation CFrame, and then moves the rotated model back to its original position.
Since our clock rotates on the Z axis and points vertically on the Y axis our offset follows this formula:
local pivotOffset = {0, -length /2, 0}
The rotation CFrame then converts the degree angles to radians. Degree angles go from 0 to 360 while radians go from 0 to 2*Pi.
local rotation = {0, 0, math.rad(angleInDegrees)}
We then reset the offset by taking the negative of it.
local pivotOffsetReset = {0, length /2, 0}
Note that to rotate counter-clockwise we simply reverse the signs of these offsets.
local pivotOffset = {0, length /2, 0}
local pivotOffsetReset = {0, -length /2, 0}
Clock script
local hourHandPart = script.Parent.hourhand
local minuteHandPart = script.Parent.minutehand
local secondsElapsed = 0
local hourPivotOffset = CFrame.new(0, -hourHandPart.Size.Y/2, 0)
local hourPivotOffsetReset = CFrame.new(0, hourHandPart.Size.Y/2, 0)
local minutePivotOffset = CFrame.new(0, -minuteHandPart.Size.Y/2, 0)
local minutePivotOffsetReset = CFrame.new(0, minuteHandPart.Size.Y/2, 0)
while true do
wait(1)
local components = os.date("*t")
--components.hour = 3
--components.min = 15
--components.sec = 0
print(components.hour, components.min, components.sec)
local hourAngle = ((components.hour + components.min / 60) / 12) * 360
local currentHourAngle = hourHandPart.Rotation.Z
local hourRotation = hourPivotOffset * CFrame.Angles(0, 0, math.rad(hourAngle - currentHourAngle)) * hourPivotOffsetReset
hourHandPart.CFrame = hourHandPart.CFrame:ToWorldSpace(hourRotation)
local minuteAngle = ((components.min + components.sec / 60) / 60) * 360
local currentMinuteAngle = minuteHandPart.Rotation.Z
local minuteRotation = minutePivotOffset * CFrame.Angles(0, 0, math.rad(minuteAngle - currentMinuteAngle)) * minutePivotOffsetReset
minuteHandPart.CFrame = minuteHandPart.CFrame:ToWorldSpace(minuteRotation)
end
This script uses the os Lua library to get the current date and time from the server. It uses the time cycle for each second, minute, and hour as it elapses. We calculate the percentage of time passed and then divide that for each cycle.
For example, 6:15 PM becomes 0.25 of a circle for the minute (90 degrees) and 0.5 of a circle for the hour (180 degrees).
An hour has a cycle of 12 and a minute has a cycle of 60 on an analog clock. We take this percentage and convert it to degrees in a circle. So each hour is separated by 360 / 12 or 30 degrees and a minute is separated by 360 / 60 or 6 degrees.
What’s Next
You now understand how to use BindableEvent to trigger code into other parts and how to use CFrames to rotate a part or model on a pivot. Using these concepts we’ve transformed a real world concept, such as a clock, into code.
Next up we’ll learn how to make and use Game passes in our Roblox scripts.
Continue your journey by reviewing how Roblox games work.
Thank you for reading and stay curious!