roblox clocks and timers

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

roblox countdown node structure

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

  1. Create a Part and scale it into a wall
  2. Add a SurfaceGui as a child to your part
  3. Update the SurfaceGui size to match your Part’s size
  4. Add a TextLabel as a child to the SurfaceGui
  5. Add a server Script as a child to your TextLabel
countdown reset event

Add your BindableEvent to ServerStorage so it’s available to your other server scripts. Make sure to name the event as CountdownResetEvent.

roblox animation example of countdown

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

real time analog clock in roblox

Here’s a time-lapse of the clock running in real time.

Clock node hierarchy

clock node structure

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!

Leave a Reply