Scripted timer

From Mod Wiki

Revision as of 03:57, 30 September 2008 by Don Reba (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

There are situations when you would like to perform an action, when certain amount of time expires. 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

Team:Dez0wave

Personal tools