8th August 2009

Staged Timers

A great way to make a dungeon, effect or quest stand out and seem unique is to use a scripted sequence in order to create an event that wouldn't regularly be seen. Some examples of scripted sequences in Fallout 3 are the birth scene at the beginning of the game and the "critical deaths" for energy weapons. Perhaps the most convenient structure for a script that is to control a scripted sequence is what I like to call a "Staged Timer".

The basic structure of a staged timer consists of two controlling variables, and a series of conditional statements within a GameMode block. One of these variables is an integer, and controls the "stage" of the sequence, and the other variable is a float that controls the timing of the sequence.

Because the timer used in a "Staged Timer" is a relatively short-term timer within a GameMode block, it is controlled via the GetSecondsPassed function. GetSecondsPassed returns the time passed, in seconds, since the script was last processed. For effect and object scripts, this will be the length of time since the last frame, but for quest scripts this will be affected by the quest's script processing delay.

Because GetSecondsPassed is not cumulative (i.e. the return value resets every frame, instead of counting up), a float variable is needed to store an accumulation of its return values in order to determine the time passed since a particular event. Luckily, the code required for this is very simple:

set fTimer to fTimer - GetSecondsPassed

You've probably noticed that I have set the timer to count down, instead of up. While timers that count up are possible, I find counting down easier to visualise for a "Staged Timer".

Now that we know basically how the "timer" part of our "Staged Timer" will work, it's time to look at how we're going to to control the "stage". Once again, the code here is going to be very simple - we'll use an integer variable to store the current "stage" of the sequence and a group of conditional statements to check the value of this variable. In this example, there are three stages, starting at stage 0:

if iStage == 0
	; Stage 0
elseif iStage == 1
	; Stage 1
elseif iStage == 2
	; Stage 2
endif

Actually, there is one more thing that we need to do in order to properly set up the "stage" part of our "Staged Timer". At the moment, nothing in the script will cause the stage to change. This means that, because all variables are automatically initialised to a value of 0, the "Scripted Timer" will remain at stage 0 forever. In order to change this, a "set" command must be used to increment the value of our "iStage" variable:

if iStage == 0
	; Stage 0
	set iStage to 1
elseif iStage == 1
	; Stage 1
	set iStage to 2
elseif iStage == 2
	; Stage 2
	set iStage to 3
endif

With the above code, the stage will be advanced by 1 every frame. While this behaviour isn't entirely necessary (you can impose extra conditions on stage increments) it is very simple so I'll stick with it for this example.

Now, the next step in setting up the structure of our "staged" timer is the most crucial step - we're going to combine the "timer" and "stage" parts of the script. In order to do this, we're going to need to include the timer in our set of conditional statements, so that the script will alternate between counting down the timer and executing the code for a particular stage. Because our timer counts down, it will be initialised by setting our "fTimer" variable to a value larger than 0, so here's how we can combine them:

if fTimer > 0
	set fTimer to fTimer - GetSecondsPassed
elseif iStage == 0
	; Stage 0
	set iStage to 1
elseif iStage == 1
	; Stage 1
	set iStage to 2
elseif iStage == 2
	; Stage 2
	set iStage to 3
endif

Now, in order to utilise our timer, we need to initialise it by setting our timer variable to a value greater than 0. Because the code for each stage runs once the timer has run out, we can reset the timer within all of the stages. For example, this code will introduce a 3 second delay between stage 0 and stage 1, followed by a 1 second delay between stage 1 and stage 2:

if fTimer > 0
	set fTimer to fTimer - GetSecondsPassed
elseif iStage == 0
	; Stage 0
	set iStage to 1
	set fTimer to 3
elseif iStage == 1
	; Stage 1
	set iStage to 2
	set fTimer to 1
elseif iStage == 2
	; Stage 2
	set iStage to 3
endif

That's the basic structure of the code set up, now we can start putting in code that actually does something, in the context of the scripted sequence that we're controlling. For this example, I'm going to set up my script so that it is attached to a token that will, when added to the inventory of an actor, cause them to play the appropriate "Explosive Planted" animation (for when you reverse-pickpocket a grenade or mine into their inventory) followed by a beep like a mine becoming active, and finishing with the actor dying in an explosion:

ScriptName PantsExplodedTokenScript

float fTimer
int iStage
ref rContainer

Begin OnAdd

	set rContainer to GetContainer
	if rContainer.IsActor
	else ; if rContainer.IsActor == 0
		RemoveMe
	endif

End

Begin GameMode

	if fTimer > 0
		set fTimer to fTimer - GetSecondsPassed
	elseif iStage == 0
		if rContainer.GetIsCreature
			; Creatures don't have the appropriate animations
		elseif rContainer.GetEquipped AnimationRestrictedArmors
			rContainer.PlayIdle PanicGrenadePlantPA
		else
			rContainer.PlayIdle PanicGrenadePlant
		endif
		set iStage to 1
		set fTimer to 3
	elseif iStage == 1
		rContainer.PlaySound3D WPNMineTick
		set iStage to 2
		set fTimer to 1
	elseif iStage == 2
		; Ensure that the explosion kills the actor
		rContainer.SetActorValue Health 1
		rContainer.PlaceAtMe MineFragExplosion
		RemoveMe
	endif

End

Don't worry if you don't know what the extra stuff that I've used there does - the important thing to take away from this is an understanding of the underlying structure of a "Staged Timer".

When setting up a "Staged Timer" to control a scripted sequence, the first question that you should ask yourself is how the event will be triggered. If the sequence should begin as soon as the script begins to run, like with a scripted effect, then the first stage should be "stage 0". Alternately, if the sequence should be triggered by some external event, such as a quest reaching a certain stage, then the first stage should be "stage 1" and the value of your "iStage" variable should be altered when this event occurs.

If you have any questions about anything on this page, or if you notice any errors, please feel free to contact me.