|Lesson 6 will focus on Loops.
These lessons have been constructed using Neverwinter Nights 1, but when the game comes out they will be revamped.
If you have any questions regarding the lessons then please post your question(s) on the forums.
It is with a bit of trepidation that I write and post this installment in my lesson sequence. Everything up to this point I consider to be "basic scripting." The techniques used in the past five lessons are going to end up being used in almost every script that you write. In this lesson, though, I'm going to talk about an intermediate concept. It is still a very important idea, it is just that you won't be using it nearly as often as the previous material.
That isn't the reason why I'm a bit hesitant though... the real reason why I worry is that this is the first lesson where what I am discussing is "dangerous." Certainly, it has been easy to make mistakes with some of the things we have talked about. If there has been a problem with a script, though, it is just that the script wouldn't work as intended. With loops, though, there is the potential to really screw things up.
Luckily, BioWare has some controls put in to keep things from getting too far out of hand. Still, you always need to be very careful to watch out for some of the problems that I'll be discussing.
What is a Loop?
In simplest terms, a loop is a way of getting part of your script to run multiple times. The engine goes through it once, then checks to see if it is supposed to run again. If so, it "loops" back to the start of the marked part and repeats itself. It does this over and over again, until things have changed to the point where it isn't supposed to happen any more.
The key in writing loops is then mainly to know when to use them, and how to set up the "check" to make certain the loop stops at the right time. The check is particularly important – this is the danger I aluded to earlier. If your check is written wrong, there is the potential to never "break out" of the loop, and the same instructions will happen over and over until an error occurs. [Since this tutorial was originally written, it's come to light that the engine has a built-in check for the 'cost of operation' of any script... if your script has too many commands or takes to long, the engine kills it, so there's no danger of getting an infinite loop this way. It is, however, still possible to crash the engine with infinite loops - an exercise for the reader - and very poor programming practice to have open-ended loops - ed. (Iskander)].
There are two types of loops: the "for loop" and the "while loop." The first question that should pop into your head is "How do I know when to use each type?" I wish there were an easy, 100% accurate rule that I could give. The fact is, though, that knowing what type of loop to use is something that comes with experience. In fact, everything that can be done with a for loop could also be done with a while loop, which compounds the confusion – why have two types, when one would suffice?
There is, however, a general rule of thumb that we can use. The rule isn't perfect, but it covers most uses. If you are going to do something a certain number of times, you want to use a for loop. If you don't have any way of knowing how many times you are going to do something, you'll use a while loop. Let's take a look at each type of loop separately.
The key to a for loop is that you have an "index" variable that you use to keep track of going through the loops.
The basic format is something like this:
for (nIndex = 1; nIndex <= 7; nIndex++)
This is a little bit complicated, so let's look at each part specifically.
First off, we have to define the index variable first. (C coders may be used to defining the variable inside the loop definition, but for NWNScript it has to be declared first.) We don't have to give it a starting value, but we have to set up the variable.
There are three things inside the parentheses. The first one (nIndex = 1) is the initialization clause; we've set up ('declared') the variable already, we just give it a starting value here ('initialise').
The second part (nIndex <= 7) is called the 'conditional'. This is the part that determines when the loop actually "runs." As long as this condition is met, the loop continues to run. It is checked each time the loop tries to start again, and as soon as it is no longer true, the loop "terminates." So if after some loop, nIndex had a value of 8, the loop would stop running.
The final part (nIndex++) is an action that the code does at the end of each loop. I briefly mentioned the '++' part in a previous lesson... it increments the variable nIndex by 1. We could just as easily put in this third slot something like nIndex = nIndex + 1. However, because incrementing by 1 is by far the most common "third clause" to the for statement, most experienced scripters prefer to use the '++'. It takes a bit of getting used to, but it really does make the code look cleaner.
Putting all this together, what happens with the loop up there? nIndex starts out as 1. 1 is less than or equal to 7, so it performs the do_things_here stuff. Then the first iteration of the loop is over, so that third clause kicks in, and nIndex is incremented to 2. It then goes back to the conditional... 2 is still less than or equal to 7, so the do_things_here runs again. The second iteration being over, nIndex is bumped up to 3. It goes through this a few more times, nIndex going through 4, 5, 6, 7 and then ending up as 8. At this point, when it checks, nIndex is now greater than 7, so the loop terminates.
Example For Loop
All this is pretty confusing stuff, so let's take a look at a concrete example. Let's say we want to punish a character for hacking open chests at random. So, we'll put in a chest that will summon 5 zombies and 5 skeletons around the character when it is destroyed. We could call the CreateObject command 10 different times, but it makes a lot more sense to use a loop instead. (Note: I'm assuming that you have a PC available for testing that can survive that many zombies and skeletons. If you don't, you may want to change the script so it summons chickens and commoners instead, or create a PC that can survive .)
1. Place a chest into your module.
2. On the "Lock" tab, set it to locked, and the difficulty to 100.
3. On the scripts tab, the "OnDeath" handle, put the following script:
// On Death Script: tm_chest_dt
// This script summons 5 zombies and 5 skeletons
// near the person that destroyed the chest.
// Written by Celowin
// Last Updated: 7/13/02
// Initialization: Get the location of the PC that destroyed the chest,
// Set up for the loop.
object oPC = GetLastKiller();
location lSpawn = GetLocation(oPC);
// Loop 5 times
for (nUndeadIndex = 1; nUndeadIndex <= 5; nUndeadIndex++)
// Each loop, create a zombie and a skeleton at the PC's location
CreateObject(OBJECT_TYPE_CREATURE, "nw_zombie01", lSpawn, TRUE);
CreateObject(OBJECT_TYPE_CREATURE, "nw_skeleton", lSpawn, TRUE);
Go ahead and test it out. Destroy the chest, and suddenly your character is surrounded by an undead horde. Easy enough?.
Another thing to note is that in any of the three parts of the for loop initialization, we aren't restricted to just the one variable. Usually the only place you'll put something else in is the second clause, but if desired you could alter the first or third as well.
Let's just modify that last script a tiny bit. Instead of summoning 5 each of skeletons and zombies, let's only summon zombies... but we'll summon a number equal to the level of the PC. I won't write out the entire script, since most of it is the same. Here is what we need to change:
1. Under the line that starts "object oPC", add the line: int nPCLevel = GetHitDice( oPC );
2. You may want to break that up onto separate lines. As long as you only have the semicolon at the end, you're fine.
3. Then, in the loop setup, the second part becomes nUndeadIndex <= nPCLevel;
4. Comment out the summon skeleton line, update the comments if you wish, and you're set to go.
Test it out, and you should be set. Check it out with a couple of different characters, and see how the number of zombies changes.
Wrap up on For Loops
Really, then, the key to using for loops is understanding the three parts. The initialization is run once, at the start of the process. The conditional is checked at the start of each loop (including the first), if it is true then the loop cycles, if not then the loop terminates. The third part is done at the end of each loop.
I can't stress enough that you have to be certain that your loop will end! What would happen if we had set our conditional to nUndeadIndex > 0? No matter how many times would increment the variable, this would always be true... so the script would summon zombie after zombie until an error occurred. It might be fun to see how many zombies it could make, but such errors should be avoided at all costs. So, always, always double check your logic when you're making a loop. Make certain that your loop will in fact terminate at some point.
On to While Loops
While loops are in many ways simpler to understand. However, I think they are harder to use, since there is less certainty about them. I mentioned before that you usually use a while loop when you don't know how many times you'll have to run the loop. That is a little bit confusing, so let me try to explain... suppose you have a bunch of hellhounds running around in an area. You want it so that when a certain altar is destroyed, all the hellhounds will suddenly die. The problem is, you can't be certain how many hellhounds there are, the PCs may have killed a few. For that matter, maybe the altar will keep summoning hellhounds until it is destroyed itself, so there could be lots around.
So, we really have no idea how many times we'll have to do a "kill hellhound" command. It might be none, it might be 50. There is just no way to tell up front. That is where the while loop comes in. Basically, we want to say "keep killing hellhounds until none are left", or in other words "while there is a hellhound still around, keep killing them one after the other."
The basic format for a while loop is like this:
It is really fairly straightforward. When the script gets to the while loop, it checks the condition. If it is true, it does the inside stuff, then checks the condition again. If it is still true, it repeats. It keeps doing this until the condition isn't true any more. Simple enough, right? Really, in a lot of ways, it is like the for loop. In fact, you really have the same three parts... it is just that the initialization and the update you have to be sure to put in yourself.
We'll get to the hellhounds in a minute, first let's attack something a little bit simpler. It is a common thing when completing a quest to give the PCs a little bit of experience for their trouble. If you only have one PC, it is a piece of cake. But what if you are running a multiplayer game? How do you make sure each PC gets their reward?
If you didn't answer "with a while loop" give yourself twenty whacks with a wet noodle.
We have a few things floating around our module, if you've been following the lessons all along. (If you haven't, just make sure you have a ring with tag PASSRING somewhere, and any NPC.) Pick one of the NPCs we have floating around, and we'll give it a simple little conversation: (I'm going to assume you know how to use the conversation editor to do this.)
(Start) Got ring?
|-(PC Response 1) No.
| |- End dialogue.
|-(PC Response 2) Yes.
|- (NPC Response) Thanks.
(I'd actually make these a bit more verbose than this, but I will leave that to your own personal writing skills.)
For PC Response 2, just use the Script Wizard on the "Appears When" tab. Say that you want it to happen when "Item is in Inventory" and then add the tag PASSRING to the list.
For the NPC Response, 'Thanks', on the "Actions Taken" tab, put this script:
// Conversation Script: tm_guard_c1
// Takes the ring from the speaking player,
// And awards 50 XP to every PC.
object oPC = GetFirstPC();
while (oPC != OBJECT_INVALID)
oPC = GetNextPC();
Save the script, save the conversation, save the module, then go test it out. You'll have to have a friend help you out to really test it properly, but you can see the basics just by running it yourself.
Notice that ignoring the DestroyObject command, really what we have it just the basic steps of the for loop. We initialize ( oPC = GetFirstPC() ), we condition ( oPC != OBJECT_INVALID ) and we update our variable ( oPC = GetNextPC() ).
So, this will keep looping, giving 50 XP to each PC, until we come up with OBJECT_INVALID, that is, until there are no more PCs.
Once again, when writing a while loop, be absolutely certain that the loop will eventually terminate. Infinite loops are bad.
Hmmmm.... I was going to go through and do the hellhound script, but the more I think about it, the less I think I have to. In fact, basically every part of the script has been used somewhere in this lesson.... What handle to use, what commands go in the script, everything. So, I'll leave that to you... drop an altar and some hellhounds into your module, and see if you can make it so that destroying the altar will destroy all the hellhounds. (Difficulty = moderate. You can also make it so that the altar will spawn hellhounds until it is destroyed.... Difficulty remains moderate if you just keep spawning them, difficulty ups to advanced if you make sure that the number of hellhounds never gets above a certain level.)
Another exercise to try: Make a trigger, that when entered will summon a number of creatures (whatever kind you like) equal to twice the number of PCs in the module. (I'll rate this one as advanced, but barely so.)