Using this module. Debounce, Delay and Wait are no longer needed!

Timer Module

Github | Rbxm | Documentation

Hello there, I present you a lightweight module that I called Timer, that will help you get rid of the wait function and make your game more performant and your workflow much more efficient and predictable…

This is just the start of a series of compact yet powerful modules that am going to make and share with you guys.

This module is open source, I’ll be actively taking care of your comments here and your issues, suggestions, pull requests in github!

Documentation

Primary functions:

function Delay(callback, duration, ...) --[[
@(function, number, params...)
Returns the 'id' of the newly created task
Call the function 'callback'
after the specified 'duration'
passing the extra parameters '...'
function Debounce(callback, duration, ...) --[[
@(function, number, params...)
Returns the Event Connection, 
Connect 'callback' to the 'event'
and restrict it's execution
until the 'cooldown' of the last event call
function GetStats() --[[
@(void)
Returns a table containing the stats
of the timer measured through previous calls,
Beware, this will return an empty table
if DEBUG is false and not being enabled before.
DebugStats = {
		Min = math.huge, -- Minumum elapsed time.
		Max = 0, -- Maximum elapsed time.
		Avg = nil, -- Average elapsed time.
		Rng = math.huge, -- Range of elapsed time.
}
can be used at any point of your development
journey to make sure your delays aren't lagged
behind.
function SetUpdateEvent(event) --[[
@(RBXScriptSignal)
Returns the connection made throught
the event, so you can disconnect it if
you no longer need it!
Allows you to change the default update
event of the timer module
DEFAULT: RunService.Heartbeat
function Random(min, max, precision) --[[
@(number..)
Returns a random number between 'min' and 'max'
, with the specified 'precision' AKA. decimal places,
 or less.
function Decimals(number, precision) --[[
@(number..)
Returns the 'number' with the specified
'precision' (decimal places) or less.
function Round(number, factor) --[[
@(number..)
Returns the 'number' rounded to 
the nearest multiplier of 'factor',
Returns the 'number' itself if
the 'factor' is zero.

Code

local Timer = {}
local RunService = game:GetService("RunService")

local UpdateEvent = RunService.Heartbeat
local UpdateEventConnection = nil
local Tasks = {}

local RandomGenerator = Random.new()

-- DEBUGGER
local DEBUG = true -- RunService:IsStudio()
local DebugStats = nil
local DebugPrecision = 4
local Seperator = string.rep("-", 30)

-- Returns void, Update the stats.
local function UpdateStats(elapsedTime, expectedDuration)
	local margin = elapsedTime - expectedDuration

	if margin > DebugStats.Max then
		DebugStats.Max = margin
	end

	if margin < DebugStats.Min then
		DebugStats.Min = margin
	end

	if DebugStats.Avg then
		DebugStats.Avg = (DebugStats.Avg + margin)/2
	else
		DebugStats.Avg = elapsedTime
	end

	DebugStats.Rng = DebugStats.Max - DebugStats.Min
end

-- Returns void. Update the queued tasks, calling and deleting them if they're expired.
local function UpdateTasks()
	local now = os.clock()

	for index = #Tasks, 1, -1 do
		local task = Tasks[index]
		if task.Timeout <= now then
			local elapsedTime = now - task.StartTime
			table.remove(Tasks, index).Callback(elapsedTime, table.unpack(task.Params))
			if DEBUG then
				local expectedDuration = task.Timeout - task.StartTime
				UpdateStats(elapsedTime, expectedDuration)
			end
		end
	end
end

function Timer.SetUpdateEvent(event)
	if not (typeof(event) == "RBXScriptSignal") then
		error("the returned event should be a roblox event signal.")
	end

	if UpdateEventConnection then
		UpdateEventConnection:Disconnect()
	end

	UpdateEventConnection = event:Connect(UpdateTasks)
end

-- Returns the internal tasks table.
function Timer.GetTasks()
	return Tasks
end

-- Returns a task through the specified 'id',
-- beware, it might return another task or nil if the specified task have expired.
function Timer.GetTask(id)
	return Tasks[id]
end

-- Returns the 'number' rounded to the nearest multiplier of 'factor',
-- Returns the 'number' itself if the 'factor' is zero.
function Timer.Round(number, factor)
	if factor == 0 then
		return number
	end

	return math.round(number/factor)*factor
end

-- Returns the 'number' with the specified 'precision' (decimal places) or less.
function Timer.Decimals(number, precision)
	precision = precision or DebugPrecision
	return Timer.Round(number, math.pow(10, -precision))
end

-- Returns a random number between 'min' and 'max', with the specified 'precision'.
function Timer.Random(min, max, precision)
	local factor = math.pow(10, precision)
	return RandomGenerator:NextInteger(min*factor, max*factor)/factor
end

-- Returns a table containing the stats of the timer measured through previous calls,
-- Beware, this will return an empty table if DEBUG is false and not being enabled before.
function Timer.GetStats()
	return DebugStats
end

-- Returns void, Print the stats returned by "Timer.GetStats" in a suitable format.
function Timer.PrintStats()
	local stats = Timer.GetStats()
	print(Seperator)
	print("TIMER STATS:")
	for stat, value in pairs(stats) do
		print("\t"..stat..": "..Timer.Decimals(value))
	end
	print(Seperator)
end

-- Returns void, Resets the "DebugStats" for a fresh debug start.
function Timer.ResetStats()
	DebugStats = {
		Min = math.huge, -- Minumum
		Max = 0, -- Max
		Avg = nil, -- Average
		Rng = math.huge, -- Range
	}
end

-- Returns DebugStats, Enable debugging stats and reset them.
function Timer.EnableDebug()
	if not DEBUG then
		Timer.ResetStats()
		DEBUG = true
	end
	return DebugStats
end

-- Returns DebugStats, Disable debugging stats,
-- The returned table won't change itself unless if enabled debugging again or done manually.
function Timer.DisableDebug()
	DEBUG = false
	return DebugStats
end

-- Returns Timer, Initialize the module.
local function Initialize()
	Timer.ResetStats()
	Timer.SetUpdateEvent(UpdateEvent)
	return Timer
end

-- Returns the Event Connection, Connect 'callback' to the 'event' and restrict it's execution until the 'cooldown' of the last event call,
-- The callback get called with the following parameters Callback(EventParams..., elapsedTime, DebounceParams...)
function Timer.Debounce(event, callback, cooldown, ...)
	local params = {}
	local lastTime = os.clock()

	return event:Connect(function(...)
		local now = os.clock()
		local elapsedTime = now - lastTime
		
		if elapsedTime >= cooldown then
			lastTime = now
			callback(..., elapsedTime, table.unpack(params))
		end
	end)
end

-- Returns the 'id' of the newly created task,
-- Call the function 'callback' after the specified 'duration' passing the extra parameters '...'.
function Timer.Delay(callback, duration, ...)
	local startTime = os.clock()
	duration = duration or 0
	local typeOfCallback = typeof(callback)

	if typeOfCallback ~= "function" then
		error("Callback is expected to be a function, got '"..typeOfCallback.."'")
	end

	local id = #Tasks + 1

	table.insert(Tasks, id, {
		StartTime = startTime,
		Timeout = startTime + duration,
		Callback = callback,
		Params = {...}
	})

	return id
end

return Initialize()

Examples

Basic benchmark

local Timer = require(script.Timer)

local function HelloWorld(elapsedTime, expectedDuration)
	print("Elapsed:", Timer.Decimals(elapsedTime, 3))
	print("Expected:", Timer.Decimals(expectedDuration, 3))
	print(string.rep("-", 30))
end

-- Stress test
for i = 0,1000, 1 do
	-- Create a timer delay call to the function HelloWorld with a random delay (5 -> 10) seconds
	local expectedDuration = Timer.Random(5, 10, 2)
	-- Timer.Delay(Callback, Duration, CallbackParams...)
	Timer.Delay(HelloWorld, expectedDuration, expectedDuration)
end

-- Print Stats after 11 seconds
Timer.Delay(Timer.PrintStats, 11)

Part touched debounce for 5 seconds.
The player can’t touch the part only after passing 5 seconds of the last accepeted touched event

local Part = script.Parent
local Timer = require(script.Timer)

Timer.Debounce(Part.Touched, function(part, elapsedTime)
	print("Part have been touched by", part)
	print("Touched after", elapsedTime)
end, 5)

Create a function that will run on a specified FPS,
in this the function will run 3 times per second
because the fps is set to 3.

local RunService = game:GetService("RunService")
local Timer = require(script.Timer)
local FPS = 3

Timer.Debounce(RunService.Heartbeat, function(delta, elapsedTime)
	print(1/elapsedTime, "FPS")
end, 1/FPS)
Work in progress, Please come back later :D

Video

Work in progress, Please come back later :D
12 Likes

Helpful and interesting. I will use it!

1 Like

Thank you. Please let me know if any problems or issues arise!
I’ll fix it immediately as the module is still pretty new.

I really like how you “upgraded” a few small built in functions, like math.Random() and math.Round(), very cool!

1 Like

More to come :smiley:
This is just the first version. going to add a lot of other useful functions!
Check the last example I added. it’s pretty cool!

EDIT: Oops, fixed it!

1 Like

This a simple lightweight solution for those functions. Otherwise I already added some functions that might make this module even more powerful. I just need the free time to document them and add them to this thread.

apart from that, thanks for the information!

I might try to copy and understand the script… For now it’s kinda confused for me to read the script but thanks for this… I’ve made myself a timer but I used while loop lol

1 Like