Difference between revisions of "Scripting for Beginners"
imported>Cipscis (Added disclaimer at top about external links →External Links: Renamed "See Also" section) |
imported>Cipscis m (→External Links: Clarified location of tutorial hosted on cispcis.com) |
||
Line 476: | Line 476: | ||
==External Links== | ==External Links== | ||
*[http://www.cipscis.com/fallout/tutorials/beginners.aspx Scripting For Beginners] | *[http://www.cipscis.com/fallout/tutorials/beginners.aspx Scripting For Beginners on cipscis.com] | ||
*[http://videojuegos.clandlan.net/videojuegos/index.php/Tutorial_Scripting_Fallout3 Scripting For Beginners (Spanish)] (translated by '''Bauhaus''') | *[http://videojuegos.clandlan.net/videojuegos/index.php/Tutorial_Scripting_Fallout3 Scripting For Beginners (Spanish)] (translated by '''Bauhaus''') | ||
[[Category:Scripting]] | [[Category:Scripting]] | ||
[[Category:Tutorials]] | [[Category:Tutorials]] |
Latest revision as of 18:21, 11 October 2009
Disclaimer[edit | edit source]
This tutorial has been hosted in multiple locations, and the information contained in each version may differ. To view the other versions, see the External Links section at the bottom of this page.
Introduction[edit | edit source]
This tutorial is aimed at people who want to learn the basics of scripting for Fallout 3, but have no prior programming experience. If this sounds like you, then hopefully you'll find this tutorial helpful. Before we start, there are a couple of things that we should go over:
- The scripting language used in Fallout 3 is not case-sensitive. While this means that you can have a perfectly good script with inconsistent capitalization, it is a good idea to try to standardize your capitalization, as it can help to make your scripts easier to read.
- This tutorial assumes that you are competent when it comes to using the GECK, or some other tool capable of creating and editing data files for Fallout 3. If this is not the case, then I recommend that you look at some of the official tutorials for the GECK to familiarize yourself with it before you attempt to follow this tutorial.
- If you don't understand any of the terminology used in this tutorial, you can check the glossary at the bottom of this page.
Now, let's get started, shall we? The first thing you need to know about scripting in Fallout 3 is that every script except for Result scripts (short script fragments attached to quests and certain other objects) must start with a ScriptName declaration, which declares the EditorID of the script.
For example:
ScriptName MyFirstScript
If you look through some of the scripts that exist in Fallout3.esm, you'll probably notice that many of them use scn instead of ScriptName. This is perfectly alright, as scn is an alias of ScriptName. That means that we could just as easily use this for our ScriptName declaration:
scn MyFirstScript
Congratulations, you've written your first script! Of course, it's not particularly exciting - it doesn't actually do anything at all at the moment. Why don't we do something about that? Let's edit that script so that it causes the text "Hello World!" to display on screen when you pick up a bobby pin.
The function that we're going to use to display this message is called ShowMessage, but before we can put this in the script we need to tell it when we want it to run, which is done by using Begin/End blocks (again, except for in Result scripts).
For every frame in which a script runs, the conditions of each Begin/End block are checked sequentially from top to bottom. If the specified condition is true, then the code contained within the Begin/End block will run, otherwise it will not run for this frame. As you can see, they are essentially highly specific conditional statements.
Because we want our code to run whenever a bobby pin is added to the player's inventory, we will be attaching the script to the bobby pin form (EditorID Lockpick), and our Begin/End block will use the OnAdd blocktype, and use player as a parameter:
ScriptName MyFirstScript Begin OnAdd player End
In order to attach our script to the bobby pin form, we have to specify which type of script it is. With the exception of Result scripts, all scripts are divided up into three types (see the glossary for descriptions):
- Object
- Quest
- Effect
Because we want to attach this to the bobby pin, which is a Misc Item form, it should be specified as an object script.
As I mentioned before, the function that we're going to use in this script is ShowMessage. However, before we can use this function, we need to create a message form to use as a parameter. This message form should have the Message Box checkbox unticked, and should have "Hello World!" written in the Message Text box. Let's give it the ID "MyMessage".
Now that we've set up a message to use, we can use ShowMessage in our script in order to display this message:
ScriptName MyFirstScript Begin OnAdd player ShowMessage MyMessage End
Now, if this script is attached to the bobby pin form, "Hello World!" will be displayed in the upper-left hand corner of the screen whenever a bobby pin is added the player's inventory.
Conditional Statements[edit | edit source]
At the moment, the code contained within our script will only run if a bobby pin is added to the player's inventory. This is all well and good, but what if we wanted to display a different message if it was added to the inventory of any actor other than the player, and display the "Hello World!" message if it is added to the player's inventory? Let's call this other message "MyOtherMessage", and have it say "Goodbye World!"
Of course, we could achieve this by having multiple OnAdd blocks, but then we would end up having duplicate code within our script, which is something that we should always try to avoid. Instead, we will use another function, GetContainer, to determine which actor has the scripted item in their inventory.
Just calling GetContainer won't be enough, we're going to have to check its return value and use that check to determine what sections of our script should run. In order to do this, we are going to use an "if" statement.
An "if" statement is very similar to a Begin/End block. It begins with the keyword "if", followed by the condition which should be checked, and it ends with the keyword "endif". "If" statements are different to Begin/End blocks in that the condition is completely defined by you, as opposed to being chosen from a list of usable conditions. Here is how we will change our script so that the code within the OnAdd block will run when the bobby pin is added to the inventory of any actor, but the message will only be shown if it is added to the player's inventory:
ScriptName MyFirstScript Begin OnAdd if GetContainer == player ShowMessage MyMessage endif End
Now, a lot of people misunderstand just how the conditions of "if" statements work, so let's work through that one. First, GetContainer is called. GetContainer returns the RefID of the reference that has the scripted item in its inventory, so once it is called you can visualize its return value as replacing it. For example, if the scripted item is in the player's inventory, then GetContainer will return the player's RefID, so you can visualize the condition like this:
if player == player ; Do something endif
The second part of the condition, "==" is an operator. The "==" operator returns 1 if both arguments are equal, and 0 if they are not. Therefore, because "player" and "player" are equal, the condition evaluates to 1:
if 1 ; Do something endif
Because the condition evaluates to a true value, the code contained within the "if" statement will run. If, however, GetContainer returns the RefID of a reference other than the player, then the condition would evaluate to 0, which is a false value, the code within the "if" statement will not run.
Now, as you can see, our script has pretty much exactly the same functionality as it had previously, although it is slightly less efficient. What we are going to do now is add an alternate section of code that will only run if the scripted item is added to the inventory of a reference other than the player. The most obvious way in which we could do this would be to create a second "if" statement, that checks a different condition:
if GetContainer != player ; Do something else endif
As you can see, this new condition is the exact opposite of the condition that we used earlier. Because the return value of GetContainer will be the same in both conditions, we can be sure that one and only one of these conditions will always evaluate to true. This means that, instead of creating an entirely new "if" statement, we can use an "else" statement.
An "else" statement can only be used in between an "if" statement and its corresponding "endif" statement, and basically means "if all prior conditions returned false". Let's update our script again, this time to display "MyOtherMessage" if a bobby pin is added to the inventory of a reference other than the player:
ScriptName MyFirstScript Begin OnAdd if GetContainer == player ShowMessage MyMessage else ShowMessage MyOtherMessage endif End
Now, when the OnAdd block runs, the "if GetContainer == player" condition is checked first. If it evaluates to true, then the "ShowMessage MyMessage" line will run, and the script will then skip to the corresponding "endif" statement - the "else" condition and the code it contains will be completely skipped. However, if the first condition evaluates to false, then the second condition, "else", is checked. As I mentioned before, "else" statements basically mean "if all prior conditions returned false" - if an "else" statement is ever checked, the code within it will run and all following conditions will be skipped.
Now our script has two possible outputs - if a bobby pin is added to the player's inventory, "MyMessage" will be shown, and if a bobby pin is added to the inventory of a container other than the player, "MyOtherMessage" will be shown. This is cool, but what if we want to have more possible outputs associated with our script. For example, what if we wanted to play a different message again (let's call it "MyDadMessage" and have it say "Hello Dad!") if a bobby pin is added to the player's father's inventory (his EditorRefID is MQDadRef)? Perhaps the most obvious approach to this would be to place a new "if" statement within the "else" statement like so:
ScriptName MyFirstScript Begin OnAdd if GetContainer == player ShowMessage MyMessage else if GetContainer == MQDadRef ShowMessage MyDadMessage else ShowMessage MyOtherMessage endif endif End
As you can see, if a bobby pin is added to the inventory of a reference other than the player, the script will then check if the bobby pin has been added to the inventory of the MQDadRef reference. While this will work perfectly, the scripting language used in Fallout 3 includes a nifty tool that allows us to use a lot of different conditions together like this without having to nest all of our "if" statements within one another. This tool is known as an "elseif" statement, and can be used like this:
ScriptName MyFirstScript Begin OnAdd if GetContainer == player ShowMessage MyMessage elseif GetContainer == MQDadRef ShowMessage MyDadMessage else ShowMessage MyOtherMessage endif End
This code will work in exactly the same way as the previous code, except it is much easier to read, as you can see, and you only have to include one "endif" statement because you are only using one "if" statement. Always remember - "elseif" and "else" statements don't need their own "endif" statements, only "if" statements need their own "endif" statements.
One of the most common general questions that I see asked about the scripting language used in Fallout 3 is "can we use switch/case?". If you haven't done programming before, then this won't really mean anything to you. "Switch/case" statements can be used in certain situations instead of "if" statements. While they do not provide any extra functionality whatsoever, they can make code much easier to read. The answer to this question is no, "switch/case" statements cannot be used in Fallout 3 scripts, so we'll just have to stick with "if" and "elseif" statements.
Variables[edit | edit source]
Often, you'll find that you need to somehow store information in a script. For this purpose, we have three types of variables available for us to use. These three variable types, including their aliases are:
- int / short / long
- float
- ref / reference
Each of these variables types can be used to store a specific type of value. Basically, "int" variables store integer values (whole numbers, which can be positive or negative), "float" variables store floating point values (numbers with decimal places, which can also be positive or negative), and "ref" variables store FormIDs (they are most commonly used to store RefIDs, hence the name).
In order to create a variable for us to use, we have to declare it. Just like how we had to use the "ScriptName" keyword when we declared the EditorID of our script, when we declare a variable we have to use an appropriate keyword to determine what type of variable it is. We can choose the name of our variable, but no two variables in the same script can have the same name, even if they are of different types, and a variable can't share its name with any Form (for example, I couldn't give a variable the name "Lockpick", as this name is already used as the EditorID of the bobby pin Form). Let's declare a "ref" variable, so that we can store the return value of GetContainer for later use:
ScriptName MyScript ref rContainer Begin OnAdd ... End
As you can see, the declaration of our new variable is located at the top of the script, outside of our Begin/End block. All of your variable declarations must be placed here, just like the ScriptName declaration. Notice as well how I have named my variable - I have prefixed it with the letter "r" so that I know that it is a "ref" variable, and I have named it according to its function, which is to store the RefID of the scripted item's container. How you name your variables and what conventions you use is completely up to you, but it is important that they are named according to their function so that your script is easy to follow. If your variables are all named "Variable1", "Variable2" etc. then your script will probably be very difficult to understand.
Once we've declared our variable, it is automatically given a value of 0. In order to change the value of a variable, we must use a "set" command, which consists of the use of two keywords - "set" and "to". For example, if we want to set our "rContainer" variable to the return value of GetContainer, we would do it like this:
set rContainer to GetContainer
As you can see, the "set" command is initiated with the keyword "set", which is followed by the name of the variable, which is followed by the "to" command, which is followed by an expression. The value of the variable will then be set to the result of that expression.
Reference Functions[edit | edit source]
The first function that we used in this tutorial, ShowMessage, is what's known as a non-reference function, as it does not act on a specific reference. Most functions, however, are reference functions, and perform an action on a specific reference. Because reference functions act on a specific reference, when calling them, the reference on which they should act must be specified. There are two ways in which a reference function may be called - with implicit reference syntax and with explicit reference syntax.
- When a reference function is called with implicit reference syntax, they are called on the scripted reference. Because of this, reference functions can only be called with implicit syntax in reference scripts. Reference functions called with implicit reference syntax use the exact same syntax as non-reference functions, and can be called on inventory items as well as references.
- When a reference function is called with explicit reference syntax, they are called on a specified reference. This reference can be specified in two ways:
- Via EditorRefID. Only persistent references can be referred to in this way.
- Via a local "ref" variable storing the reference's RefID.
- Reference functions called with explicit reference syntax specify a reference by prefixing the function name with either the reference's EditorRefID, or the name of a "ref" variable, and separating this from the function with a period - just like the one at the end of this sentence.
It is important to note that functions cannot be used directly to call reference functions with explicit reference syntax, nor can they be used directly as parameters in other functions. Instead, the return value of a function must be stored in a variable, and that variable should then be used when calling the function. For example, the following code will not compile:
GetContainer.AddItem Caps001 10
Instead, the return value of GetContainer must be stored in a local "ref" variable, and that local variable should be used to call GetContainer:
ref rContainer ; In the code block set rContainer to GetContainer rContainer.AddItem Caps001 10
In our script, we have already called a reference function - GetContainer. Because GetContainer only really works on inventory items, it should always be called with implicit reference syntax, just like we have done.
Anyway, where were we? Ah, that's right, we'd just declared a "ref" variable, called rContainer, and had set it to the return value of GetContainer. Now that we have the RefID of the reference that has the scripted bobby pin in its inventory stored in a "ref" variable, we can call reference functions on it with explicit reference syntax. Now, what if we wanted to change our script so that if a bobby pin is added to the inventory of a reference other than the player and MQDadRef, 10 caps are added to that reference's inventory instead of showing a message? We could do this by using explicit reference syntax to call AddItem on the reference, like so:
ScriptName MyFirstScript ref rContainer Begin OnAdd set rContainer to GetContainer if rContainer == player ShowMessage MyMessage elseif rContainer == MQDadRef ShowMessage MyDadMessage else rContainer.AddItem Caps001 10 endif End
One more thing that I should bring up here is that, when comparing RefIDs (both RefIDs stored in "ref" variables and those represented by EditorRefIDs), the GetIsReference function should be used instead of using the "==" operator, so our script should look like this:
ScriptName MyFirstScript ref rContainer Begin OnAdd set rContainer to GetContainer if rContainer.GetIsReference player ShowMessage MyMessage elseif rContainer.GetIsReference MQDadRef ShowMessage MyDadMessage else rContainer.AddItem Caps001 10 endif End
As you can see, GetIsReference is called by the reference that has its RefID stored in our rContainer variable, and uses the refID that it is being compared against as a parameter. For this function, we could swap these two around (e.g. "player.GetIsReference rContainer) and it would work just fine.
Comments[edit | edit source]
As your scripts become longer and more complex, they may become difficult to follow. In order to help make scripts easy to understand, it is important to annotate them using comments. Comments are basically text that is part of the script's source code, but is completely ignored by the compiler - it has no effect whatsoever on how a script works.
Comments in Fallout 3 scripts are specified by the use of a semicolon (;) - all text on a line that comes after a semicolon is a comment, and will be ignored by the compiler. This type of comment is known as a line comment, as the comment extends from the specifying character to the end of the line. Many other computer languages also allow a type of comment known as a block comment, where another specifying character or string is used to signify the end of a comment, but Fallout 3 scripts do not allow this.
One particularly useful way to utilise comments is to explain what a certain value represents. For example, the function GetOpenState uses different values to represent different "open states" of a door. On their own, they do not particularly make sense, so it is usually a good idea to use a comment to explain the meaning of the value. For example:
if GetOpenState == 3 ; Closed ... elseif GetOpenState == 4 ; Closing ... endif
As you can see, the comments here help to explain the true meaning of the conditions. If the comments were absent, then someone reading the script would need to look up the documentation of GetOpenState in order to understand what the conditions really mean. The same concept applies to variables - if you use a variable in which different values represent different states, it is a good idea to use comments when you declare the variable in order to explain what each value represents.
Another good way to use comments is to describe the function of a script. For example, if you have a script that uses some complicated calculations to place a reference in front of the player, then it would be a good idea to explain this in a comment at the top of the thread. That way, anyone reading the thread will only have to read this comment, instead of trying to understand your calculations in order to determine how they work.
Whenever you write scripts, you should always annotate them wherever you think it is necessary. If you think that any part of your script would benefit from a little explanation, don't hesitate to add a comment nearby. Even if it's only one or two words - a little clarification can go a long way, and scripts that don't contain any comments can sometimes take a long time to fully understand.
Style[edit | edit source]
Style is often thought of as the least important aspect of scripting. Experienced programmers, however, generally consider it more important than anything else.
Odds are, you'll read scripts written by others, to learn how to do things. And there will come a point when you say to yourself "hey, this script is really well written!"
Well written. Not necessarily full of clever or flashy tricks, nor a deep understanding of the engine, but rather a real pleasure to read through, and easy to understand. This is good style.
Defining good style can be hard, and everyone has their own opinion on it. But there are some traits which are widely recognised to help, even though in many cases the traits conflict with each other. The art lies in striking the right balance, and the more you write, the more confident you will be, at knowing the right balance for you.
Readability
Two facets of this have already been mentioned: choosing clear names for variables, and commenting well. But there are many others, such as using the full names of commands rather than their abbreviations, so that your readers don't have to try to remember acronyms; or using extra spaces to make a line clearer, or to show the similarities between two lines. At the ultimate level of readability, you could pass your code to someone who does not know any scripting, and they could still understand what it did, and how. Imagine, perhaps, your grandmother reading your scripts.
Consistency
Closely related to readability, this says that things with a similar purpose shouldn't be unnecessarily different. Avoid solving a problem in different ways in different places, and try to name similar things in similar ways. For example, in the default kits, doorway pieces can be named "Doorway", "DoorWay", "DoorFrame", "Door", "Exit", or "Ex". Does this variety make it any easier to find the piece you need?
Layout
This is what the script looks like on the page: a good layout lets you understand the structure of the code before you even read any of the words. Separate any conceptually-different sections of code with a blank line, and preferably a comment to summarise the following section. Indent the contents of each block by one level; also indent the contents of each if, else, and other "program flow" commands. Most text editing programs, including the GECK's, make this easier by letting you select a group of lines that you need to indent, then hit tab to indent them (or shift-tab, to unindent).
Coherence
Scripts can easily get fragmented between multiple script objects, conditions in quests and conversations, object actions, and so forth. The greater the number of things affected, the harder it becomes to keep track of it all, and to build a mental picture of their inter-relationships, so that you can make a change without having unintended consequences. If the affected things are kept to a minimum, grouped by similar names, and the relationships are highlighted with comments, then the tangled web becomes much more coherent.
Simplicity
This is often mistaken for terseness. Certainly, if it makes the script easier to understand, then doing something in fewer lines is good. But if you can choose between doing something in one long, tangled line of equations, or three easy to understand lines, then go with the simpler, multiline option.
Compatibility
This trait is sometimes at odds with readability and simplicity. This is the art of making scripts that do not break other mods. As a good start, strive to avoid naming conflicts, and to change as few existing parts of the game as you absolutely need, even if it means extra work for you.
Glossary[edit | edit source]
Alias
Many functions and keywords have "aliases". These are alternate names or keywords that can be used in the place of the main function name or keyword. For example, the function StopCombatAlarmOnActor has an alias - scaonactor. This means that the following two lines of code are essentially identical:
StopCombatAlarmOnActor scaonactor
Blocktype
There are a limited number of conditions that can be used for Begin/End blocks, such as OnAdd and OnDeath. Each of these conditions is known as a blocktype. Different types of scripts have different blocktypes available to them, so it can help to think of blocktypes as being split into several categories:
- Reference-specific
- All of these blocktypes are summarized on the Begin page.
As you can see, the vast majority of blocktypes are reference-specific. Most of these blocktypes can only be used in certain types of forms. For example, OnDeath blocks can only be used on actors (NPCs and creatures), and OnAdd blocks can only be used on carryable forms, such as weapons.
EditorID
All non-reference forms, as well as certain references, have EditorIDs. These are basically aliases for their FormIDs, and are used in scripts to refer to specific forms.
EditorRefID
The EditorID of a reference.
Effect Script
Effect scripts can be attached to base effect forms, which can be used in object effect, actor effect and ingestible forms. Effect scripts are reference scripts, although they are run on the target of the effect as opposed to the scripted form, which would be the effect itself. Because of this, variables declared in effect scripts cannot be accessed remotely. Effect scripts are limited to three usable blocktypes:
False
Anything that evaluates to a value of zero.
Form
Pretty much every object stored in a data file is a type of form. There are many different types of form, each of which stores information about a different type of object. For example, Script forms store information about scripts, whereas Weapon forms store information about weapons. Different forms can be identified by their unique EditorID and FormID.
FormID
A six-digit hexadecimal number that specifies a form. The first two digits of a FormID correspond with the position of the data file from which the form originates in your load order. This allows for up to 255 data files to be loaded simultaneously, including Fallout3.esm. The "ff" prefix is reserved for forms that are stored in the save file (most of these will be references.)
Function
Aside from the manipulation of variables, everything in scripting is done by functions. Certain functions can be used to determine information about specific forms or the current game state, and others can be used to perform actions - affecting specific forms or the state of the game in some way. Functions which act on specific references are known as reference functions, whereas functions that do not act on specific forms are known as non-reference functions.
Object Script
Object scripts can be attached to forms that can have references, such as actors, as well as carryable forms, such as weapons. They are reference scripts, and can use reference-specific blocktypes. Object scripts are executed every frame in which the scripted object (or its container, in the case of inventory objects) is loaded.
Operator
Operators are used to manipulate values, and are split into three categories:
Arithmetic operators
+ Addition - Subtraction * Multiplication / Division % Modulo (The remainder of a division of one number by another.)
Comparison operators
== Equal to != Not equal to > Greater than < less than >= Greater than or equal to <= Less than or equal to
Logical operators
&& AND || OR (Shift backslash, normally above the Enter key)
The comparison operators will return 1 if they evaluate to true, and 0 if they evaluate to false. For example, "3 < 5" will return 1 because the statement "3 is less than 5" is true, whereas "3 == 5" will return 0 because the statement "3 is equal to 5" is false. These operators cannot return any values other than 1 and 0.
Like the comparison operators, the logical operators will also return 1 if they evaluate to true, and 0 if they evaluate to false. Here is a truth table that show the possible outputs of the logical operators:
A B A&&B A||B false false false false false true false true true false false true true true true true
Although the NOT and XOR logical operators are not available, you can still achieve the same output by using "A == 0" in the place of "NOT A" and "(A||B)-(A&&B)" in the place of "A XOR B".
Keep in mind that when using complicated expressions it is helpful to include parentheses so that you and anyone reading your code can be certain of what order your operators are evaluated in.
Parameter
Many functions, especially those that perform actions rather than gather information, require more information in order to work correctly. This information is passed to functions in the form of parameters, which are separated from the function name and each other by white space, and/or an optional comma. For example, the following two lines are equivalent:
player.AddItem Caps001 10 player.AddItem,Caps001,10
Some functions have optional parameters, which can be omitted from the function call. If they are not included in the function call, then they will be given a default value. For example, the following two lines are equivalent:
player.AddItem Caps001 10 0 player.AddItem Caps001 10
Player
Several forms are hard-coded into the game. Most of these needn't concern you, but a very important and useful one is the "player" reference. Basically, the fact that this reference is hard-coded means that you will always be able to refer to the "player" reference by the keyword "player", which is the EditorRefID of the "player" reference, no matter which data files are or aren't loaded.
Quest Script
Quest scripts can be attached to quest forms, and are not reference scripts. They cannot use reference-specific blocktypes. Quest scripts have the unique ability to have a script delay time, which defines how often the script will be executed. This is defined in the quest form that the script is attached to, and can be changed via script with the SetQuestDelay function. In order for a quest script to run every frame, the script delay time should be set to a very low value like 0.001.
Reference
A reference is a special type of form. References are the forms that you can see and interact with in the game world, and basically acts as pointers to forms. As such, they contain two sets of information:
- Reference-specific information
- Information about the base object (the form that a reference is based on)
Reference Script
Scripts attached to forms that can have references are called reference scripts. Each reference to one of these forms will run its own instance of the script.
RefID
The FormID of a reference.
True
Anything that evaluated to a non-zero value.
Variable
A variable is something that allows you to store information in a script for later use. There are three types of variable available for use in Fallout 3 scripting, each which stores a different type of information.
External Links[edit | edit source]
- Scripting For Beginners on cipscis.com
- Scripting For Beginners (Spanish) (translated by Bauhaus)