Celowin - Part III: Conditionals
Preamble
The purpose of this sequence of lessons is to take a complete beginner to programming, and teach him or her how to use NWScript to write modules. The early lessons will be very basic, and anyone that has done any coding at all will be able to skip over them. The goal here is to make the lessons so that even the people that just shudder at any type of code can learn.
Feel free to post these lessons on any forum, print them out, or modify them. However, just give me credit for doing them. Any comments on these lessons, good or bad, can be sent to me, Celowin
I am going to assume that anyone looking at these lessons has at least played around with the Aurora Toolset a bit. If there is enough feedback that people don't know how to do the simple placements that I have in these lessons, I will consider spelling out in more detail what needs to be done.
Introduction
This lesson is going to cover what is probably the most important part of scripting... how to make your code do something only some of the time – that is, only when some condition is met. At a guess, on looking through the forum, 90% or so of the questions come down in one way or another to dealing with this.
I've already previewed this idea at the end of Lesson 2, but I didn't give any explanation. The script I really want to showcase in this lesson is fairly complicated, so I'm going to go ahead and talk a bit of theory first.
The basic format for a conditional, or "if statement" is like this:
if ( condition ) { do_this; do_something_else; do_another_thing; }
(The above is not a true script, just something to give the general idea. This sort of example script with bogus functions is commonly referred to as 'pseudo-code')
Basically, if the 'condition' is met, the script will do the things between the '{' and the '}'. If the condition isn't true, it does nothing. (Note on the format: It is a common beginner's error to put a semicolon after the line starting with 'if'. There isn't supposed to be one there.)
That is really all there is to it... the tricky part is figuring out exactly what a "condition" is. There are many, many types of conditions we can put in here, with lots of little rules affecting how they work. I am certain that this lesson will not be able to cover every single rule that can be put here. I doubt that I myself know all of the functions that we could call. I can only hope to get you to the point where you know enough to understand the basics, so that you can look at other people's scripts and learn from them.
Most conditions come down to a comparison. For example, in the script I put at the end of Lesson 2, the relevant line was
if (nCount == 1)
Notice the '==' is in fact two equals signs. As we discussed in lesson 2, a single equals sign is used to "set" a variable (assignment). The double equals sign is more asking the question "are the two sides the same?" (comparison).
Just looking at the statement, the sides look very different. One side has a bunch of text, the other side is a number. How can there be any question that they are the same?
Well, remember that 'nCount' is a variable and is actually equal to a number. So, it all depends on the value stored in 'nCount'. If 'nCount' is storing the number 1, then the condition will be true, and the associated actions are performed. If 'nCount' is storing the number 7, then the condition will be false, and this statement will not do anything.
The other major way to test a condition is through a predefined function. We'll explore that in this next example.
Example Script
Let's build a script with a simple conditional. There are going to be a number of new functions introduced in it, I'll explain them afterward. We're going to need to do some setup for this script to work.
- Open up the Test Module we've been working with in the Toolset.
- Create a new area, forest tileset, dimensions 4 by 2. Call it 'Test Area 002'.
- At one end, paint the module start location.
- At the other end, paint an NPC. Just make it a commoner, for simplicity.
- Change the tag of the NPC to GUARD
- Go to the scripts tab of the NPC, delete all the scripts. (Next lesson, I hope, we can get rid of this step. It irks me to have to use NPCs that don't react.)
- Go to the OnPerception event handle, and input the following script:
object oSeen = GetLastPerceived(); void main() { if (GetIsPC(oSeen)) { ActionSpeakString("Greetings, friend."); } }
- Save it, using our standard naming convention, tm_guard_op (op for OnPerception)
- Ok everything on the NPC, save the module, and go test it.
Run toward the guard.... when you get near, it will speak its phrase. Run far enough away and come back, it will speak it again. If you stay close to the NPC, though, it won't do anything.
Let me switch into question and answer mode to try and explain this one. I'm using the same naming conventions used before, so I won't go into those. It is just the script that I'll focus on.
- Another new handle, eh? What does this OnPerception thing do?
This one calls the attached script whenever the NPC notices something in game. If something is invisible or hiding, and the NPC doesn't notice it, the script won't be called.
We want the NPC to react when she sees a character, hence the use of this handle.
- What is this object stuff in the first line? I didn't understand it the last time you threw something before main, and now you're pulling it again!
Well, really, we're using it the same way we did before, it is just a new 'data type'. Before, we set up a variable to store an integer. Now, we are setting up a variable to store an object.
I've said it before, and I'll probably repeat it again later. Nearly everything in the game is an object. NPCs, players, items, waypoints, placeables... these are all objects. Many, many functions in the game are written just to deal with figuring out what object is what, and many more are written to manipulate objects.
So, we are setting up a temporary variable, which we are calling oSeen (the starting o to remind us it is an object), and storing a value into it.
- What about the GetLastPerceived() part of the line?
This is a BioWare written function. Any of their functions that start with 'Get' will return some sort of data. The names are usually rather descriptive... this function gives as an output the last object that was seen by the NPC.
(As a note... I have a tough time thinking of any time you would use this function outside of the OnPerception event handle. It gets used in just about every OnPerception script, but basically never outside of it.)
So, putting this together with the last question, this first line of our script is just setting things up so that we can refer to the object that the NPC saw in the first place.
- You spent all that time up above talking about comparisons, and now you've only got one thing inside your if condition! What gives?
Well, this is a peculiarity of the GetIsPC() function. It takes an object as an input, and returns TRUE if it is a PC, or FALSE if it isn't.
But this is exactly what we need for a condition! If it is a PC, the attached lines run. If it isn't a PC, then they won't. If you prefer, you can change the line to:
if (GetIsPC(oSeen)==TRUE)
to make it look more like a comparison... but as we've seen, it isn't really necessary.
- Do we even need the if check at all? Won't it always be a PC that the NPC notices?
For our little test module, yes. There is really nothing else in the module for the guard to perceive.
Everything I write in these lessons, though, I try to write in the same manner that I would for a real module. What if there was a hostile goblin nearby? You wouldn't want the guard to call it a friend.
For that matter, even friendly NPCs...why bother talking to them if there is no PC around to see the interaction?
- Aha! Now I've caught you! It won't be that all PCs are friendly either!
Good point. Let's play around with our module a bit.
The Changing of the Guard
(Sorry, couldn't resist the pun....)
What we're going to do is modify our module so that the guard will attack any PC it sees that doesn't have a special ring. Only if the PC carries the ring will the guard call out the friendly greeting.
- Open the toolset, load up the module, and go to Test Area 002
- First, create the ring. Go to "Paint Items", "Miscellaneous", "Jewelry", "Rings", "Copper Ring." Place it near the module start.
- Edit the properties of the copper ring, change the tag to PASSRING
- If you're feeling ambitious, you can edit the name of the ring, give it a description, whatever. For the purposes of the script, only the tag matters.
- Now, go to the guard and open up the OnPerception script we had before. Change it so that it is like this:
// Friend or Foe Script: tm_guard_op // This should be placed in the OnPerception handle of a guard. // // The guard will check to see if a PC has a passring, and if not, attack. object oSeen = GetLastPerceived(); object oRing = GetItemPossessedBy(oSeen, "PASSRING"); void main() { // If it isn't a PC that the guard sees, it won't do anything. if (GetIsPC(oSeen)) { if (oRing == OBJECT_INVALID) { // If the PC doesn't have the ring, attack the PC. ActionSpeakString("Die, trespasser!"); ActionAttack(oSeen); } else { // Otherwise the PC does have the ring. Be friendly. ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING); ActionSpeakString("Greetings, friend."); } } }
I'm starting to embellish my scripts a bit, throwing in more and more new commands. While it may be a bit confusing at first, you'll really notice that if you start understanding the basic structure, all these random commands start to fall into place.
Anyway, save it all, load it up as a module, and see what happens. First, pick up the ring from the ground, and approach the guard. It should be friendly. Next, run away, drop the ring on the ground, and approach again. It will attack you this time.
Breaking it Down
- Argh! You've added another new initialization! I hate that!
All I can say is to trust me, the scripts look a lot worse without them. So, let's take a look at this new initialization line.
object oRing = GetItemPossessedBy(oSeen, "PASSRING");
Once again, we're setting up a temporary variable, which will hold an object. The GetItemPossessedBy() function takes in two inputs... the first is the creature object that you want to check for the item, the second is the tag of the item you're checking for.
We've already defined oSeen as the person triggering the script, the one getting noticed by the NPC. So, oRing is the ring carried by that person that has the tag PASSRING.
- But what if the person doesn't have the ring? What happens then?
Well, the GetItemPossessedBy() still runs. But since it can't find the object on the person, it comes back with OBJECT_INVALID. Basically, a fancy way of saying "No such thing."
- What are all these lines starting with '//'? They look like English instead of NWScript?
Anything in a line after a '//' is ignored by the script. These are called "comments." Any good scripter will put in comments to explain what is going on.
It is really for your own good. You may perfectly understand a script when you write it... but that doesn't mean you'll remember every detail a month from then when you want to modify it.
Also, it is polite to do it for the sake of anyone else that will look at your script. The more explanation you give on what you are trying to do, the easier someone else will understand what you have written.
- Your main script is looking really confusing. You have four sets of '{' and '}'! How am I supposed to keep track of all of them?
It can be tough, I freely admit it. The more complicated the script, the more confusing these "nested" statements can be. One trick that helps a lot is to use indentation, as I have been.
Some people like putting in blank lines to further break the script into "blocks." Again, I've done it a bit up there. I don't think it helps much in this script, but it takes no effort, so I may as well. Comments can help break up a script into blocks as well.
Even with all these things, it can still be confusing. I'm not sure what more I can say, other than "you'll get better at reading them with practice."
- What is this else?
else is an addition to the if-statement. The format becomes something like:
if ( condition ) { do_this; and_this; } else { do_this_instead; and_also_this; }
Basically, if the "if" part of it doesn't "go off", it does the "else" instead.
Let's analyze our whole inside if-statement. It checks... is the temporary variable oRing equal to OBJECT_INVALID? (That is, did the PC not have the ring?) If so, attack.
On the other hand, if that wasn't true, then the PC did have the ring. So, we do the "else" part, and be friendly.
For many people tracing through the logic of things like this is the hardest part of scripting. If you are a visual person, making a flowchart helps a lot. I would put one here, but I'm not going to try to draw one using ASCII.
- What are the new actions you put in here?
I think the names are rather descriptive. ActionAttack attacks the thing you pass to it as an input. ActionPlayAnimation has the NPC perform an animation – in this case, the one called ANIMATION_FIREFORGET_GREETING. Basically, it is just a way to tell the NPC to wave.
One More Modification
This lesson is getting huge, but there is one more thing that I want to throw in. (Ok, I lied... there are 5,217 more things that I want to throw in, but I'm trying to make this digestible.)
What if we want the opposite behavior from our guard? We want to attack if the PC has the ring, and be friendly if not? For example, maybe the ring was stolen?
What we could do is move a bunch of blocks of text around... and if we did it right, it would work. But we can actually achieve this result by changing 1 character in our script. Where it says:
if (oRing == OBJECT_INVALID)
...change it to:
if (oRing != OBJECT_INVALID)
'!=' is another kind of comparison. It checks to see if the two sides are not equal to each other. So now, if the PC does have the ring, the guard will attack.
Conclusion
There are a lot of new ideas in here, but once you have them mastered you really have unlocked the true power of NWScript. In particular, combining conditionals with local variables opens up all sorts of possibilities.
As always, feel free to ask questions about anything you don't understand. I do my best to explain things in a way that makes sense to everyone, but these are complicated ideas if you haven't dealt with them before. Sometimes, something that seems obvious to me can be confusing to someone else.
Lesson Four should be easier to write... I'll probably have it out soon. For those curious, I'm going to discuss User Defined Events, and how to make an NPC without throwing away all of BioWare's hard work in writing the default scripts.
author: Celowin, editor: Jasperre, additional contributor(s): Iskander Merriman, Shadow, Jasperre