Threading Notes (Papyrus)

Revision as of 11:42, 6 December 2011 by imported>JBurgess (→‎Note about tracing "GoToState()")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Papyrus is a threaded scripting language. In essence, this means that the game can run multiple scripts in parallel. This allows the Creation Engine to manage script processing more effectively. Papyrus scripters should have at least a cursory understanding of the ramifications this creates for them.

This article attempts to explain threads in layman's terms, in the context of scripting for Papyrus.

Threading

The basic gist is this:
Only one thing at a time can be doing anything with a script.

If multiple things (threads) try to manipulate the script at the same time, a "queue" forms of all the threads which essentially wait in line for the script to stop doing something, then it lets the next thread in line in.

If the script calls another function in an external script, or calls a "latent" function (a function that does something on it's own before returning), it lets in the next thing from the cue while the other thread is off doing that other thing. Then while the new thread is occupying the script, when the previous thread that was off doing it's thing wants to come back and continue processing, now it has to wait in line until the new thread is done with the script (or it calls a latent function or function on another script)

Example: ObjectA has script with a function: DoSomething()

ObjectB calls ObjectA.DoSomething()at the exact same time ObjectC calls ObjectA.DoSomething()

First ObjectB finishes running ObjectA's DoSomething() and then ObjectC runs it.

But if DoSomething() in a script on ObjecttA looks like this:

 Function DoSomething()
    SomeThing1() ;another function in ObjectA
    ObjectD.DoSomethingElse() ;a function in a script on ObjectD    
    Something2() ;Another function in ObjectA
 EndFunction

Then first ObjectB calls ObjectA.DoSomething(), and while ObjectA is busy calling Something1() for ObjectB, ObjectC waits in line, not yet processing ObjectA's DoSomething().

But as soon as ObjectB's thread calls ObjectD.DoSomethingElse() because it has called a function on a different script/object, ObjectA waves in ObjectC who was waiting patiently to call DoSomething().

Now while ObjectC's thread is processing SomeThing1(), ObjectB's thread is done with DoSomethingElse on ObjectD, but before it runs Something2() it now must wait in line until ObjectC's done with Something1() and itself moves out to ObjectD's DoSomethingElse(). As soon as ObjectC's thread heads out to DoSomethingElse() in ObjectD, ObjectA waves in ObjectB's thread who is returning from ObjectD's DoSomethingElse() and will now be allowed to continue on with Something2()... and so on...

Threading: A Basic Example

In the image below, the player is activating a lever. Because this a nice and cooperative player, he's only activated it once. The game sends Papyrus a 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. The game queues this thread up, and will process it momentarily.

 

Very soon - probably on the next frame - the game processes the thread. Let's pretend that the contents of our onActivate() event look like this:

; Note - this is a non-functional snippet
EVENT onActivate()
    wait(5.0)
    player.additem(gold001, 10)
endEVENT

The player activates the switch, which notifies Papyrus to create a thread containing those instructions - After five seconds, ten gold pieces are added to the player inventory. 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 - thread is created and begins processing. Here comes the second activation - and another thread is created shortly after the first. This continues as long as the player spams activations on the 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 will wait for five seconds, then add gold to the player.

This can get messy fast. We need a way to keep things organized.
 
The above example represents the script as we'd want to re-write it - blocking further activations after the first. There are several ways to get this result. This example will be solved using States.

States are very useful for creating a script that reacts to events differently based on different circumstances. The pseudo-script example below shows an example.

; Note - this is a non-functional snippet
STATE readyState
    EVENT onActivate()
       gotoState("emptySTATE")
       wait(5.0)
       player.additem(gold001, 10)
       ; want to allow subsequent activatiosn after the 5 sec delay?  add a "gotoState("readyState") here.
    endEVENT
endSTATE

STATE emptySTATE
    ; This state contains no events
    ; Note that other scripts attached to this reference will still process their activate events
endSTATE

Note about tracing "GoToState()"

In the previous example, take note of where goToState() is called. Why? Because setValue() is in an external script, it creates an opportunity for another thread to start processing your script before you go to the state you expect it to be in. Therefore, go to your desired state first. Your thread will complete the instructions below the gotoState() call just fine, and future threads will treat the script as being in the state - "XYZ" in the example below - when they encounter it.

In other words - Don't do this:

 gigawatts.setValue(1.21)
 GoToState("XYZ")

Instead, do this:

 GoToState("XYZ")
 gigawatts.setValue(1.21)

Another example

From an email to Jeff asking if calling IncrementMyProperty from an external script multiple times in a row was "thread safe."


...assuming that all functions and the property itself are in the same script.

 Function IncrementMyProperty
   myProperty += 1
 endFunction

100% “thread safe”. myProperty will correctly increment. Right now it resolves to two function calls and as far as you should ever know, will always resolve that way.

 Function IncrementMyProperty
   myProperty += 1
      if myProperty == 5
         ;do something
      endif
 endFunction

myProperty is guaranteed to be 5 when “do something” executes. If “do something” goes latent, or calls a function outside your script or its parents, myProperty may change.

Function IncrementMyProperty
   myProperty += 1
   if myProperty == 5
      myQuest.setStage(10)
   endif
 endFunction

 ;Elsewhere In MyQuest’s stage 10 fragment:
 If myProperty == 5
   ;do something
 endif

SetStage is Latent, therefore myPropery may change, even if you just use “SetStage(10)” and your script extends Quest and is attached to the quest.

In the case of the fragment – it is entirely likely that myProperty will no longer equal 5 by the time the fragment runs. Remember: you called setStage, NOT a function. Fragments execute “some time in the future” (which happens to be very soon in most cases). This is one case where a function call is much more useful than a fragment.

Please note:
If myProperty is NOT in your local script, then “myProperty += 1” may not increment. This is because it resolves to “myProperty.set(myProperty.get() + 1)”, and myProperty’s value may change between the get and set calls since it isn’t on your own script. [Editor's note: Which is why you would write a IncrementMyProperty() function in that other script -- JPD ]

-Jeff

See Also