Scripted timer
From Mod Wiki
There are situations when you would like to perform an action, when certain amount of time expire. For instance, say you are working on a task in which you have to occupy certain position for a period of 15 minutes, or you want to spawn more npcs/mutants after each 24 hours, or you want to implement a blowout script which has 3 phases - each phase remain different amount of time. To do this in a convenient way, you can implement a timer. In this article I would like to show you a simple timer class. It defines two different types of timer, both timers counts backward starting from a given value (expiration_time):
silent timer - its main purpose is to perform previously defined action (when the timer expire)
counterclockwise timer - same as silent timer, but with text presentation on hud (useful for debugging purposes)
Contents |
Useful variables
First let's define some variables with default values, i.e. time after the timer expires (def_expiration_time), type of timer (def_timer_type) and function which will be called when timer expires (def_action):
local def_expiration_time = {h = 0, m = 5, s = 0} -- default time - 5 minutes local def_timer_type = "counterclockwise" -- default type of timer local def_action = "no_action" -- default function called when timer expires
Timer class constructor
Class constructor is called when you create a new timer object. With arguments passed to constructor we can choose timer type, timer expiration time and action. Variable timer_expired indicates whether the timer expired or not. Function time_global() returns a number of seconds passed since you start the game. Function script_name() returns name of the current script (without extension), so if you named your script my_timer.script then function script_name() returns my_timer. Variable action holds name (string) of the function which will be called when the timer expired. There are two ways you can call functions in Stalker:
- using name of the script where the function you want to call is defined, like this:
my_script_name.my_function_name(arguments)
- or you can use global variable (array) _G
_G["my_script_name"]["my_function_name"](arguments)
We will define the action function in the same script as timer class, so we will use script_name() function, but of course you can use different script for action functions only. Variable timer_wnd holds a pointer to hud window where you can see the text presentation of counterclockwise timer (upper right corner). We will use the window called hud_timer_text, defined in config\ui\ui_custom_msgs.xml (you can define your own "window" there if you want, for example change the font of the timer text etc)
class "Timer" function Timer:__init(timer_type, expiration_time, action) if timer_type ~= "counterclockwise" and timer_type ~= "silent" then timer_type = def_timer_type end self.timer_type = timer_type self.expiration_time = self:normalize_expiration_time(expiration_time) self.activation_time = time_global() self.timer_expired = false self.timer_wnd = nil if not action or not _G[script_name()][action] then self.action = def_action else self.action = action end if self.timer_type ~= "silent" then get_hud():AddCustomStatic("hud_timer_text", true) self.timer_wnd = get_hud():GetCustomStatic("hud_timer_text"):wnd() end end
For variable expiration_time we will use a string data type, so we can pass the time in a convinient way. Here is the format we will use: hours:minutes:seconds, for instance:
expiration_time = "15:34:34"
Function normalize_expiration_time() parse the expiration_time we passed as a constructor argument and returns the number of seconds. We will use 1000 multiplier and ... math backgrounds ;)
- 1 minute = 60 seconds
- 1 hour = 60 minutes
- 1 hour = 60 * 60 = 3600 seconds
so to convert expiration_time to seconds (because function time_global() is using seconds) we will use this formula:
local multiplier = 1000 local hours = expiration_time_hours * 60 * 60 * multiplier local minutes = expiration_time_minutes * 60 * multiplier local seconds = expiration_time_seconds * multiplier -- for instance: local expiration_time = "10:15:02" local hours = 10 * 60 * 60 * 1000 -- 36 000 000 seconds local minutes = 15 * 60 * 1000 -- 900 000 seconds local seconds = 2 * 1000 -- 2000 seconds local total_num_of_secs = hours + minutes + seconds -- 36 902 000 seconds
function Timer:normalize_expiration_time(exp_time) if exp_time == nil then exp_time = "" end local t = {} for word in string.gmatch(exp_time, "[%d]+") do table.insert(t, word) end local hours = tonumber(t[1] or def_expiration_time.h) * 3600000 local minutes = tonumber(t[2] or def_expiration_time.m) * 60000 local seconds = tonumber(t[3] or def_expiration_time.s) * 1000 return hours + minutes + seconds end
Destructor
Destructor is used to release any resources allocated by the object. Since our timer does not consume much resources, we can use an empty destructor:
function Timer:__finalize() end
or if you want, you can help a bit lua interpreter releasing the variables on your own:
function Timer:__finalize() self.timer_type = nil self.activation_time = nil self.expiration_time = nil self.timer_wnd = nil self.action = nil self.timer_expired = nil end
Update method (the core of our timer)
Update method should be instantly called when we create our timer object. Its main purpose is to count the time and if expiration_time expire - perform an action (call a function with certain action)
function Timer:Update() if self.timer_expired then return end local new_time = self.expiration_time - (time_global() - self.activation_time) if new_time <= 0 then self.timer_expired = true new_time = 0 end -- if we are using the counterclockwise timer if self.timer_type ~= "silent" then -- convert seconds to: hours, minutes and seconds local hours = math.floor(new_time / 3600000) local minutes = math.floor(new_time / 60000 - hours * 60) local seconds = math.floor(new_time / 1000 - hours * 3600 - minutes * 60) -- update hud window (6 digit display) self.timer_wnd:SetTextST(self:TimeToString(hours) .. ":" .. self:TimeToString(minutes) .. ":" .. self:TimeToString(seconds)) end if self.timer_expired then -- timer expired: self:Expired() end end function Timer:TimeToString(t) if t >= 10 then return tostring(t) else return "0" .. tostring(t) end end
When the time expires
We perform the action using special function:
function Timer:Expired() -- if we are using counterclockwise timer then remove hud display if self.timer_type ~= "silent" then get_hud():RemoveCustomStatic("hud_timer_text") end -- perform action _G[script_name()][self.action]() end
Creating timer object and using its methods
Let's create a local variable to hold our timer object. Function get_timer() will be our "handle" to timer object.
local pTimer = nil function get_timer(timer_type, expiration_time, action) if pTimer == nil then -- create timer object pTimer = Timer(timer_type, expiration_time, action) end return pTimer end
If you want to use more then one timer at the same time - create an array of objects (timers):
local my_timers = {} local timer_type, expiration_time = nil, nil local h, m, s = nil, nil, nil -- create 10 timers for index = 1, 10 do if index % 2 == 0 then -- 5x silent timers timer_type = "silent" else -- 5x counterclockwise timers timer_type = "counterclockwise" end -- just an exaple: h = tostring(math.random(100) % 24) m = tostring(math.random(100) % 60) s = tostring(math.random(100) % 60) expiration_time = h .. ":" .. m .. ":" .. s table.insert(my_timers, Timer(timer_type, expiration_time, "no_action")) end -- and let's call an update method for each created timer for index, timer in pairs(my_timers) do timer:Update() end
Action when timer expires
As a default action we used no_action function. Now it's time to define it, we will do that in the same script where we implemented our timer class:
function no_action() -- default action end
Obviously, the empty action function is not very useful, however it is only a default function, in case we forgot to pass an action function to timer constructor. What action function should contain depends on the purpose of timer. For instance:
-- in the same script where timer class is defined, add: local spawn_vectors = { {pos = vector():set(1, 2, 3), lvid = 123, gvid = 456}, {pos = vector():set(4, 5, 6), lvid = 789, gvid = 1011}, {pos = vector():set(7, 8, 9), lvid = 1213, gvid = 1415}, {pos = vector():set(8, 7, 6), lvid = 1617, gvid = 1819}, {pos = vector():set(5, 4, 3), lvid = 2021, gvid = 2223}, } -- action for our timer function spawn_mutants() local spawn_data = nil for index = 1, 5 do spawn_data = spawn_vectors[math.random(1, #spawn_vectors)] alife():create("bloodsucker_strong", spawn_data.pos, spawn_data.lvid, spawn_data.gvid) spawn_data = spawn_vectors[math.random(1, #spawn_vectors)] alife():create("tushkano_normal", spawn_data.pos, spawn_data.lvid, spawn_data.gvid) end end -- create and update timer, for example in bind_stalker.script: local my_timer = nil function actor_binder:update(delta) ... if my_timer == nil then -- timer class is defined in timer.script my_timer = timer.Timer("silent", "0:30:0", "spawn_mutants") else -- update timer, if it expires then spawn_mutants() function -- will be called and after that, my_timer will be suspended my_timer:Update() end ... end
As you can see the timer will be suspended when the expiration_time expires and then the action function will be called. We can also add a method to resume our timer:
function Timer:isSuspended() return self.timer_expired end function Timer:Resume() if self.timer_type ~= "silent" then get_hud():AddCustomStatic("hud_timer_text", true) self.timer_wnd = get_hud():GetCustomStatic("hud_timer_text"):wnd() end self.activation_time = time_global() self.timer_expired = false end
and resume the timer when it expired:
function actor_binder:update(delta) ... if my_timer == nil then my_timer = timer.Timer("silent", "0:30:0", "spawn_mutants") elseif my_timer:isSuspended() then my_timer:Resume() else my_timer:Update() end ... end
We can also stop the timer (suspend it) if the certain conditions are meet
function Timer:Suspend() self.timer_expired = true end
for instance:
function actor_binder:update(delta) ... if my_timer == nil then my_timer = timer.Timer("silent", "0:30:0", "spawn_mutants") else if not xr_conditions.is_day() then if my_timer:isSuspended() then my_timer:Resume() end -- update only during nights my_timer:Update() elseif not my_time:isSuspended() then -- suspend if not suspended yet my_timer:Suspend() end end ... end
Author
dez0wave