Scripting Tutorial: My Second Script
This article is a Featured Article. This means that it has been found to be among the best articles on the Wiki, and that it may serve as an excellent example of what articles should strive for.
While the My First Script tutorial is a good first taste of scripting for Oblivion, it does not appreciably demonstrate what the scripting language in Oblivion can do. It is a wonderful introduction for those who have never seen a programming or scripting language before, but a more in-depth tutorial would be helpful to further introduce modders to this amazing resource.
This tutorial has been largely adapted from GhanBuriGhan's excellent Morrowind Scripting for Dummies; all credit goes to GhanBuriGhan for his fantastic work on the original.
This tutorial is meant to be a more complete introduction to scripting for Oblivion than the My First Script tutorial, and assumes that the reader is already familiar with My First Script. If you do not understand the main points of that tutorial, you may find yourself in over your head here. If you're comfortable with My First Script, though, let's begin scripting!
Tools used in this tutorial
Required Optional |
More information about scripting in Oblivion[edit | edit source]
What is a script?[edit | edit source]
Scripts are basically pieces of code written in a special scripting language (we will call it TES Script from here on). These little "programs" will run during the game and can perform certain things in the game, lots of things actually: scripts trigger events, control time and place, make things and creatures vanish, appear or move, give messages to the player, change stats, even change the weather—the possibilities are incredible.
TES Script is a unique scripting language, but it is not used outside the TES Construction Set. As a scripting language it has certain limitations compared to a "real" programming language, like C++:
- The scope of TES Script is limited; don’t expect to be able to program anything that is not already in the game in one way or another. This is not to say you cannot achieve new and unusual things with scripting! But you can’t use TES Script to, say, program a word processor.
- TES Script is also not an SDK (software development kit) that lets you actually work with and change (parts of) Oblivion's source code. That’s why you can’t use TES Script to, for example, add new weather effects. Those are handled elsewhere, and you would need a completely different kind of knowledge to do that.
- It’s an interpreted language, not a compiled one—the code needs a separate program (in this case Oblivion) to run—unlike compiled code that could be run by itself, like an .exe application.
TES Script is not case sensitive. This means that the command "player.getpos z" runs exactly the same as the command "Player.GetPos z", which runs the same as "PlAyEr.GeTpOs Z", which runs the same as any of the remaining increasingly illegible options. Many people (the author included) use the second format because it is the most easily understandable; many others prefer the first format because it's easier to use only lowercase. The only time where case matters is when you want to print a message to the screen: even there, proper case is a matter of good use of English rather than good TES Script syntax.
What can scripts do?[edit | edit source]
Scripts for Oblivion are a way to have the game dynamically react to what the player does in the game world. You can use scripts to manage complex quests. You can use scripts to create custom items that perform actions beyond what regular enchantments could. You can use scripts to create traps. You can use scripts to change NPC or creature behavior. Remember the character creation in Oblivion? It’s basically controlled by a number of scripts. Done any quests? Those are handled by scripts, too. So the short answer to the question is: a lot.
What scripts can't do[edit | edit source]
The TES Script language is limited in its capabilities—there are only so many functions that you can use and sometimes the possible uses are not all that you may wish them to be. In many cases smart scripters can find workarounds for apparent limitations, but don't expect miracles. Many things are hardcoded and cannot or only indirectly be influenced by scripts.
Scripting tutorial: before we code[edit | edit source]
If you are completely new to scripting and programming in general, and even if you've already done the My First Script tutorial, the thought of using TES Script might be a little daunting. That's why there's this extended tutorial that will walk you through making another, more involved, script. As you progress, you'll also see explanations about the main elements of the scripting language. There will be other explanations on the way, but the key instructions and important information will be in bold print.
Let's start![edit | edit source]
We start by opening the script editor: Start up the TES Construction Set, open the Oblivion.esm file and then select Edit Scripts from the Gameplay menu to open the scripting window.
The scripting window[edit | edit source]
You enter the script editor either by selecting Gameplay –> Edit Scripts from the menu; by clicking the edit script button (the pencil) at the far right of the taskbar; or by accessing it from an Object or NPC dialogue, clicking the button with the ellipses […] next to the script field. The editor window is pretty basic, and doesn't look like much right now:
Let's have a look at the buttons in the taskbar, from left to right:
- Open lets you select a script to edit.
- Save error checks the current script and either compiles it or gives out error messages. Note, however, that the plugin and thus the script is not really saved to disk at this time. When programming large scripts you should frequently use the save command in the main TESCS window after you have saved the script here, just in case the TESCS crashes. Note also that if you edit the script and suddenly hit "save plugin" to backup in the middle of the work, your updated script will NOT be saved with it. You must save it manually first. Also, if you simply close the script window, it doesn’t mean that script will be saved. You must take care of it yourself.
Side Note: You can also write your code in external editors and paste it into the CS. This can make it easier to keep scripts saved, and can be more convenient, since you don't have to load the CS and your mod. Options include S!lk, emacs, and Notepad++, all of which have syntax highlighting plugins for Oblivion script (only Notepad++ has up-to-date OBSE functions included, however).
- Forward and Backward arrows jump to the next or previous script, respectively (in alphabetical order). If you give your scripts a common tag, that will make it easier to jump between the different scripts of your project. For example, say a modder's pseudonym is Grundulum, and he starts every script name with "GR_ShortReferencetoProject_", with that second part being a two or three letter reference to the current mod; this keeps all of the scripts you're working on neatly together.
- Compile all recompiles all scripts in all loaded files. Do not ever press this button. It also adds every script in Oblivion to your mod, so you'll wind up with a 2MB esp file that conflicts with everything.
- Finally, the Delete button deletes a script, and
- the last Arrow down button closes the script window.
At the far right of your toolbar, you'll see a dropdown box called Script Type. This box allows you to choose one of three kinds that your script will fall under: Object, Quest, and Magic Effect. More on these later, but by way of brief introduction Object scripts are attached to objects in the game world (such as items or NPCs), Quest scripts control the flow of quests (such as character generation), and Magic Effect scripts control a very special magic effect (specifically, the Script Effect).
The help menu gives quick access to the function and command pages of the wiki (of only moderate utility for beginners, hence the creation of this tutorial!). You can cut, copy, and paste from and into the editor window by using the Windows-standard ctrl-c for copy, ctrl-x for cut and ctrl-v for paste.
What do we want?[edit | edit source]
Before we really start writing our tutorial script we should decide what we want it to do! For this tutorial we are going to make a Riddle Cupboard: The cupboard will ask a riddle and only the right answer will open the cupboard. If the player provides the wrong answer, a trap will go off, hurting the player, and the cupboard can't be opened. That’s a fairly complex undertaking, but we will take it step by step and see that it's not so bad after all.
Writing the script[edit | edit source]
Once you have the Script Edit window open, click Script -> New.... You should see that the Script Type is "Object", which is exactly what we want it to be. Click in the main part of the window, which should now have gone from grayed out to white. This is where you'll be writing the script.
Scripting tutorial: the first lines[edit | edit source]
Naming a script[edit | edit source]
First of all we must give our script a name. Every script must start with a declaration of that script's name. In the editing field, please type
ScriptName RiddleChestScript
Note that there are no spaces and no underscores in the name. Your script name must be one word, so you may not use spaces. Also, Oblivion ignores underscores in the alphabetical listing of scripts; you may use underscores to make the title clearer for yourself, but you will not see them when you look to open the script later. (Advanced scripting information: Oblivion allows something called aliasing in TES Script. This is where you replace the full name of the command with a shorter version. For instance, "ScriptName" becomes "scn", or "ForceActorValue" becomes "ForceAV". We will not be using aliasing in this tutorial, but don't be surprised if you see other scripts that do use it.) Remember that TES Script is not case sensitive; you could have written "scriptname" or "ScriptnamE" just as well.
Try to save your script using the Save button in the toolbar. Nothing obvious should happen, but if you click on the Open button, you should see your script name in the list of scripts in Oblivion. Click on the X at the top right corner of the Select Form window to close it and return to the Script Edit window. RiddleChestScript is now the shortest script possible in Oblivion, but it doesn't do anything right now. Let's change that.
Begin and End[edit | edit source]
The Begin and End commands define, somewhat obviously, the beginning and end of blocks of the script—separate chunks of code that will be performed under certain conditions. For example, "Begin GameMode" defines the start of a block of text that will run every frame the user is not looking at a menu. For our purposes, we want an OnActivate block: one that will run when our chest is activated, but no other times. (It would get very annoying having to answer the riddle every frame, or every time an NPC died, both of which are possible using other variants of Begin!) So hit enter twice to move the cursor down, and edit your script to look like this:
ScriptName RiddleChestScript Begin OnActivate ; Here we will enter what happens when the chest is opened. End
Click Save again now. The script still doesn't do anything, but at least now we have defined under what conditions it will run. There are a couple of things to note here. First, we have ended the current block before beginning a new one—not so important when there's only one block, but as this script gets more complicated we'll need multiple blocks in a single script. In somewhat more technical terms, Begin/End blocks do not nest. The other thing to notice is the semicolon ";" on line 4. A semicolon marks the rest of the line as comment.
Code Comments[edit | edit source]
Comments can be placed anywhere in-code with the semicolon character - ; - Whatever you type after the semicolon on the same line will be ignored when the script compiles. If you want more than one line of comment, you must have one semicolon for each line.
You use comments inside your scripts to generally explain what you are trying to accomplish in your code, which can help you and others understand what's going on inside the script quickly at a later date. Comments can also serve as a debugging technique by commenting out lines of code one-by-one to pinpoint the source of some odd or unintended effect in-game.
Note: Because everything appearing on a line after the semicolon is not treated as compilable code, one issue you might face is that you cannot simply put semicolons in strings. If you need to have a semicolon as part of a string - for example a message in-game - you could instead use the OBSE "%a" Format Specifier for functions that support this and passing in ASCII character code 59, or use the AsciiToChar OBSE function.
Writing text and obtaining decisions from the player[edit | edit source]
Now we want our trapped chest to ask the player a riddle. For this we use the MessageBox function, which should look familiar: the My First Script tutorial used a close relative of MessageBox, the Message command. MessageBox allows us to display some text on the screen and also to display choices that the player can select from. Unfortunately Oblivion (like Morrowind before it) has no option to have the player type in the answer to our riddle, so we will have to give multiple choices. The line for that could read:
MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
The first string (the text between the first pair of quotation marks) is the text actually displayed in the box; the other texts, separated by commas, tell the game to make "buttons" with the given text displayed one string per choice.
But how do we ensure that the riddle is asked only the first time we try to open the chest and not every time? We now come to a very central point: the use of do-once conditions and state variables. Most of the problems that beginners encounter with scripting for Oblivion have their roots in misunderstanding how the scripts are actually executed and how scripts should accordingly be structured. So let's have a look at this in greater detail. Remember to save regularly.
How Object scripts are executed[edit | edit source]
Every script that is attached to an object or an NPC (Object script) is executed every frame the game displays on screen while the cell with the object is active (when indoors only the cell the PC is currently in is active, when outdoors the PC’s cell and all adjacent cells are active). So the complete script (not just one line of it) is executed 10-60 times a second or however fast your computer runs the game! It is best to imagine every local script wrapped in a big "whileloop":
while (Object is in active Cell) [Your script code] endwhile
This is the reason why the following script spits out a continuous stream of messages (if attached to an object or NPC in the same cell as the player). Try it, if you want:
ScriptName HorribleMessageScript Begin GameMode MessageBox “Thousands of useless messages!” End
This example is relatively harmless, but imagine what happens if you had used a line of code that adds an item to the player's inventory, or places a monster next to him, etc.!
For this reason, “Do Once” constructions are very essential and something you will probably use a lot while scripting for Oblivion. So, let's go on with our tutorial script: we need to declare a variable and use it to make sure the message is only displayed once. Change the script to the following:
ScriptName RiddleChestScript Short controlvar Begin OnActivate If ( controlvar == 0 ) MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith" Set controlvar to 1 EndIf End
and save the script again. Note that the MessageBox command should be on one line in your script! Do not break it up!
Once again, there are a few things to point out in this latest addition. The "If" command is there to check a condition—whenever the expression in the parantheses is true the following lines of code will be executed until the "EndIf" command is encountered. The "==" checks if an expression on the left of it (in our case the variable controlvar) is equal to the expression on the right of it (in our case to 0). If you forget the EndIf command after an If command, the editor will complain with an error message when you try to save.
"Short controlvar" declares a new variable we'll call "controlvar", of type short. For the moment it's enough to know that this is variable that will contain integers (whole positive or negative numbers). A variable is a "placeholder" that can take on different values. The Set command is also new, but simple enough—it sets our variable that had the value 0 before (all variables start out at zero when declared) to 1. This, in connection with the "If ( controlvar == 0 )" command, provides a do-once condition—the next frame the script is executed after the variable was set to 1, the If condition will be false and the message box will not be displayed again.
Scripting tutorial: the first test[edit | edit source]
Saving and preparing the mod[edit | edit source]
Now our script is already capable of being run, so lets test it:
- Save the script and close the script editor window.
- Look to the Object Window, and proceed in order through the directories of WorldObjects > Container > Clutter until you find the object with the Editor ID of CupboardFoodLower.
- Now either right-click and choose Edit or double-click the object to bring up its properties.
- In the script dropdown menu, select the script you just made RiddleChestScript.
- Hit OK, save the mod, and quit TESCS.
NOTE: PAY ATTENTION TO WHAT WE HAVE JUST DONE. EDITING AN OBJECT WITHOUT GIVING IT A NEW ID IS TERRIBLE MODDING PRACTICE. NEVER DO THIS UNLESS YOU ARE SURE YOU KNOW WHAT YOU ARE DOING!
The script in-game[edit | edit source]
- Now use Data Files to activate your mod, start Oblivion, and load a savegame.
- When your game has loaded, bring down the console (usually the ~ key, or whatever you have to the left of the "1" on the main keyboard) and in the console type:
player.coc "AleswellInn"
and hit Enter.
- You will appear in the Aleswell Inn, and be immediately greeted by a group of invisible people. This has nothing to do with the mod, but relates instead to the author's random choice to use this cell to test the mod (it's near the top of the list of interiors, so is very easy to find). Click through until you can move, and approach the cupboard on the wall to your left.
When you activate the cupboard, you should see your messagebox appear on the screen, like the picture at left:
Clicking on any of the choices should close the messagebox for the moment, and nothing should happen when you activate the cupboard again—which is good, because it means our controlvar check is working like we wanted. (The cupboard is not activating because we have not included an Activate command anywhere in our script, but we'll come to that later.)
Quit Oblivion and reload your mod in TESCS. Those poor people in Aleswell Inn will have to wait till some other time to regain their visibility, but such is life being an NPC in a video game.
Scripting tutorial: player choice, bugs, and fixes[edit | edit source]
Letting the player choose an answer[edit | edit source]
We now need to figure out which answer the player selects, and script appropriate reactions for right and wrong answers. The function to test the selected answer is GetButtonPressed. This function returns a number depending on which of the buttons of a message box has been clicked on with the mouse. It will return "0" for the first button ("Bat" in our example) and 1, 2, 3, etc. for the following buttons, in the order you listed them in the MessageBox command. Until an answer has been selected, the function will return –1, so we have to take care of that, too.
The "Activate" function will make our cupboard open; in fact, Activate will simply trigger the standard action that would usually be performed when you "use" the scripted object—doors will swing open, NPCs will initiate dialogue, etc. The following update to our script also demonstrates how you can use a control variable to force Oblivion to process functions one after the other, although the complete script is processed every frame of the game: simply increment the control variable and test it in a series of If–ElseIf statements. This is a very safe way of scripting for Oblivion. It may not always be necessary, but it's safe.
Please edit the script to look like the following:
ScriptName RiddleChestScript Short controlvar Short button Begin OnActivate If ( controlvar == 0 ) MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith" Set controlvar to 1 ElseIf ( controlvar > 1 ) Activate EndIf End Begin GameMode If ( controlvar == 1 ) Set button to GetButtonPressed If ( button == -1 ) Return ElseIf ( button == 2) MessageBox "Your answer was correct." Activate Set controlvar to 2 Else MessageBox "Your answer was wrong." Set controlvar to -1 EndIf EndIf End
Take a look at the new GameMode block that starts with "If (controlvar == 1)". We have set controlvar to 1 as soon as the cupboard got activated. But OnActivate only runs script for the frame the object was activated; we must use the GameMode block to continue checking for the player's choice, since that runs every frame. Now we test for which button is being pressed. We do this by assigning the new variable "button" a value returned by GetButtonPressed. Since the script is still running, even while the game seemingly pauses to await your decision, we first test if no button has been selected yet—return tells the game engine to stop processing the script for this frame.
Our correct answer was "Wind", which corresponds to button number two—if button number two gets pressed, we want to tell the player that he gave the right answer, and allow Activate to open the cupboard's inventory in the usual way. All other values of button mean that the player has selected a wrong answer, so we can use the Else command here. In this case we tell the player what a fool he was and the chest is not activated.
Now look at the little addition at the top of the script:
Begin OnActivate If ( controlvar == 0 ) MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith" Set controlvar to 1 ElseIf ( controlvar > 1 ) Activate EndIf End
This means that, whenever the cupboard is activated in the future, it will only open if controlvar is greater than 1. Remember the last paragraph: if the player provides the wrong answer to the riddle, controlvar is set to –1, so he will never be able to open the cupboard. But if he knows the right answer, controlvar is set to 2, and from now on the player can open the chest as often as he likes. 'Save and run your plugin, and test as described above.
Your first bugs, and the fixes[edit | edit source]
If you tested the plugin, you should already have noticed that things do not go quite as planned. Choosing incorrectly does lock the chest forever, but if the player chooses correctly the inventory and the correct-answer messagebox come up at the same time. The messagebox must be cleared before anything else may be done, but it's covered by the inventory!
Let's try the following (change the corresponding section of your script to look like this:
If ( controlvar == 1 ) Set button to GetButtonPressed If ( button == -1 ) Return ElseIf ( button == 2) MessageBox "Your answer was correct." Set controlvar to 2 Else MessageBox "Your answer was wrong." Set controlvar to -1 EndIf ElseIf ( controlvar == 2 ) Activate EndIf
See how we moved the activate command to the section that tests for controlvar == 2? This provides a cleaner sequence of events by preventing the two boxes from being open simultaneously, and as was mentioned above, clean sequences of events can be very important when scripting for Oblivion—always try to avoid doing too many things at once! Well, save, run and test it.
Great, now the inventory opens as we wanted, but what is this? We can't close the inventory! Look above:controlvar was set to two, and remains there since we do not change it again. Therefore the game now gets continuous "Activate" commands each time the script is processed (every frame)! That’s why we can't close the inventory—it gets reopened immediately. So change the following part of the script:
ElseIf ( controlvar == 2 ) Activate Set controlvar to 3 EndIf
Test the mod again: now everything works the way we wanted. Hopefully you are not confused from with the above excursion into the process of debugging, but it is a very important thing to know about—you will constantly have to rethink your scripts and try different ways of doing it to be successful.
What's missing now? The trap effect, of course!
Scripting tutorial: adding the trap[edit | edit source]
Our cupboard will put a curse on the player if he fails to answer the riddle. First, select the spell you want to hit the player with: click on the + sign next to Magic, the + sign next to Spell, and click to highlight Spell. There are quite a few choices here, but for the purposes of the tutorial we'll use the Mg05FingerSpell15 spell.
With our painful consequence chosen, we need to add it to the player when he chooses the wrong answer. Edit the script again:
Else MessageBox "Your answer was wrong." Cast Mg05FingerSpell15 Player Set controlvar to -1 EndIf
Note the use of the Cast function. (Advanced scripting information: Cast requires a calling object to work. We did not include one in our script because it is an Object script, and will be attached to an object in the game. The script will therefore default to the attached object when functions require a calling object.)
Now, your script should look like this:
ScriptName RiddleChestScript Short controlvar Short button Begin OnActivate If ( controlvar == 0 ) MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith" Set controlvar to 1 ElseIf ( controlvar > 1 ) Activate EndIf End Begin GameMode If ( controlvar == 1 ) Set button to GetButtonPressed If ( button == -1 ) Return ElseIf ( button == 2) MessageBox "Your answer was correct." Set controlvar to 2 Else MessageBox "Your answer was wrong." Cast Mg05FingerSpell15 Player Set controlvar to -1 EndIf ElseIf ( controlvar == 2 ) Activate Set controlvar to 3 EndIf End
Ok, now we have our final working script. Congratulations! If you want, experiment a little more with this script using some of the other functions in TES Script.
How to learn more[edit | edit source]
After this tutorial you may ask yourself how to continue learning how to script. A good way is to look at the example scripts in this guide or at scripts that are already in the game (either from Bethesda or from mods). Try to find a script that is similar to what you want to do and then copy the script and change it to fit your needs. Read the general information on the functions page and the descriptions of the functions you may need to do what you have planned. The categorization of the functions into function types should help you to find the right ones. Finally, the official forums are a great place to find information (use the search function) or to get help on a specific problem. The rest is practicing, practicing, practicing.
Last notes[edit | edit source]
Readers who paid attention to the If page will note that the parentheses around conditions are not necessary. I included them in this tutorial because I feel that they help to organize the script and make it easier to understand.
Finally, immense thanks go to GhanBuriGhan for the fantastic Morrowind Scripting for Dummies, which included the tutorial from which this one came. I was unable to contact him to request permission to use the tutorial; if he has any issues with it he is free to take this work down, or edit it until he is satisfied.