Mod:Creation Kit/Using States In Papyrus

=Overview=

Goal
Upon completion of this tutorial, the user should understand:


 * Script States in Papyrus and their use
 * Coping with objects having multi-state behavior graphs
 * Some implications of threaded scripting

=Tutorial=

Sure, But I need it to work DIFFERENT!
Let's assume that the down-up functionality we set up in the previous tutorial isn't quite what you want. Instead, we'll make this lever work as a toggle. Papyrus allows us to do this with the very same piece of art, but our script will have to become more sophisticated to handle it.

scriptName myScript extends ObjectReference import Debug		; Import Debug.psc for access to debug notices int myState		; 0 == in the up position 	_|_ ; 1 == in the down position 	_\_ EVENT onActivate ( objectReference triggerRef ) if mystate == 0 playAnimationAndWait("open","opened")	;animate down and wait messageBox("I am now DOWN") myState = 1 elseif mystate == 1 playAnimationAndWait("close","closed")	;animate up and wait messageBox("I am now UP") myState = 0 endif endEVENT
 * We can study the behavior graph of NorLever01 or consult with the responsible artist to learn that the open event triggers our animation into the down position, and opened signals that as having completed. Likewise, close plays the reverse animation and closed alerts us that the switch has returned to its starting position
 * Those familiar with legacy scripting may be tempted to do the following - which will appear to work fine. This isn't the best use of Papyrus, however, and is vulnerable to problems if the player activates the object while it's animating.

A lesson in Threads
To understand the significance of our next step, it helps to have an understanding of threads. This is an entirely new concept, relating to how Papyrus scripts are processed by the game. Most users will have a little difficulty understanding threads.

The following is a crash-course in threaded scripting. Feel free to Skip Ahead for now if you are willing to trust me and get on with the scripting. Understanding threads and their implications is important, however, so it's highly recommended you read on.

Papyrus is a threaded scripting system, which essentially means that the game can grab a piece of script and run it independently. Because of this, if the player activates multiple times, multiple "threads" of this script can run.

In the image above, the player is activating our lever. Because this a nice and cooperative player, he's only activated it once. The game sends our script notification of the activate event. Because our script contains an onActivate Event, the game creates a "Thread", which you can think of as a set of instructions copied from our script which the game is going to run in a moment.

Very soon - probably on the next frame - the game parses the thread. In our case this thread has some duration, since it waits for the lever animation to tell us when it's done, and the thread is thrown out.

So the player activates the switch, which notifies our script to create a thread. The game processes the thread, our switch animates, all is well. What happens with a less cooperative player, though?

Here the player has activated the lever several times in a short period of time. That's valid input, and our script is about to receive it. In comes the first activate event. A thread is created and begins processing. Here comes the second activation - another thread is created shortly after the first. This continues as long as the player spams activates on our lever.

The trouble comes with the time-sensitive nature of our script and threads in general. Thread #1 knows nothing about Thread #2 and so on, so each one attempts to raise an animation event on the lever and waits for the lever to notify us that it's done.

This can get messy fast. We need a way to keep things organized.

The above example represents the script as we're about to write it. When the onActivate event is raised in the script by Thread #1, we will be telling the script to change states. This new state we create is empty, so those threads die out harmlessly. Meanwhile our instructions parse properly and we get a single, clean signal from Thread #1 when we're ready to move on.

Using States
So, instead of using variables to emulate states, Papyrus can let us actually define a state within the script. This is done simply by creating a new block. Add this to your script now: STATE upPosition ; This is the state I'm in when up and at rest. endState STATE busy ; This is the state when I'm busy animating endState STATE DownPosition ; This is the state I'm in when down and at rest. endState auto STATE upPosition auto STATE upPosition EVENT onActivate (objectReference triggerRef) gotoState("busy") playAnimationAndWait("open", "opened") ; animate gotoState("downPosition") endEVENT endSTATE STATE downPosition EVENT onActivate (objectReference triggerRef) gotoState("busy") playAnimationAndWait("close", "closed") ; animate the other way gotoState("upPosition") endEVENT endSTATE ''NOTE - Those messageBox debugs might get a little intrusive, and you certainly don't want them appearing for other developers when they look at your content. Importing debug.psc also gives access to the trace function. Just replace messageBox with trace and your debug messages will only appear on the Papyrus TDT page and the Papyrus log file, located in the Logs/Papyrus folder in your game folder. (Log 0 is the newest log, the game will save your 4 newest log files for you) scriptName myScript extends ObjectReference import Debug			; Import Debug.psc for access to debug notices auto STATE upPosition EVENT onActivate (objectReference triggerRef) gotoState("busy") messageBox("Animating Down") playAnimationAndWait("open", "opened") ; animate messageBox("Switch now in DOWN position!") gotoState("downPosition") endEVENT endSTATE STATE busy ; don't do a thing! EVENT onActivate (objectReference triggerRef) messageBox("I'm busy right now, so I'm ignoring you.") ; this onActivate is just for testing. We'll remove it later. endEVENT endSTATE STATE downPosition EVENT onActivate (objectReference triggerRef) gotoState("busy") messageBox("Animating Up") playAnimationAndWait("close", "closed") ; animate the other way messageBox("Switch now in UP position!") gotoState("upPosition") endEVENT endSTATE
 * We've just created a new state. STATE, like EVENT, is a blocktype, so you'll need to tell the script where it ends.
 * One state isn't enough, however. We'll need to create two more for this example.  Go ahead and add these as well -
 * We also need to tell the script what state to start in. This is done by prefixing our default state with "auto", as such:
 * Once the switch is activated our first priority is to block subsequent activations until the animation completes, so we'll put our script into the busy state.
 * Then we simply need to set up our states to react appropriately to activation. We already know that we want to call the "open" animation event by default, so we can just cut that line of script and paste it next.
 * Finally, when the animation has completed, we'll put our script in the downPosition state before ending our EVENT and STATE.
 * The only job we need our "busy" state to perform is to do is ... nothing. So just leave that state empty.
 * Our "downPosition" state is essentially the same as "upPosition", but it's going to be firing a different animation event and state, so it should look like this:
 * Let's prep this for testing with a couple of debug messages. Match your script to this, save, compile and launch the game!

Next Steps
The next tutorial will give this lever a purpose in life - we're going to set up a portcullis gate for it to open and close. We'll also put a couple of finishing touches on our script and discuss one or two more new scripting concepts, such as data type casting.