Jump to content

Lua Timers: Adding repeatable timers with less then 10 minute intervals


Fenris_Wolf

Recommended Posts

As you (probably) already know, PZ allows us to define timed events using OnEveryTenMinutes, EveryHours, and EveryDays.  As handy as they are, sometimes we want a more refined system. Maybe we want a event to trigger in 15 minutes, and possibly repeat itself a few times then stop.

Maybe we want something to happen every 60 seconds in game.

Well I got bored this morning and cooked up such a system... (this would go in the lua/shared folder)

Spoiler

Timers = {
    ActiveTimers = {},
}

Timers.add = function(delay, repeats, callback, args)
    local hours = getGameTime():getWorldAgeHours()
    local t = {
        delay = delay,
        callback = callback,
        args = args,
        repeats = repeats,
        trigger = hours + (delay/60),
    }
    t.id = ZombRand(100000) .."-".. t.trigger
    Timers.ActiveTimers[t.id] = t
    return t
end

Timers.remove = function(timer)
    if type(timer) == "table" then
        -- not a proper timer table
        if not timer.id then return end
        timer = timer.id
    end
    Timers.ActiveTimers[timer] = nil
end

Timers.check = function()
    local current = getGameTime():getWorldAgeHours()
    for id, timer in pairs(Timers.ActiveTimers) do
        if timer.trigger <= current then
            if not timer.repeats or timer.repeats == 0 then
                Timers.ActiveTimers[id] = nil
            elseif type(timer.repeats) == "number" then
                timer.repeats = timer.repeats - 1
            end
            timer.trigger = current + (timer.delay/60)
            timer.callback(timer, timer.args)
        end
    end
end

 

 

First we create our Timers table:

Timers = {
    ActiveTimers = {},
}

This will hold our functions, and the sub-table ActiveTimers will hold our actual timer queue.

 

Now we need a handy function to add timers to our table:

Timers.add = function(delay, repeats, callback, args)
    local hours = getGameTime():getWorldAgeHours()
    local t = {
        delay = delay,
        callback = callback,
        args = args,
        repeats = repeats,
        trigger = hours + (delay/60),
    }
    t.id = ZombRand(100000) .."-".. t.trigger
    Timers.ActiveTimers[t.id] = t
    return t
end

This function takes 4 arguments:

delay = the number of minutes to wait before triggering our timer. This should be a integer value greater then 0.

repeats = the number of times to repeat the timer, after the first triggering. If repeats is 0, nil or false, it will only fire once. If true then it will endlessly fire. A value of 2 will trigger a total of 3 times (the initial trigger, then 2 repeats)

callback = the function to call when this timer is triggered. It should accept 2 arguments: the timer itself, and whatever value is in the 'args' variable

args = passed to your callback function. This can be anything, although you should be careful not to pass variables that may have expired (ie: a IsoPlayer object on a server, in case that player disconnects before the timer is triggered)

 

Its going to be up to you to make sure your callback function properly verifies anything you pass to it as args.

Notice we use getGameTime():getWorldAgeHours() to help define when this timer should trigger (see the t.trigger variable in the above function). This will return a float value such as 125.25, which would mean 125 hours and 15 minutes into the game (125.50 is 125h and 30min). Since our 'minutes' are a percent value of 1 (15 minutes = .25)  we use delay/60

 

Now we need a function to remove our timers just in case we need to cancel early:

Timers.remove = function(timer)
    if type(timer) == "table" then
        -- not a proper timer table
        if not timer.id then return end
        timer = timer.id
    end
    Timers.ActiveTimers[timer] = nil
end

The timer argument can be one of 2 things: the actual timer table returned by Timers.add(), or the timer id.

 

Now those 2 important functions are out of the way, time for our function that actually checks if a timer should be triggered:

Timers.check = function()
    local current = getGameTime():getWorldAgeHours()
    for id, timer in pairs(Timers.ActiveTimers) do
        if timer.trigger <= current then
            if not timer.repeats or timer.repeats == 0 then
                Timers.ActiveTimers[id] = nil
            elseif type(timer.repeats) == "number" then
                timer.repeats = timer.repeats - 1
            end
            timer.trigger = current + (timer.delay/60)
            timer.callback(timer, timer.args)
        end
    end
end

basically we loop through our Timers.ActiveTimers table, and if the timer.trigger is less then the current world age hours, its time to call the callback function! Notice if the timer.repeats is a number (and not 0) it lowers the repeat by 1.  It also sets up the next timer.trigger value.

Pretty easy stuff.

 

Now we need to be able to call the Timers.check() function on a interval, using PZ's events system. There's 2 events we can use, each with benefits and drawbacks:

Events.OnTick.Add(Timers.check)

This method using OnTick will give you the most accurate timers and allows you to setup timers with a delay of 1 minute. Its drawback is that its called ALOT (multiple times per 1 second of game time), so if your planning on having alot of active timers at once, performance may suffer and you should probably use this instead:

Events.EveryTenMinutes.Add(Timers.check)

The drawback here is timers are effectively only triggered every ten minutes.

 

 

So now for a example of a timer added client side (they can be added server side as well)

local callback = function(timer, args)
    if not timer.repeats or timer.repeats == 0 then
        args:Say("Last Call!")
    else
        args:Say("Timer Triggered!")
    end
end

Events.OnGameStart.Add(function()
    Timers.add(15, 3, callback, getSpecificPlayer(0))
end)

On logging into the game, this will make our player say "Timer Triggered!" every 15 minutes, 3 times, then 15 minutes later say "Last Call!"

 

Please note these timers are NOT saved on exiting (or server shutdown), I'll leave that part to you creative people to figure out a method of doing so.

This is after all, a tutorial on timers, not saving data ;)

Link to comment
Share on other sites

17 hours ago, Fenris_Wolf said:

Now we need to be able to call the Timers.check() function on a interval, using PZ's events system. There's 2 events we can use, each with benefits and drawbacks

This bugged me, why can't we have the best of both worlds? A timer system that is accurate and wont degrade performance if a lot of timers are active? Well....

 

local lastMinute = nil
Events.OnTick.Add(function()
    local minute = getGameTime():getMinutes()
    if minute ~= lastMinute then
        Timers.check()
        lastMinute = minute
    end
end)

Now we can use the OnTick event, and only call the Timers.check() function if the minute has actually changed, instead of multiple times per game second.

 

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...