Celowin's Scripting Tutorial, Lesson
Three - Conditionals
Introduction
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.
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.
Let's Begin
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:
(The above is not a true script, just
something to give the general idea.)
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. The double
equals sign is more asking the question "are
the two sides the same?"
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 editor.
- 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 "OnPerceived" handle, and input
the following script.
- Save it, using our standard naming
convention, tm_guard_op (op for On
Perceived)
- 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
OnPerceived 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 OnPerceived script handle. It
gets used in just about every OnPerceived
script, but basically never outside of it.)
So, putting this together with the last
question, this first line of our script is
just making it 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
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
OnPerceived script we had before. Change it
so that it is like this:
NWScript:
// Friend or Foe Script: tm_guard_op// This should be placed in the OnPerceived 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, get the ring on
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.
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
script?
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 have
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, like
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:
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 the 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?
We could do 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.