Celowin's Scripting Tutorial, Lesson
Four - User Defined Events
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.
Clean Up on
Lesson Three
The more astute people following this
sequence of lessons noticed that the guard
script we came up with in Lesson Three was
"good enough", but didn't behave exactly as
we wanted it to. Every time the guard was
"friendly", it would call out the greeting
twice. Only people paying close attention
would notice, but it is something that we
should probably fix before going on.
Besides, it gives me a chance to explain
another concept.
First, we need to pinpoint why the guard was
behaving that way... and it boils down to
the OnPerception script handle. There are
four things that can cause the attached
script to fire:
the NPC sees something
the NPC hears something
the NPC notices something disappear from
sight
the NPC notices something stop making
sounds
So, what was happening is that the NPC
was both seeing and hearing the pc, and so
the script was being executed twice. Not the
end of the world, but not the behavior we
wanted, either.
Let's fix this in the following way: We'll
have the guard only react if it sees
something. After all, the guard is looking
for a ring, and those are usually pretty
tough to hear.
We could do this by nesting another level of
if statements, but it was already getting a
bit confusing with all the different layers
we had. Instead, what we want is for the
script to check two things at once: we want
to be sure the object perceived was a pc
and that it was perceived by sight. Only
if both things are true will we do all the
rest.
We can do this just by modifying the
condition, along with the operator "&&". &&,
(read as simply "and") when put into a
condition, is a way of linking two
conditions together. The entire thing will
be true only if both parts are true. To fix
our script, then, all we have to do is
change the line:
if (GetIsPC(oSeen))
to
if (GetIsPC(oSeen) && GetLastPerceptionSeen())
Then, the script will only run if it was a
pc noticed by the guard, and also the guard
saw the pc, instead of noticing the
pc some other way.
The GetLastPerceptionSeen is a little
function that returns TRUE if the last
perception was by sight, FALSE if any of the
other three.
I won't do an example right now, but another
way of linking conditions together, instead
of && is ||. || is read "or" and means that
the condition is true if either one of the
parts is true. This might be used if there
were multiple reasons why the guard might
attack. (Perhaps guard would attack if the
pc didn't have the ring, or if the pc was
carrying the head of the mayor. Either one
by itself is a reason for the guard to
attack.)
A Confession
Right now, I have to admit that I've been
lying to you all throughout the past three
lessons. Multiple times, I've said that I
use the methodology I do is because I am
doing everything the way I would inside a
real module. But now I have to come clean –
not a single script I have done here is
really the way I would put it in final form.
The problem is, that our NPCs so far have
been totally unresponsive to most stimuli.
The guard we finished up top is starting to
get there, but I would hardly call its
behavior realistic. If you're interested,
here is an experiment you could do: Beef up
the hp and level of the guard. Start up the
module. Get the ring, so the guard will be
friendly to you. Then go attack the guard.
It will just stand there, and let you beat
on it. This is definitely not what we
want from most of our NPCs.
The reason that we have been creating such
morons is that we threw away all of
BioWare's hard work in writing scripts. The
default scripts that we have been deleting
define a number of useful "standard"
behaviors that we really probably want to
keep in almost every instance. (Actually,
one of the NPCs we'll be creating today we
will want to throw away the default
scripts, but more on that when we get to
it.)
So, how do we define our own behavior,
without throwing away all the default stuff?
We use the "user defined" script handle. If
we are clever, we can really use every other
handle – we'll make only minor modifications
to the OnSpawn script, and do most of our
scripting in the UserDefined.
We've spent so much time on our guard so
far, let's go ahead and fix it up to the way
it should be.
Open up the Test Module
Remove the guard
Create a new NPC where the guard was
(So we have all the default scripts back)
Change the NPC tag to GUARD
On the "Advanced" tab, make sure the
guard has faction of "commoner."
Go to the scripts tab
Edit the "OnSpawn" script. There is a
lot of stuff here, we'll ignore most of
it.
Go down to the bottom of the script.
Find the line:
NWScript:
//SetSpawnInCondition(NW_FLAG_PERCIEVE_EVENT);
//OPTIONAL BEHAVIOR - Fire User Defined Event 1002
On that line, remove the first //.
(Keep the second, in front of OPTIONAL)
Save the script as tm_guard_os
Close the script window, and go to the
OnUserDefined handle.
Select the script tm_guard_op
Open it up into the editor. Save it as
tm_guard_ud
Update the comments to reflect that it
is in a new place, and has a new name.
Save again.
Ok your way out of the guard, and save
your module.
Now, if we start up the module, the guard
will behave more realistically. He still
does the "friend or foe" reaction that we
scripted into him, but also reacts to other
stimuli. If you attack him, he fights back.
There are lots of other behaviors that are
scripted in there, many of which happen
"behind the scenes" that you will probably
never notice.
So, what did we actually do? Well, when
removing the // in the OnSpawn script, we
"uncommented" something. Remember that
anything after // on a line in a script is
ignored. So what BioWare did was put in a
bunch of "optional" stuff into the OnSpawn
script, and put // in front of it so that it
wouldn't happen.
But now, we want some of it to
happen. By removing the //, now we are
saying that we want that line to actually
take effect.
So, what does that line we "put back in"
actually do? In essence, it is saying: "When
you are running the OnPerception script,
also do what I put into the OnUserDefined
script."
Now, the more astute of you might be
thinking ahead, and asking "What if I want
to have multiple new behaviors from an NPC?
What if I want to have it do something
special on OnPerceived and also on
OnHeartbeat?"
It can still be done, but takes a bit of
extra work. Just to keep the script small,
let's make an NPC that does something
simple. It will say "I'm bored," every six
seconds, and bow when it sees a pc.
Open the toolset, paint the NPC, and
give it the tag BORED
Open the OnSpawn script, and uncomment
the lines for the OnHeartBeat and
OnPerception lines.
Notice that each of these has a
"number" associated with it. 1002 for
OnPerception, and 1001 for OnHeartBeat.
Save it as tm_bored_os
Put the following script into
OnUserDefined, and save as tm_bored_ud
NWScript:
// On User Defined Script: tm_bored_ud// Will be called by the OnHeartbeat and OnPerception scripts//// The NPC will complain about being bored every six seconds,// and will bow if it sees a pc.//int nCalledBy=GetUserDefinedEventNumber();
void main(){switch(nCalledBy){case 1001: // Called by OnHeartbeatActionSpeakString("I'm bored.");
break;
//case 1002: // Called by OnPerceptionobject oSeen=GetLastPerceived();
if(GetIsPC(oSeen) && GetLastPerceptionSeen())ActionPlayAnimation(ANIMATION_FIREFORGET_BOW);
break;
}}
Question and Answer time....
What is this GetUserDefinedEventNumber?
This is exactly the number I told you to pay
attention to up there. BioWare was very
clever... not only can each different handle
call the user defined script, but each one
passes a different number to it when it does
so. So, what I'm doing in that first line is
checking which one of the scripts called
this one. Was it the Heartbeat (1001) or the
Perception (1002)?
What about this switch command?
I don't want to go a whole lot into detail
on this one. Basically, you give it an input
of an integer, and the script then "jumps"
to the line marked with "case" and that
number. So if our nCalledBy is 1001, the
script looks down for the line "case 1001:"
It starts doing commands at that point,
until it gets to a break.
A few notes on formatting: The switch
command only looks within the lines tied to
it with { and }. Also, like the if
statement, there is no semicolon after the
switch line.
When I'm writing UserDefined events, I
usually try to always set them up with a
switch like this, even if I only plan on
having one type.... just because I might
change my mind later. It is better to be
prepared for a possibility, than have to
monkey with a bunch of work because you were
too lazy to plan ahead.
Hey, you didn't use { and } with your if
statement!
If there is only one command attached to the
if statement, the curly braces aren't
needed. To me, sometimes it looks better
with them, and sometimes without them. I go
with whichever makes the script look
cleaner. In this case, I decided that they
would just clutter up the script.
One More Example
At this point, you know a good deal about
scripting. I shouldn't be calling these
Absolute Beginner lessons any more, because
you aren't. There are still a lot of
functions you need to learn, and a few more
tricks. But really, there are lots of cool
things that we can do with what we've
learned so far, if we put the pieces
together.
I'm going to do an example like that now.
The script is pretty complicated, requires a
lot of setup, and uses some new commands.
But I think the end result is worth it.
Open up the Test Module
I want to get away from our guard, so
go back to the first area that we created.
Remove the SINGER NPC that is there
Paint in a module start point
Paint a commoner NPC in one corner of
the room. (Doesn't really matter if it is
a commoner, we're going to change pretty
much everything.)
On the "Basic" tab: Change the first
name to Dartboard, the tag to DARTBRD, the
race to "Construct", the appearance to
Archery Target, the gender to none, and
the portrait to po_PLC_F01_ (found by
clicking the "Placeable Objects and
Doors")
On the "Advanced" tab, check the
"Plot" box.
Still on the Advanced tab, go to the
faction editor. Create a new faction
"Target", whose parent faction is
"Hostile." Set both Target-Commoner and
Commoner-Target to 50.
Change the Dartboard faction to
Target.
On the "Scripts" tab, delete all the
scripts. (In this case we don't
want the dartboard going on a rampage and
attacking people.)
Edit the "OnDamaged" handle. Put in
the following script.
NWScript:
// OnDamaged Script: tm_dartbrd_dm// Dartboard script// Goes "thunk" when hit with a ranged weapon.//void main(){if(GetWeaponRanged(GetLastWeaponUsed(GetLastAttacker()))){SpeakString("**Thunk**");
}}
If you want, you can test out just the
dartboard by saving and loading up the
module, but that isn't the cool part.
Paint a waypoint about 1 square away
from the dartboard.
Give it the tag DARTWP001
Paint a commoner NPC near the
waypoint.
Change the NPC tag to DARTPLAY
For fun, click the "random name"
button.
Under the "Feats" tab, give it "Weapon
Proficiency (simple)"
Click the "Inventory" button, under
the full body picture of the NPC
On the right, go to "Weapons",
"Throwing", "Dart" and drag it to the
"Contents" spot.
Right click the dart you just dragged
over, edit the properties, and change the
stack size to 3
Click OK, then click and drag the dart
up to equip it on the NPC.
Click OK again, to get out of the
inventory.
Now, we go to the scripts tab.
Edit the OnSpawn script, uncomment the
HeartBeat line.
There is another line to uncomment as
well: SetSpawnInCondition (NW_FLAG_SET_WARNINGS);
Also, add in a line somewhere:
SetLocalInt(OBJECT_SELF, "DARTSTATE", 1);
Save the script as tm_dartplay_os
Now go to the UserDefined handle, and
add in the following script:
NWScript:
// On User Defined Script// tm_dartplay_ud// Used to have someone play darts. Called by the OnHeartBeat script.//// The Dart Player will throw all darts in inventory (should start with 3),// walk to the dartboard, get 3 darts, walk back, and repeat.//void main(){int nCalledBy = GetUserDefinedEventNumber();
object oTarget = GetNearestObjectByTag("DARTBRD");
int nDartsReady = GetLocalInt(OBJECT_SELF, "DARTSTATE");
//switch(nCalledBy){case 1001: // Called by OnHeartbeat//// nDartsReady will be 1 if ready to throw, 2 if not.//if((GetIsObjectValid(GetItemInSlot(INVENTORY_SLOT_RIGHTHAND)))
&& (nDartsReady == 1)){// If we have darts in the right hand, and we're ready to throw, go for it.ClearAllActions();
ActionAttack(oTarget, TRUE);
}else{// Otherwise, there are two cases. We've either just run out, or we are in// the process of getting darts. We don't want to interrupt
// the cycle if we're// already working on it.if(nDartsReady == 1){SetLocalInt(OBJECT_SELF, "DARTSTATE", 2);
ActionMoveToObject(oTarget);
ActionWait(0.5);
ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 1.0);
ActionWait(0.5);
ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 1.0);
ActionWait(0.5);
ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 1.0);
object oDestination=GetNearestObjectByTag("DARTWP001");
ActionMoveToObject(oDestination);
CreateItemOnObject("nw_wthdt001", OBJECT_SELF, 3);
ActionEquipMostDamagingRanged();
ActionDoCommand(SetLocalInt(OBJECT_SELF, "DARTSTATE", 1));
}}
break;
}}
Well, this is a really complicated
script. I'm not going to explain absolutely
everything about it, but I'll point out a
few key things.
I'm using the local variable DARTSTATE to
make sure that we don't attack when not
ready, or go through the "get darts"
sequence multiple times.
The ActionDoCommand is amazingly
handy. It takes a non-queued command, and
turns it into a queued one. Normally, as
soon as the script sees a SetLocalInt
instruction, it sets the local variable.
This forces the script to wait until the NPC
has completed all the previous actions.
The CreateItemOnObject is what is used to
make the new darts for the NPC.
"nw_wthdt001" is the blueprint ref for a
dart, and the final 3 is the stack size.
Other than that, see if you can trace
through the script yourself. There are other
new commands, but most of the names are
pretty self explanatory. As always, ask
questions if you can't figure it out.