Differences from Previous Scripting
This covers differences from ObScript to Skyrim Papyrus. To see differences between Skyrim and Institute, a new page has been set up.
- 1 What's New?
- 1.1 New Concepts
- 1.2 Syntax Changes
- 1.3 Functionality Changes
- 2 Setup
Papyrus, the new scripting language, is in many ways similar to the old scripting language used in Bethesda Game Studios' games. However, due to the introduction of added functionality and flexibility, there are many significant changes to syntax, workflow, and occasionally functionality. This page is for people are already familiar with the concepts of a scripting language, and know a little bit about what Papyrus is. If you haven't already, please be sure to first read the Papyrus Introduction.
For a technical, detailed explanation of the new scripting language, please refer to the Papyrus Language Reference category.
The new scripting language allows you to do a lot more. This means that the amount of things that you will have to know regarding the new scripting system will increase. On the plus side, certain additions to the scripting language make things much faster and easier to do. Among these new additions, the ability to make your own functions, and the ability to use timers, looping logic and states should make your life easier. I've personally found that it's much easier to think in a more straightforward manner when scripting. There's much less jumping back and forth between GameMode and other events. I find that I have to keep track of much fewer tracking and enable/disable variables as well.
In the old system, running an event meant that you would run through all of the script in that event immediately and in sequence. If you didn't want part of the script to run, you would have to use an IF statement to conditionalize out part of the script. If you wanted part of the script to run at a later time, you would have to "turn on" a timer in the GameMode block by setting a condition and then running getSecondsPassed until you could run another section of the gameMode block.
In the new system, you can use Latent Functions (such as the Wait() function) in order to temporarily pause the script. Once the pause is over (it is usually defined by you somehow) then the script will pick up running again right where it left off.
Some Latent Functions:
;waits for 5.1 seconds Wait( 5.1 )
;This plays an animation, and then waits for an animation event to occur ;This animation event is set up by the artist in Havok Behavior. PlayAnimationAndWait( "AnimationName", "EventName" )
In the old system, you could loop by setting up a series of IF statements and have them increment a counter until a certain condition (usually using that counter) was no longer true.
In the new system, you can use looping statements (currently While is implemented) inside any events you want. As long as the expression following While is true, the loop will continue running.
While counter <= 100 ;do something 100 times. ... counter += 1 ;Shorthand for "counter = counter + 1" EndWhile
One Script Running Multiple Instances of the Same Event
Events, like OnActivate, used to run in a single frame. With the addition of multithreading, looping, and latent functions like Wait(), running a single event may take a long time. That may bring you to ask "What happens if I cause an OnTriggerEnter event before the previous OnTriggerEnter event is finished?" It turns out that one single script is capable of running multiple instances of a single event. To prevent this, you can use States or control variables to disable or change what happens if an event is called before another instance of the same event is called before the first one is finished.
There are a few keywords in the new scripting language that are useful for one reason or another.
- None - this indicates that a reference is essentially empty.
refVariable01 = GetLinkedRef() if refVariable01 != None ;do something with refVariable01 because GetLinkedRef() returned something endif
- Self - variable, which refers to the current instance of the script. It's similar to the getSelf function from the old scripting language, but it gives the script instance instead of the reference running the script.
- Parent - special keyword that you use when calling a function to ensure that you call the function on the script you extend (your parent), and not the function you have in your local script.
- Hidden - hides a script or property from the editor.
- Conditional - flags a script or a variable as visible to the condition system. Note that the script must be flagged as conditional for any of the variables in it to be visible, and you may only have one conditional script attached to a single object at a time.
- True - Used for Booleans instead of 1
- False - Used for Booleans instead of 0
- Script variables can be initialized to a specific value when they are defined using "= <value>" syntax.
int myVar = 20
- Function variables can be initialied with full math expressions, instead of just simple values.
- Scripts are types - as in, if you want to call a special script function, or access a property on that script, you must first cast the object you have to the script you expect to have.
- Casting between types uses "<genericType> as <specificType>" syntax. If you try to cast a variable into something it isn't, you will get the "None" value.
Function DoFunnyDance(Actor target) FunnyDanceScript targetScript = target as FunnyDanceScript if targetScript != None targetScript.doingFunnyDance = 1 targetScript.DoFunnyDance() else Print("Cannot do funny dance, be cause the actor doesn't have the right script") endIf endFunction
- Anything can be cast to or treated as a boolean.
- Int and float: non-zero values are true, zero is false.
- Strings: non-empty strings are true, empty strings are false.
- Objects: non-none objects are true, none objects are false.
- Arrays: non-none arrays or arrays with 1 or more elements are true (even if the elements are false). None arrays or arrays with 0 elements are false.
- Anything can be cast to a string. Numbers will be converted to a string representation, booleans will be simple "true" or "false" text, and objects will be converted to a readable format detailing the script name and the actual in-game object it is attached to.
- Strings can be cast to numbers if the string actually represents a number.
- If the compiler can automatically cast for you with no chance of error, it will do so. The following casts are done automatically:
- Int -> Float
- Anything -> String
- Anything -> Bool
- Object -> Parent Object
- Anything can be cast to or treated as a boolean.
- You can use the "+" operator to concatinate ("paste together") strings.
string Hello = "Hello" string World = "World" Trace(Hello + " " + World) ;prints "Hello World" to the log
- The "!" operator has been added, and stands for "not". The expression "!value" is true if, and only if, value is false.
- Hexadecimal numbers are prefixed with "0x" - most commonly these are Form IDs. For example, if you want form ID 00012EC7, you use 0x00012EC7 (the leading zeroes are not required)
- Operators like "+=" and "-=" have been added. They work as follows:
myNumber += 1 myFloat -= 2 + 4 * player.getAV("repair")
Is equivalent to:
myNumber = myNumber + 1 myFloat = myFloat - (2 + 4 * player.getAV("repair"))
All math operators have equivalent versions: +=, -=, *=, /=, and %=
Functions now take parameters from within parentheses.
PlayGroup Forward 1
This change to syntax should help things to be a little more explicit. Also, this should help things to be less confusing when using user-defined functions.
set variable01 to 25
variable01 = 25
This new syntax is a more standardized way of doing things.
Event Declaration Syntax
Begin OnTriggerEnter Player ... End
Event OnTriggerEnter( Actor akTriggerer ) ... EndEvent
Again, this is a more explicit way of doing things. "Event" makes it much more clear that you are starting an event, and "EndEvent" makes it more clear that you're ending an event. Events are essentially the same things as user-defined functions, except that something that happens in the game can trigger that event to occur.
There are a few things to keep in mind when keeping things like performance in mind. With the old scripting language, writing inefficient script would eventually bog down the framerate of the game. In the new language, each script gets an allotted amount of processing time. After it uses up that processing time, the script will pause until it's allowed to have more processing time. This processing time is measured by function calls and logic done by the scripting language. You're much more likely to bog down all the scripts in the game then the game's actual framerate with a poorly-written script.
With the new language being fully multithreaded, timing is not quite as reliable as it was with the previous system. Previously, a script could say "I want to do this immediately," and the governing system would have no choice but to agree. A script in the new language can say "I want to do this immediately," and the script manager will essentially tell the script to take a ticket and wait in line. Telling a script to wait for 0.5 seconds really means that the script will wait for 0.5 seconds plus the time it takes for the script to get its number called. This amount of time is usually quite small, but it could have an impact on scripts where timing needs to be very precise.
Hooking Stuff Up in the Editor
Currently, scripts can be added to references, quests, activators, and other types in the editor. Open up the edit window, and find either a "scripts" tab, or a script list on the main dialog. Then click the "Add Script" button to pick from a lists of scripts to add to the object. You can doubleclick on the the script once it has been added (or click on the Properties button) to open the Properties window. Properties are essentially variables that can be set in the editor per instance of the script on the reference.
Multiple scripts can be added to the same object.
If you add a script to a base object, all references of the object will also gain those scripts, and the properties you set on those scripts on the base object. However you can easily override those settings just on that particular reference if you want that one to behave slightly differently then the others.
Before getting directly into properties, it should be noted that in the new scripting language, variables declared in a script are considered private. This means that if you're not inside the script, you can't set those variables or even see what values they have.
Properties are much like variables except they can be accessed from outside of the script, and can even be accessed and set in the editor per each reference. Pretty neat, if you ask me.
int property NumShots auto
This is an example of how to declare a property in script. NumShots is the property name, it is an integer, and the "auto" modifier is simply a quick way of defining a property. "Auto" will let you see and change the value from outside of the script, and you can set it from inside of the editor by going to the Reference Window-> Scripts Tab-> Script Name-> Properties, and you'll see the property and be able to put any int you want in there.
There's a longer way to declare properties as well, and this can come in handy if you want to do specific things, such as let other scripts see the property but not set it, or set some bounds on valid input, or even call some piece of script when a value gets read or written.
int shotCount = 20 ;This is an internal variable, can't be seen from outside, but you can set defaults this way. int Property NumShots ;This is what's seen externally int Function get() return shotCount endFunction Function set(int value) if (value > 50) ;value is too big, so clamp it at 50 and write a warning to debug debug.trace ( "Tried to set NumShots to a number over 50. Clamping to 50" ) shotCount = 50 elseIf (value < 1) ;value can't be less than one or there would be nothing to shoot. debug.trace ( "Can't set value to less than one. Not going to change the value." ) else ;everything's ok, just set value to the number of shots shotCount = value endIf endFunction endProperty