A dt based timer for callbacks

britzl’s timer library is great for a os time based timer.

Inspired by it, and needing a similar solution for handling events in my game but based on passed in game time rather than os time, I wrote a simple Lua module for it.

timer.lua:

local M = {}
local callbacks = {}
local currentId = 0

local function buildCallback(func, seconds, ...)
	currentId = currentId + 1
	return {
		instance = __dm_script_instance__,
		id = currentId,
		callback = func,
		time = seconds,
		timeLeft = seconds,
		repeating = false,
		paused = false,
		args = { n = select("#", ...), ... }
	}
end

function M.once(func, seconds, ...)
	local callback = buildCallback(func, seconds, ...)
	table.insert(callbacks, callback)
	return currentId
end

function M.repeating(func, seconds, ...)
	local callback = buildCallback(func, seconds, ...)
	callback.repeating = true
	table.insert(callbacks, callback)
	return currentId
end

function M.cancel(id)
	for i = 1, #callbacks do
		if callbacks[i].id == id then
			table.remove(callbacks, i)
			return true
		end
	end
	return false
end

function M.pause(id)
	for i = 1, #callbacks do
		if callbacks[i].id == id then
			callbacks[i].paused = true
			return true
		end
	end
	return false
end

function M.unpause(id)
	for i = 1, #callbacks do
		if callbacks[i].id == id then
			callbacks[i].paused = false
			return true
		end
	end
	return false
end

function M.cancel_all()
	callbacks = {}
end

function M.update(dt)
	for i = #callbacks, 1, -1 do
		local callback = callbacks[i]
		if callback.paused == false then
			callback.timeLeft = callback.timeLeft - dt
			if callback.timeLeft <= 0 then
				if callback.repeating == true then
					callback.timeLeft = callback.time
				else
					table.remove(callbacks, i)
				end
				if pcall( function() local test = callback.instance.property end ) then
					local currentInstance = __dm_script_instance__
					__dm_script_instance__ = callback.instance
					callback.callback(unpack(callback.args, 1, callback.args.n))
					__dm_script_instance__ = currentInstance
				end
			end
		end
	end
end

return M

How to use it? First, make sure you set up a game object dedicated to updating the timer, like so:

local timer = require "path/to/your/modules/timer"

function update(self, dt)
	timer.update(dt)
end

And finally, how to actually add your timed functions:

-- simple call, supply your callback and seconds until trigger
timer.once(function()
	print("Hello world!")
end, 0.5)

-- third/rest parameters turn into arguments for the callback
timer.once(function(name)
	print("Oh hai " .. name .. "!")
end, 0.8, "Mark")

-- example of using more than one argument for the callback
timer.once(function(who, data)
	print("It's " .. data.state .. ", I did not " .. data.action .. " " .. who)
end, 1, "her", { action = "hit", state = "not true"} )

-- and id is returned, can be used to cancel
local timerId = timer.once(function(name)
	print("This will never trigger since we cancel it")
end, 2)
timer.cancel(timerId)

-- how to have a repeating callback instead
local repeatingId = timer.repeating(function(state)
	state.count = state.count + 1
	print("Repeating, count: " .. state.count)
end, 1, {count = 0} )

-- better cancel that repeater if we don't want it to run forever!
timer.once(function(id)		
	if timer.cancel(repeatingId) then
		print("Cancelled the repeater")
	end
end, 4, repeatingId)

Result:

DEBUG:SCRIPT: Hello world!
DEBUG:SCRIPT: Oh hai Mark!
DEBUG:SCRIPT: Repeating, count: 1
DEBUG:SCRIPT: It's not true, I did not hit her
DEBUG:SCRIPT: Repeating, count: 2
DEBUG:SCRIPT: Repeating, count: 3
DEBUG:SCRIPT: Cancelled the repeater

I’m not an expert on Lua, so if anyone more experienced want to vet the code please do not hesitate to call on improvements!

I haven’t gotten it working properly with functions referring to self yet, perhaps someone can give some input on that?
For example, if I have a callback that calls go.get_id(), it returns hash: [/timer]. So it’s in the context of the object calling update.
Is there way to call a function in the context of a specific game object?

3 Likes

Nope. Not officially (unless you use native extensions). There is a hack to do it, though. There is this global variable named __dm_script_instance__ which you can save when registering the callback and then temporarily set while calling it.

See discussion here:

2 Likes

Great tip, thank you!

I’ve modified my timer.lua module to employ this method, and it works great now with my own scenarios (self-exploding missiles happening after x seconds, now behaving exactly the same on both desktop and mobile).

I was a bit worried that my timers would keep a reference to the instances and bugs would ensue.

Consider this scenario for example:

  • Missile is launched, sets a self desctruct timer to 5 seconds.
  • Collides with player after 4 seconds.
  • In the final-method I call timer.cancel to remove the self destruct timer.
  • Works fine

However, if I skip the third step (cancelling), I was expecting bad things to happen. But instead it works out great, in that my callbacks table automatically has the timer removed. I’m not sure how/why, but I’m guessing the engine looks up references somehow and deems it suitable for removal.

How can it be automatically removed from the callback table? Sounds strange to me.

1 Like

Yep you’re right. Just produced an isolated example project for this and it makes the engine crash instead. Not sure where in my game I accidentally take care of it.

So now the question is, is it possible to check on an instance if it has been killed or not. I realize there might not be support for this since it’s not supposed to be used and stored.

Such as dmScript::IsInstanceValid

It’s good practice to clean up after yourself in final() anyway, so why not do that? Invalidate timers corresponding to the current script in final.

1 Like

Yeah that’s fine I guess, just pushing the boundary to see if there’s a way to take care of those cases where you accidentally do forget :slight_smile:

I managed to solve it in a sneaky way.

On my saved instance, I can do:
pcall( function() local test = v.instance.property end )
This returns true if the instance is still valid, and false if it has been deleted. Instead of directly trying to use the saved instance which would result in a crash if it is no longer valid.

I have updated my timer.lua code above to reflect this, and this version thus supports the irresponsible developer that does not clean up any timers after themselves in the final function.

Updated the code to use regular for-loops instead of ipairs. Supposedly this is more performant, and if this is to be called on each update I might as well take that into account, should anyone use this and have hundreds of timers at the same time.