Using States In Papyrus

From the Fallout4 CreationKit Wiki
Jump to navigation Jump to search

Overview[edit | edit source]

Goal[edit | edit source]

The goal of this tutorial is to continue the lessons of Getting_Started_With_Papyrus. 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[edit | edit source]

Sure, But I need it to work DIFFERENT![edit | edit source]

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.

  • 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.
 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

A lesson in Threads[edit | edit source]

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 reccomended 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.

700px
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? 700px
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. 700px
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[edit | edit source]

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
  • 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 -
 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
  • We also need to tell the script what state to start in. This is done by prefixing our default state with "auto", as such:
auto STATE upPosition
  • 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.
 auto STATE upPosition
 	EVENT onActivate (objectReference triggerRef)
 		gotoState("busy")
 		playAnimationAndWait("open", "opened") ; animate
 		gotoState("downPosition")
 	endEVENT
 endSTATE
  • 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:
 STATE downPosition
 	EVENT onActivate (objectReference triggerRef)
 		gotoState("busy")
 		playAnimationAndWait("close", "closed") ; animate the other way
 		gotoState("upPosition")
 	endEVENT
 endSTATE
  • Let's prep this for testing with a couple of debug messages. Match your script to this, save, compile and launch the game!

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

Viewing trace() messages in the papyrus TDT log

Next Steps[edit | edit source]

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.