Page 1 of 1

Creating a timer [Tutorial] (FAQ)

Posted: Sat Dec 12, 2009 2:28 pm
by YaNkFaN
I've looked around for one of these and I couldn't find one so I decided to make one. This tutorial will just teach you how to make a timer in the .lua. Now why would you need a timer exactly? Well basically a timer is kind of self explanatory. It tells the game to do something once a certain amount of time elapses. Here are some basic overviews. A timer goes after this line in your .lua
the lines that should go in the .lua are bolded and the lines with -- are notes and explanations

function ScriptPostLoad()


and usually has to be more than one second in length. If you need something to happen after .5 seconds its usually best not to do a timer.

timername= CreateTimer("timername") --this creates your timer name it's easier if you leave this how it is (of course you can change it but then you have to change the name everywhere)
SetTimerValue(timername, (number)) --this sets the time the timer will run for (you need to put a number here)

StartTimer(timername)--this starts the timer

OnTimerElapse( --this sets up what happens when the timer elapses
function(timer)--this is unimportant

--here is where you put what you want to happen after your timer elapses
DestroyTimer(timer) --self explanatory
end,-- ends the timer function
timername--same as above
) --same as above
there you have it, how to create a timer. A lot of the timers in BF2 have lots of thinks linked to them. This is a simplified version. Timers IMO were the hardest to do because all the examples of stock timers were really full of lots of code lines and hope this helps people

all and all this is what the timer looks like
Hidden/Spoiler:
[code]timername= CreateTimer("timername")
SetTimerValue(timername, (number))
StartTimer(timername)
OnTimerElapse(
function(timer)

DestroyTimer(timer)
end,
timername
) [/code]

Re: Creating a timer [Tutorial] (FAQ)

Posted: Fri Aug 26, 2016 3:57 am
by SkinnyODST
How would I make a sound play with this?

Re: Creating a timer [Tutorial] (FAQ)

Posted: Fri Aug 26, 2016 1:58 pm
by Marth8880
SkinnyODST wrote:How would I make a sound play with this?
From soundhooks.txt:

Code: Select all

-------------------------------------------------------------------------------
ScriptCB_SndPlaySound(soundID);

soundID        : ID of sound properties to play

Re: Creating a timer [Tutorial] (FAQ)

Posted: Mon Jul 31, 2017 7:33 am
by GAMEDOGSHIPMENT
Don't want to bump anything here but here is a cleaner version of this script, thanks for the finding this.

Code: Select all

CreateTimer("timerhere")
SetTimerValue("timerhere", (6))	--in seconds
StartTimer("timerhere")

OnTimerElapse(
		function(timer)

		--Make something here

		DestroyTimer(timer)
                end,
              	"timerhere"
              )

Re: Creating a timer [Tutorial] (FAQ)

Posted: Thu Jan 24, 2019 11:43 pm
by Sporadia
I’m bumping this tutorial because I’ve been working with timers recently and have a bunch to add, specifically on how to work with respawn timers.

If you look in the documentation that comes with BF2_ModTools then there’s a doc called Battlefront2_scripting_system. This doc is all about lua events and it lists all the functions that you should ever need relating to a timer.

Now to add to what’s already in this tutorial:

Important point 1 (Using timers in other events):
OnTimerElapse will only be recognised if that timer’s CreateTimer has been run by the lua. So, putting CreateTimer in an event like OnCharacterDeath and then putting OnTimerElapse outside of that event does not work. The engine would read through the ScriptPostLoad before any characters die, make the OnCharacterDeath but not run it, then reach OnTimerElapse, see that the timer it refers to hasn’t been created and ignore it. Then when a character finally dies, the engine will create the timer correctly and start it correctly but you will find that the OnTimerElapse does not exist. You either need both CreateTimer and OnTimerElapse to be outside of the event, or you need them both to be inside, (or you could even have CreateTimer outside of the event and OnTimerElapse inside). But you cannot put CreateTimer inside an event and OnTimerElapse outside of the same event because the code outside events is always run first.

Edit 2025: You see this claim: "OnTimerElapse will only be recognised if that timer’s CreateTimer has been run by the lua." This was just wrong. You can declare OnTimerElapse before CreateTimer if you want. Concerningly, I don't remember why I thought you couldn't. But I've tested it now and you definitely can.

Important point 2 (Preventing a weird crash):

If you don’t destroy timers, then your map may crash when it’s run in an instant action playlist. This is something of an invisible bug because the diagnostic tools don’t let you run instant action playlists (to check for the problem) and this crash never effects the first map in a playlist (from what I’ve seen). So, it’s important that DestroyTimer is included in an OnTimerElapse to ensure that all timers are destroyed when they have run to 0. However, it’s also important that you never use a destroyed timer. For single use timers the solution here is easy, you put CreateTimer at the start of ScriptPostLoad and OnTimerElapse at the end of it. Provided you can guarantee that the timer will only ever elapse once from the way you’ve coded its other settings then you’ll be fine.

Multiple use timers

For multiple use timers like a respawn timer, the solution is more complicated. Since you’re destroying the timer every time it runs out, you need to ensure that you create the timer again every time you need it. This is where you would want to put both CreateTimer and OnTimerElapse inside of OnCharacterDeath. Putting OnTimerElapse inside OnCharacterDeath does not create an event inside an event like you might expect to happen. This will not create a situation where a character has to die at the same time the timer runs out for OnTimerElapse to run. What actually happens is the game will need OnCharacterDeath to run in order for it to see that OnTimerElapse code is there, then when it does it will treat OnTimerElapse as its own event, the same as if the code for it was outside of OnCharacterDeath.

So, putting CreateTimer and OnTimerElapse together inside an event will ensure that a new timer is made every time the event happens, then that new timer is destroyed every time it runs out. To stop an event making multiple OnTimerElapse sections for one timer, you need some code to ensure that the OnTimerElapse is only seen the first time the event is called. Define a local variable such as k or something before the event. Set k = 0 before the event. Then put the OnTimerElapse inside of an ‘if k == 0 then’ condition and set k = 1.

Multiple use hero respawn timer example:

Code: Select all


local k = 0 -- need this for OnTimerElapse to only be created once

-- for this example, we’re respawning the team 1 hero
OnCharacterDeath(
    function(player, killer)
        -- code to check if the team 1 hero died
        local team = GetCharacterTeam(player)
        if team == 1 then
            -- assume the mission has 6 units and a hero on team 1
            -- GetCharacterClass of 0 – 5 is the 6 units
            -- GetCharacterClass of 6 is the hero
            if GetCharacterClass(player) == 6 then
                -- now it’s been determined that the team 1 hero died
                CreateTimer(“HeroRespawnTimer”)
                -- print(“HeroRespawnTimer Created”) -- for debugging
                SetTimerValue(“HeroRespawnTimer”, 4)
                StartTimer(“HeroRespawnTimer”)
                -- print(“HeroRespawnTimer Started”) -- for debugging
                -- k = 0 would set k to 0
                -- k == 0 checks if k is currently equal to 0 (and returns 1 if it is)
                if k == 0 then
                    OnTimerElapse(
                        function(timer)
                            -- print(“HeroRespawnTimer elapsed”) -- for debugging
                            -- assume SpawnHero is a function I’ve already made for this example
                            -- which spawns a hero for the team you tell it to
                            SpawnHero(1)
                            DestroyTimer(timer)
                        end,
                        “HeroRespawnTimer”
                    )
                    k = 1
                end -- end of ‘if k == 0 then’
            end
        end
    end -- end of function(player, killer)
)
-- OnCharacterDeathTeam and OnCharacterDeathClass have not been used
-- because I don’t think those filters apply to the player
-- and can never get them to work with OnCharacterDeath

It’s a good idea to just put OnTimerElapse immediately after its CreateTimer and a good idea to have a different name for every timer.

If you forget to put DestroyTimer in an OnTimerElapse, you might see a new timer being made every time CreateTimer is called, all of which have the same name and are effected by the same SetTimerValue, StartTimer and StopTimer commands etc, and all of which hit 0 at the same time, calling OnTimerElapse multiple times when you’d expect it to only run once. Also, if you end up with code that’s creating timers faster than it’s destroying them, you might see similar weird behaviour. Respawn timers are easiest for classes that there is only one of on the battlefield at any given time.

Re: Creating a timer [Tutorial] (FAQ)

Posted: Sat Jan 26, 2019 2:36 pm
by Marth8880
Sporadia wrote:If you don’t destroy timers, then your map may crash when it’s run in an instant action playlist. This is something of an invisible bug because the diagnostic tools don’t let you run instant action playlists (to check for the problem) and this crash never effects the first map in a playlist (from what I’ve seen). So, it’s important that DestroyTimer is included in an OnTimerElapse to ensure that all timers are destroyed when they have run to 0. However, it’s also important that you never use a destroyed timer. For single use timers the solution here is easy, you put CreateTimer at the start of ScriptPostLoad and OnTimerElapse at the end of it. Provided you can guarantee that the timer will only ever elapse once from the way you’ve coded its other settings then you’ll be fine.
Very interesting and good to know. That would honestly explain quite a lot. Engine must not do any proper end-of-mission garbage collection for timers, so this makes a lot of sense.

Re: Creating a timer [Tutorial] (FAQ)

Posted: Mon May 12, 2025 5:47 pm
by Sporadia
Sporadia wrote:
Thu Jan 24, 2019 11:43 pm
If you don’t destroy timers, then your map may crash when it’s run in an instant action playlist. This is something of an invisible bug because the diagnostic tools don’t let you run instant action playlists (to check for the problem) and this crash never effects the first map in a playlist (from what I’ve seen). So, it’s important that DestroyTimer is included in an OnTimerElapse to ensure that all timers are destroyed when they have run to 0. However, it’s also important that you never use a destroyed timer. For single use timers the solution here is easy, you put CreateTimer at the start of ScriptPostLoad and OnTimerElapse at the end of it. Provided you can guarantee that the timer will only ever elapse once from the way you’ve coded its other settings then you’ll be fine.
I'm bumping this because I've never been able to replicate this bug. I vaguely remember that when I wrote this, I had a fairly complicated user script running which was doing these phantom crashes about 3 missions in. And I remember that it stopped when I made the decision to start destroying timers. But in the process of that I could have very easily changed something else. (It wasn't very well tested). Either way, today I've been running code to try to make sure that destroying timers is a necessity:

Code: Select all

local base = "testTimer"
local i = math.random(10000)
for j = i, i + 50 do  -- create 50 timers with hopefully different names to the last round
	local name = base.."-"..i
	
	local loopEvent = OnTimerElapse(
		function(timer)
			SetTimerValue(name, 30)
			StartTimer(name)
		end,
		name
	)
	
	CreateTimer(name)
	SetTimerValue(name, 30)
	StartTimer(name)
end
I've tried putting this directly in the mission lua, I've tried loading it via a ScriptCB_DoFile and I've tried injecting it into the missions with a user script. None of them crashed the playlists today so I really have no idea if timer cleanup is a real problem. (It does strike me as odd though that the DestroyTimer function exists).

Edit: I'll need to redo these tests. There is a subtle flaw in this code. My fault for rushing into the main game without checking it works with the debug log first.

Edit2: Ignore last edit, I don't need to retest this. But remember how I said a few years ago that OnTimerElapse needs to be declared after CreateTimer? It turns out that was completely wrong. You can declare them in the opposite order and the TimerElapse event will still exist. (There must be a reason why I thought that didn't work, but I don't remember. It definitely does work though. The test I'm doing now is a lot simpler than the stuff I was doing back then.)


Update to timer tutorial


I'm also bumping this tutorial, because I have some tips which make timers easier that I just didn't know before.



ReleaseTimerElapse

Straight forward, but I didn't know about this when I wrote in this tutorial years ago. If you capture an event like so:

Code: Select all

local event = OnTimerElapse(
	function(timer)
		-- do something
	end,
	"Timer1"
)
...then you can delete the OnTimerElapse like so:

Code: Select all

ReleaseTimerElapse(event)
It makes timers a bit more flexible. An extra thing to note is that passing local variables into events can be tricky. Global variables are fine, but if I were to try this:

Code: Select all

local event = OnTimerElapse(
	function(timer)
		ReleaseTimerElapse(event)  -- event is nil here
		-- do something once
	end,
	"Timer1"
)
ReleaseTimerElapse is actually acting on nil here, so it doesn't work. One solution is to get rid of local and just pass in globals. But what I've actually started doing is passing variables in via tables. For some reason, events can still access the tables that were made outside of them, just not variables. See here:

Code: Select all

local eventStore = {}  -- a hack to pass the event in to TimerElapse via this table
eventStore[1] = OnTimerElapse(
	function(timer)
		ReleaseTimerElapse(eventStore[1])
		-- do something once
	end,
	"Timer1"
)


You can have multiple TimerElapse events for the same timer

I had kind of been working around this for a few years without really understanding it. Every declaration of OnTimerElapse creates a separate event, which triggers when the timer ends. First of all, that means you don't want to be performing OnTimerElapse multiple times (ie putting OnTimerElapse inside an OnCharacterDeath). Because you'll get duplicate TimerElapse events if you do that. (It turns out that's not as big of a deal once you know how to release events).

But on the plus side, this is really handy for making timers loop. You can have one OnTimerElapse which contains the code you're really interested in (but doesn't reset the timer), and then declare a second OnTimerElapse which only contains code to reset the timer and start it again. And then you can quite easily stop the timer looping by using ReleaseTimerElapse just on the event which was looping it. You can turn a timer into a loop timer whenever you want by declaring a new OnTimerElapse which resets it. (Something to note is the order of events here. When the timer runs out, it will first trigger every TimerElapse event related to it. And then code in those events will start to run / interrupt each other in a random order. So every TimerElapse is guaranteed to trigger, even if one of them contains code to restart the timer.)

Here is an example:

Code: Select all

local timerName = "Timer1"
CreateTimer(timerName)

local i = {1}
local loopStore = {}

OnTimerElapse(
	function(timer)
		print("Elapsed!")  -- debug proof that it's working
		
		-- do some really complicated thing
		
		-- this is just something extra I'm adding, to make this timer stoop looping after 5 elapses
		local iteration = i[1]
		i[1] = iteration + 1
		if iteration == 4 then
			-- release the looping event on iteration 4, which is 1 lower than you might expect
			-- the looping event that creates iteration 5 has already been triggered here, so there will be an iteration 5
			-- releasing events will stop them triggering again, but it won't stop them running if they're already triggered
			ReleaseTimerElapse(loopStore[1])
		end
	end,
	timerName
)

loopStore[1] = OnTimerElapse(
	function(timer)
		SetTimerValue(timer, 2)
		StartTimer(timer)
	end,
	timerName
)

SetTimerValue(timerName, 5)
StartTimer(timerName)