Introduction
When making a game, most devs don’t care about speedrunners, which is kinda annoying sometimes.
If your game is a platformer, an obby or just a story game, having a timer for speedruns is really good since it allows for way more accurate timing.
An example of good usage of in-game speedrun timer is in Robot 64 and Try To Die, making it convenient for speedrunners to run those games.
So, today i am going to teach how to make an accurate speedrun timer.
Tutorial
To start off we are going to make a module and place it in ReplicatedStorage.
Now we can start writing the script.
The first step is making some variables that will make the timer work:
Variable explanations:
- RunService: Uh, it’s roblox’s run service, what did you expect?
- StartTime: When the timer was started
- PausedTime: When the timer was last paused
- Accumulated: The time accumulated from all pauses
- Running: Is the timer running?
- Paused: Is the timer paused?
- Signal: The preferred signal used to update the timer label
- Connection: The connection made for updating the timer
- Label: The label used to show the timer
Now that we have our variables, we can make the formatting function.
We are going to start by calculating the minutes, seconds and milliseconds:
And then, we can put them together in a string:
How string.format works here
Basically, it will replace the patterns with the numbers passed
The first %02i will be replaced by the minutes with 2 or more digits. Examples: 01, 04, 20, 76, 127.
The second %02i will be replaced by the seconds with 2 or more digits. Examples: 05, 09, 60.
The %03i will be replaced by the milliseconds with 3 or more digits. Examples: 001, 052, 934.
Now we can make the updating function.
First we check if the timer is running and if there is a label to update, if not then the function is not executed.
(I didn’t add a warn saying “Timer not running.” because this function will be executed a lot)
Now we will get the time passed since the timer started and set the label’s text to the formatted version of it.
Everything we need are in the variables, so we what we do is:
Now that all the local functions are done, we can start making the module functions.
The first module function we are going to make is the SetLabel function which allows to change the label from any client script.
This function is pretty simple, all it does is this:
Now, lets make the Start timer function.
First, we need to check if the timer is already running, we only want this function to be executed if the timer is not running.
Now we are going to setup the variables
And that’s everything for the Start function, now lets make the Pause function.
This function will only run if the timer is running, so let’s write that
After that, we have check if the timer is already paused or not so we can change the variables accordingly
How the “if else” of the Pause function works
It is divided by two parts, one that is executed if the timer is paused and another that is executed if the timer is not paused.
If the timer is paused, we check if the PausedTime variable exists, if it does we can add the time passed since that variable was set to the Accumulated variable, which will be subtracted from the total time since the timer started when updating the label. Then, set the Connection variable to a new signal connection so the label updates.
If the timer is not paused, we set the PausedTime to the current os.clock() and then check if a signal connection exists, if it does then we disconnect it and set the Connection variable to nil.
Now the last thing for the Pause function to be done is toggling the Paused variable.
Our last module function is the Stop function, which will be stopping the timer.
Again, we will be check if the timer is running, we can’t stop it if it’s not running
Now we just need to set all the variables to default:
And with that, the module is done!
Results
Full module code
--!nocheck
local RunService = game:GetService("RunService")
local StartTime: number?
local PausedTime: number?
local Accumulated: number = 0
local Running: boolean = false
local Paused: boolean = false
local Signal: RBXScriptSignal = RunService.Heartbeat
local Connection: RBXScriptConnection?
local Label: TextLabel?
local Timer = {}
local function Format(Time: number): string
local Minutes = Time / 60 % 60
local Seconds = Time % 60
local Milliseconds = Seconds * 1000 % 1000
return string.format("%02i:%02i.%03i", Minutes, Seconds, Milliseconds)
end
local function Update()
if not Running or not Label then
return
end
Label.Text = Format(os.clock() - StartTime - Accumulated)
end
function Timer:SetLabel(NewLabel: TextLabel)
Label = NewLabel
end
function Timer:Start()
if Running then
warn("Timer is already running.")
return
end
StartTime = os.clock()
PausedTime = nil
Accumulated = 0
Connection = Signal:Connect(Update)
Running = true
Paused = false
end
function Timer:Pause()
if not Running then
warn("Timer is not running.")
return
end
if Paused then
if PausedTime then
Accumulated += os.clock() - PausedTime
PausedTime = nil
end
Connection = Signal:Connect(Update)
else
PausedTime = os.clock()
if Connection then
Connection:Disconnect()
Connection = nil
end
end
Paused = not Paused
end
function Timer:Stop()
if not Running then
warn("Timer not running.")
return
end
Running = false
Paused = false
StartTime = nil
PausedTime = nil
Accumulated = 0
if Connection then
Connection:Disconnect()
Connection = nil
end
end
return Timer