3rd June 2012

States in Papyrus

One very useful feature of Papyrus is the ability to create "States". States essentially let your script swap between using different sets of function definitions.

Creating States

In order to create a state, the "State" and "EndState" keywords are used, along with an identifier for your state. For example:

ScriptName StatesExample extends ObjectReference

State Active
	; Code for the "Active" state goes here
EndState

Controlling the Current State

There are essentially 2 ways to control the current state of your script. The first way is by using the Auto keyword to specify a default state like this:

ScriptName StatesExample extends ObjectReference

Auto State Inactive
	; Code for the "Inactive" state goes here
EndState

State Active
	; Code for the "Active" state goes here
EndState

If a script has an "auto" state, then that script will start in that state. Each script can only have a single default state, though. If you try to create more than one default state in a single script your script will not compile. Instead, you will be given an error like this:

script already has the automatic state set to inactive, cannot have more than one

The other way to switch between states is by calling the GoToState function. This function takes a single parameter, which is the identifier for the state you want your script to enter as a string. For example:

ScriptName StatesExample extends ObjectReference

Auto State Inactive
	Function Toggle()
		GoToState("Active")
	EndFunction
EndState

State Active
	Function Toggle()
		GoToState("Inactive")
	EndFunction
EndState

Function Toggle()
	; Empty function definition to allow Toggle to be defined inside states. More on this soon...
EndFunction

Event OnActivate(ObjectReference akActionRef)
	Toggle()
EndEvent

When the above script is first initialised, it enters the "Inactive" state automatically. In this state, the function Toggle is defined such that it will send the script into the "Active" state. Likewise, the definition of Toggle in the "Active" state sends it back into the "Inactive" state.

When the OnActivate event is called on an ObjectReference with this script attached, Toggle is called, and the effect it has will depend on the current state of the script. So, when the object is activated while in the "Inactive" state, it will be sent to the "Active" state, and when it is activated in the "Active" state it will be sent to the "Inactive" state.

In this example, OnActivate is only defined outside of any states, so it will use the same definition regardless of the script's current state.

The Empty State

All code that exists outside of all the states you've created is in what can be called the "Empty" state. If no default state is created, the script will start in this "Empty" state.

Before any function can be defined within a state, it must be defined within the empty state. This is true even if a script has a default state, and so would never be in the empty state.

The reason for this is so that if a function is called but no definition exists within the current state, the script can use the definition in the empty state instead. In this way, any function definitions within the empty state are essentially the default definitions for that function. Unless they are overridden by a definition in a state, they are the one that will be used.

If a function has been defined in the empty state of a script which the the current script extends, then that function can be defined inside the current script's states without being defined in the empty state. This is because the extended script's definition can be used as the default definition for that function. This means that events can always be defined inside states without having a definition in the empty state, as available events are defined in the native scripts that all custom scripts must extend.

OnEndState and OnBeginState also don't need to be defined before they're used inside a state, but that's for a different reason. Unlike other native events, these two are not defined inside native scripts. Because of their function, they only make sense when defined within a state, so they have been given the special property of not requiring a definition within the empty state.

Detecting and Using State Changes

Whenever you call GoToState, three things happen:

  1. The OnEndState event is called
  2. The state is changed
  3. The OnBeginState event is called

It's worth noting that the OnBeginState event is not called when the script initially enters its default state. Also, your script will still compile if the string you pass to GoToState doesn't match an identifier for an existing state, so be careful when doing things like changing the name of a state, and be aware that the compiler won't tell you if you've incorrectly typed the name of your state.

I find that the most useful way in which these two events can be used is to ensure that, for states that can be triggered in multiple ways, the same thing will always happen when the script enters or leaves them.

For example, if a light switch script has an "On" state and an "Off" state, and it should Enable a set of lights when switched on and Disable a set of lights when switched off, it could be useful to use those events. Especially if the light switch could be triggered either by being activated or by being hit:

ScriptName LightSwitchActivateOrHit extends ObjectReference
{ When this switch is activated or hit, switch its associated lights on or off }

ObjectReference Property MasterLight Auto
{ The "Master" light, which is the enable parent of all other lights that should be affected by this switch }

Function Toggle()
	; Empty function definition to allow Toggle to be defined inside states
EndFunction

Auto State Off

	Event OnBeginState()
		MasterLight.Disable()
	EndEvent

	Function Toggle()
		GoToState("On")
	EndFunction

EndState

State On

	Event OnBeginState()
		MasterLight.Enable()
	EndEvent

	Function Toggle()
		GoToState("Off")
	EndFunction

EndState



Event OnActivate(ObjectReference akActionRef)
	Toggle()
EndEvent

Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
	Toggle()
EndEvent

When this script is first initialised, it will enter the "Off" state, as this is the state that has been created with the "Auto" keyword. From there, when it is activated or hit, the Toggle function is called.

The default definition of this function, in the empty state, is empty. However, a definition exists in the current state, which overrides the default definition, so GoToState is called and the script goes to the "On" state.

When GoToState is called, the OnEndState event is called first, but it has no definition in either the empty state or the current state so nothing happens as a result. Then, the script moves to the "On" state and the OnBeginState event is called, which causes the ObjectReference stored in the MasterLight property (and all of its enable children) to be enabled.

The next time the switch is activated or hit, the same thing will happen, except instead of going to the "On" state the script will go to the "Off" state and, as a result, all the lights will be disabled.

Other Uses

Disabling a Function

There are various other useful things that can be done with states. One example is that you can redefine functions to be empty, so that while the script is in that state the function will do nothing. This can be particularly useful when you want to disable something once its job is complete. For example:

ScriptName ActivateThreeTimes extends ObjectReference
 
int iTimesActivated = 0
 
State Inactive
 
	Event OnActivate(ObjectReference akActionRef)
		; Do nothing
	EndEvent
 
EndState
 
 
Event OnActivate(ObjectReference akActionRef)
	iTimesActivated += 1
	Debug.Notification(iTimesActivated)
 
	If (iTimesActivated >= 3)
		GoToState("Inactive")
	EndIf
EndEvent

In this example, the first 3 times the scripted object is activated it will print out a notification to the player telling them how many times it has been activated. However, after the 3rd time it's activated, it will enter the "Inactive" state, and when the OnActivate event is called on it in this state nothing will happen.

Without using events, this definition of OnActivate could be used to the same effect:

Event OnActivate(ObjectReference akActionRef)
	If (iTimesActivated < 3)
		iTimesActivated += 1
		Debug.Notification(iTimesActivated)
	EndIf
EndEvent

However, with this setup the variable iTimesActivated will be checked on each activation even after the first 3 activations have been taken place, so using states is a more efficient method. In this example it won't make much of a difference, but in more complicated setups it can be quite an important thing to know about.

Stopping a Function From Running Again Before Finishing

It's possible that a function may be called again before it has finished running from the last time it was called. This is particularly likely for functions that take some time to complete, but are called often. By using states, it is possible to prevent a function from running more than once simultaneously:

ScriptName LongFunctionScript extends ObjectReference
 
Function LongFunction()
	GoToState("Busy") ; Don't run this function again until it's finished
 
	; Do something that takes a long time
 
	GoToState("Waiting") ; The function has finished, so it can be called again
EndFunction
 
State Busy
	Function LongFunction()
		; Do nothing
	EndFunction
EndState

In this example, when LongFunction is called the script is sent into the "Busy" state. This doesn't stop the current call of LongFunction from running, but if it's called again while in this state then nothing will happened, as it has an empty definition in the "Busy" state.

Once the body of LongFunction is complete, it is sent to the "Waiting" state. This script doesn't have a "Waiting" state defined, so for all intents and purposes it's being sent back into the empty state. I could have created a "Waiting" state to use, but that would still have required me to write a function definition for LongFunction in the empty state so I decided to just use that.

Conclusion

As I'm sure you can see, states can be quite a useful tool to know about. They're great for keeping scripts clean and clear, and they can make your scripts much easier to maintain if you make good use of them. The Creation Kit wiki has some more information on states that you can read about here if you want to know more.