Celowin - Part VIII Functions

From NWN Lexicon
Jump to: navigation, search

Celowin - Part VIII: Functions

Preamble


The purpose of this sequence of lessons is to take a complete beginner to programming, and teach him or her how to use NWNScript 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 be a difficult one, and I'm not even going to really be scratching the surface of the topic. Ever since lesson one, we've been using functions that have been written by BioWare. This time, we're going to start to learn how to write our own functions.


Don't worry if as this lesson goes on, you don't understand everything that I say. This stuff is never learned in one sitting, and in fact takes a lot of practice before it makes sense. If you feel you're getting over your head, put it aside for a bit, go do something else, and come back to it later.


A bit of warning before we start. Even though we are going to be writing functions, they will only be able to be used "locally." That is, imagine we are writing a script for a particular handle, say the OnPerception for an NPC. We write a function to use in that script. We can use it in our "main" for that script, but we can't use it anywhere else. We couldn't use it for any other handle on that NPC, nor could we use it for a different OnPerception script.


Yes, this limits us a lot as to what we can do with the functions we write. At the same time, it still opens up a lot of possibilities. And fear not, in a future lesson I will explain how to write more generically useful functions, but we have to take things one step at a time.


Function Declaration


Every function will start with a line something like this:

void GateIn(string sBluePrint, location lGate)

It is a brief line, but there are a lot of complicated concepts tied up into it.


The first part, void tells us what the output for the function will be. So, in this case, the output is going to be .... um ... nothing. All the other data types we have been using could go here: int, float, object, event, etc. The most commonly used one will be void, and for this lesson it will be the only one that we deal with.


The next part, GateIn names the function. Once we have it defined, we can call the function using this name, just like all the BioWare defined functions. (Though again, we must remember the restriction that we can only call our function in the script we are writing it for.)


The part inside the parentheses is the most difficult part to understand... here we are setting up the inputs to our function. (The technical term for these is "parameters," but you probably don't need to remember that.) We are saying that we are going to have two inputs into our function... one string, and one location.


So far, so good. Now for the confusing part. Before we can actually write the function, we need to understand what will happen when the function is called. (Right away, that seems backwards, that we need to understand the call of the function before we write it, but bear with me a moment.) Presumably, we are writing a function that will do something with those inputs, otherwise we wouldn't need them.


Suppose, then, that somewhere in our script, we call the function like this:

GateIn("nw_fireelder", lSummonPoint);

Now, then, "nw_fireelder" is our first input, a string. Effectively, the first thing our function does is set sBluePrint equal to "nw_fireelder". Anywhere in our function, we can use the variable sBluePrint to stand for this. The same thing for our second input: lGate is set to whatever location lSummonPoint is holding.


Function Definition

OK, let's look at the whole function now.


// This function summons the creature with blueprint ResRef 
// given by sBluePrint at the location lGate, then it has the
// summoned creature attack the closest PC to the object that
// calls this function.
void GateIn(string sBluePrint, location lGate)
{
  // Create the creature
  object oNewCreature = CreateObject(OBJECT_TYPE_CREATURE, sBluePrint, lGate);
 
  // Find the closest PC
  object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, OBJECT_SELF);
 
  // Cause the creature to attack the PC
  AssignCommand(oNewCreature, ActionAttack(oPC));
}

Note that if you type this up and try to compile it, you will get an error message: ERROR: NO FUNCTION MAIN() IN SCRIPT. By itself, a function isn't a script. You still need your void main(), but for now I just want to look at the function by itself.


Probably the most important line is the first one, where we create the object. We are just using the same old CreateObject routine we've used many times before, but we are passing our inputs to it. Whatever inputs are given to GateIn when it is called, are then passed along to the CreateObject function. It seems confusing, but it is here that the real power comes in. We can call the GateIn command multiple times, passing it different inputs, and it will summon the different creatures.


The next line, starting object oPC = is long, but mainly it is just because of the things we have to tell it. That whole line is just saying "find the nearest PC to OBJECT_SELF." We don't know what OBJECT_SELF is at the moment, because we don't know what object is actually calling the GateIn function.


Finally, then, we tell the new creature to attack the PC we just found. Nothing major there.


Let's Try it Out


I'm going to write a fairly complicated script using the function I just showed. We could certainly do something a lot easier than this, but I want to do an example where we can see why we'd want to use a function.


  1. Start up the toolset.
  2. First, use the item wizard to create a Miscellaneous Medium item.
  3. Give it the name Sorcerer's Skull
  4. Put it under Plot Items on the palette
  5. Finish the wizard, and edit the properties
  6. Give it the tag ALTSKULL
  7. Change the appearance to iit_midmisc_021
  8. OK out.
  9. Paint a waypoint, tag it ALTSUMWP
  10. Place an altar nearby, edit the properties.
  11. Tag the altar with SUMMALTR
  12. Check the "usable" and "has inventory" boxes.
  13. Open the altar's inventory, put in a copy of the skull we just made.
  14. 'OK' out of the inventory, go to the scripts.
  15. In the OnDisturbed handle, put the following script:
// OnDisturbedScript: tm_summaltr_ds
//
// This script gates in one of 10 random creatures
// when a skull ALTSKULL is removed from the altar.
// The creature is gated in at the waypoint
// ALTSUMWP
//
// Written by Celowin
// Last Updated: 7/16/02
 
// This function summons the creature with blueprint ResRef 
// given by sBluePrint at the location lGate, then it has the
// summoned creature attack the closest PC to the object that
// calls this function.
void GateIn(string sBluePrint, location lGate)
{
  // Creates the creature
  object oNewCreature = CreateObject(OBJECT_TYPE_CREATURE, sBluePrint, lGate);
  // Find the closest PC
  object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, OBJECT_SELF);
  // Cause the creature to attack the PC
  AssignCommand(oNewCreature, ActionAttack(oPC));
} // end function GateIn
 
// Here is our main function:
void main()
{
  // If the skull is in the altar, we don't care, nothing will happen.
  // Note that OBJECT_SELF refers to the altar
  if (GetItemPossessor(GetObjectByTag("ALTSKULL")) != OBJECT_SELF)
  {
   // Find the summon spot, via the waypoint.
  location lSummonPoint = GetLocation(GetWaypointByTag("ALTSUMWP"));
 
  // Create the visual effect for the gate.
  effect eGate = EffectVisualEffect(VFX_FNF_SUMMON_GATE);
  ApplyEffectAtLocation(DURATION_TYPE_TEMPORARY, eGate, lSummonPoint, 3.0);
 
  // Randomize what creature is being summoned.
  int nCreature = d10();
  switch(nCreature)
    {
    case 1:  // Summon a polar bear.
      DelayCommand(3.0,GateIn("nw_bearpolar", lSummonPoint));
      break;
    case 2:  // Summon a cow.
      DelayCommand(3.0,GateIn("nw_cow", lSummonPoint));
      break;
    case 3:  // Summon a bone golem.
      DelayCommand(3.0,GateIn("nw_golbone", lSummonPoint));
      break;
    case 4:  // Summon an elder fire elemental.
      DelayCommand(3.0,GateIn("nw_fireelder", lSummonPoint));
      break;
    case 5:  // Summon an ogre high mage
      DelayCommand(3.0,GateIn("nw_ogremageboss", lSummonPoint));
      break;
    case 6:  // Summon a yuan ti mage
      DelayCommand(3.0,GateIn("nw_yuan_ti002", lSummonPoint));
      break;
    case 7:  // Summon a spitting fire beetle
      DelayCommand(3.0,GateIn("nw_btlfire02", lSummonPoint));
      break;
    case 8:  // Summon a Kreshar
      DelayCommand(3.0,GateIn("nw_kreshar", lSummonPoint));
      break;
    case 9:  // Summon a werecat, human form
      DelayCommand(3.0,GateIn("nw_werecat001", lSummonPoint));
      break;
    case 10:  // Summon a high lich
      DelayCommand(3.0,GateIn("nw_lichboss", lSummonPoint));
      break;
    } // end switch
  } // end if
} // end main

Save everything, and go test it. When you remove the skull from the altar, a random one of the 10 creatures will gate in and attack (well, the cow won't attack, but the others will).


As long as it is, our main function really isn't all that complicated. First, we check to see who has the skull... if it is anything but the altar, we go forward.


We create a gate visual effect at the waypoint. This is just like the effects we did last lesson. Then, we get a random number from 1 to 10, and call our gate in function to summon the creature.


Now that we have this example in front of us, let's discuss why we wanted to use a function here. There are actually two reasons... one that is pretty straightforward to understand, and another that is a bit more complicated.


Let's discuss the easy one first. Basically, every time we call the GateIn function, we are saving ourselves from writing out three lines of script. By just calling the GateIn function, we are summoning the creature, finding the target, and attacking the PC all in one. Since we have 10 different cases, we've saved ourselves about 20 lines in our script, not counting comments. (And given how ugly that function was to find the PC, I'm glad not to have to have it in my script repeatedly.)


The second reason is a bit tougher to understand... but basically, we had to use a function in this case. We wanted to delay the summon until the gate was formed, so that the fire effect would mask the appearance of the creature. Thus the need for the DelayCommand. However, we can only DelayCommand things that have an output of void, and the CreateObject command returns an object. If you try to do a DelayCommand for a CreateObject call, it won't compile. By putting the CreateObject into a separate function, that did return void, we get around that restriction.


Cleanup


I think this lesson is a bit too complicated to expect you to be writing your own functions just yet, but I'll have you fix the previous script a bit. The way it is now, if the skull is in the altar, everything is peachy. You can add items to the altar and remove them, as long as the skull remains. As soon as you remove the skull, a creature is gated in. All this is fine.


However, what if you continue to play with the altar? If you hold onto the skull, and put any other item into the altar, it will gate in another creature! Remove that new item, and it gates in another! This may be what you want, but probably not. So, fix it so that only when the skull is removed will the creature be summoned. You actually don't have to change the function at all, you just need to play around with the conditional for your main.


Example 2: Removing Plot Items


A certain sick, twisted DM (oh wait, that was me) once designed a surreal dream sequence for his PCs. Talking penguins, nonsequiturs, bizarre puzzles, the works. Eventually, though, the PCs woke up. In pen and paper, it is easy to just get rid of all the items the players picked up in the dream world. But how about in NWN?


Well, it takes a bit of planning, but it can be done. First off, I named all my dream world items with similar tags. All plot items were DREAMITM, all weapons were DREAMWPN, all armor was DREAMARM, and the key (there was only 1) was DREAMKEY.


Then, to the dream world OnExit handle, I attached this script:


// On Exit Area script: tm_area002_ex
//
// This removes all items with tag DREAMITM, DREAMWPN,
// DREAMARM, or DREAMKEY from the exitng PC.
//
// Written by Celowin
// Last updated: 7/16/02
 
// This function strips all items with tag
// sTag from the object oStrippee
void StripItems(object oStrippee, string sTag)
{
  // Initialize: Get the first inventory item
  object oCurrentItem = GetFirstItemInInventory(oStrippee);
 
  // Loop through all items in inventory
  while (oCurrentItem != OBJECT_INVALID)
  {
   if (GetTag(oCurrentItem) == sTag)
     DestroyObject(oCurrentItem); // Destroy items with correct tag
   oCurrentItem = GetNextItemInInventory(oStrippee);
  } // end while
} // end StripItems function
 
void main()
{
  // First, get the one exiting
  object oPC = GetExitingObject();
  if (GetIsPC(oPC))  // If it is a PC, strip the items
  {
    StripItems(oPC, "DREAMITM"); // Generic dream items
    StripItems(oPC, "DREAMWPN"); // Dream weapons
    StripItems(oPC, "DREAMARM"); // Dream armor
    StripItems(oPC, "DREAMKEY"); // Dream key
  } // end if
} // end main

(Note, there is actually a better way of doing this with some string manipulation... this version is really inefficient, since it loops through the PC inventory four times. It works, though, and since it will only be run once per PC if you design the module right, it isn't a big deal.)


You can test this if you want. Create and drop a few DREAM*** items into your module, attach the script to one of the areas' OnEnter scripts, and play around with it. Also, for you cruel dms, this can easily be modified to remove every item from a player...


Wrap Up


I've just barely scratched the surface of functions here. We can write functions to calculate things for us, we can create libraries of functions, we can use functions to drastically reduce the complexity of some scripts. Because of the power of functions, though, it is difficult to cover everything at once.


For the most part, the feedback I've gotten from people has been positive. There is only one minor complaint that comes up repeatedly, something like this: "I can follow what you do, and you explain yourself well. So, I can see the how and the why. But I have a tough time figuring out the when. I never know when I should use one of the tools you have shown, and when I should do something else."


I try to explain that, as well, but the problem is that it is something that comes with time. After you've looked at enough scripts, you start to develop an intuition about when to use a certain technique. This isn't to say that it is easy... even once I know what I'm doing, I often bang my head against the keyboard for hours to get a complex script to work.


So, for functions, what is the "general rule" on when to use them? I'd say, look at what you're doing. If you find yourself writing the same things over and over, odds are that you want to use a function to simplify it. Even if it is only a few lines that you find yourself repeating, your code will end up much easier to understand if you write a function to encapsulate those lines.


The Future


I'm really starting to run out of ideas for these lessons. I've really covered all the "basics," and I'm actually starting to touch on some pretty advanced concepts. Sure, there are little details here and there that I've glossed over, things that I've omitted, but I would think at this point that you should be able to look at almost any script and say "Hey, I know what that does." Maybe you couldn't write it yourself from scratch, but you should be able to dissect it and figure out what it does.


I certainly should talk a little bit about "include" statements, and writing your own function libraries, but I don't think that will be a very complicated lesson.


So, unless I'm struck with a sudden inspiration, I think this series will be ending around the 10th tutorial – at least in this form. I have an idea for a "followup" series, that will take a different approach. I'll give more details on that after I hammer out a few details.





 author: Celowin, editor: Charles Feduke