Ten Caveats for the Novice Scriptor

From NWN Lexicon
Revision as of 08:32, 8 March 2018 by Squatting Monk (talk | contribs) (Update script example for efficiency and ease of understanding)
Jump to: navigation, search

Resref vs Tag

The tag of an object is fundamentally different from the resref. The resref is the blueprint or template for any object having identical characteristics, and forms a list on the right of the toolset screen. The tag is an identifier for a particular example. So creating a new object that does not yet exist must call the template, not the tag. This is why CreateObject() always calls the resref. When you call up the tag, you are calling one of a number of examples of actual objects bearing that tag. Getting this wrong will not call up the object. I usually give identical names to the tag and the resref to minimize confusion, and perhaps if required later change an object's tag once it is already placed in the area.

Infinite CreateObject() Loops

A commonly used script creates a creature, item, or placeable on a heartbeat script whenever the PC approaches to within a certain distance. The problem is that the GetDistanceToObject() function returns -1 when the PC is in a different area. This will, of course, satisfy a "less than certain distance" condition and ever increasing numbers of objects are created every 6 seconds. On starting the adventure (in a different area) all the players will experience is ever increasing game pauses every 6 seconds. So the condition should be written as:

if (GetDistanceToObject(oPC) > -1 && GetDistanceToObject(oPC) < x))
{
    // execute code here ...
}

GetObjectByTag() Not Working

Problems arise when there is more than one object in the game with the same tag. The one the computer selects might not be the one you want to select. For example, on one occasion my main OnActivateItem script controlling unique function items stopped working. Eventually I realized this was because during play-testing I had created, for convenience, another copy of the unique items near the place where they were to be used, and had forgotten to remove them again. Avoid by having only one copy of items or, when multiple copies should be present, change the tags or use GetNearestObjectByTag().

Action Functions

Ever found those NPCs just don't do what they have been scripted to do? This is often because of inappropriate use of ActionX commands versus straight X commands, and the ClearAllActions() function. ActionX (e.g., ActionSpeakString()) puts the X command at the back of the action queue, while an X command (e.g., SpeakString()) executes immediately.

ClearAllActions() clears the queue. One problem is that the include files that code for special called functions often include ClearAllActions() functions; for example DetermineCombatRound() will interfere with previous actions. Also basic NPC AI may activate various scripts that "rub out" your special commands, particularly if you have queued more than one.

Finally, in a conversation, the quit conversation scripts may cancel out what you wanted the PC to do and they might WalkWayPoints() instead. These quit conversation scripts can be removed within the conversation editor (the tag to the right of "Other actions").

Getting Effects to Work and to Stop Working

I have found that, when I put an Effect on something, e.g. a petrification effect...

ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectPetrify(), oObject);

...it is difficult to cancel again by RemoveEffect() or by a reversing spell. This is partly because there is no such thing as a "GetLocalEffect" command allowing easy identification of the effect to be removed between different scripts. One has to create a script to work through all the effects on the target and use the appropriate EFFECT_TYPE_* constant to remove the one you want. Example:

effect eEffect = GetFirstEffect(oPC);
 
while (GetIsEffectValid(eEffect))
{
    if (GetEffectType(eEffect) == EFFECT_TYPE_PETRIFY)
    {
        RemoveEffect(eEffect);
        break;
    }
 
    effect = GetNextEffect(oPC);
}

A workaround I sometimes use (because I am far from an "advanced" scripter and they sometimes don't seem to work the way I intended) is, instead of applying an effect, actually to cast a spell on the object using invisible object casters. One must remember to set cheat to TRUE in the cast spell action, and to uncheck the invisible object's "static" property. When the PCs enter the area, I could have an Area OnEnter script tell the invisible object to cast the appropriate "effects". These effects can be reversed with appropriate spells cast by scripts or by the PCs.

Declaring Variables at the Top

It is best to declare all variables at the beginning of the script. Sometimes, when they are only declared within an if statement, for or while loop, or switch-case routine, strange things happen. I think that version 1.31 might be dealing with this. A similar version-related problem (1.59?) has something to do with ordering of different DelayCommand() functions when their delay time is set to be simultaneous. If something isn't working, try experimenting with introducing a 0.1 s delay just to force things to occur correctly.

Information icon.png Editor's Note: Declaring a variable within lines of code between curly brackets ("{" and "}") is called scoping. A variable goes out of scope when the terminating curly bracket ("}") is met. Sometimes it is desirable to have a variable scoped to a specific area of code rather than an entire function. Also note that you can have anonymous scopes; that is, curly brackets with no surrounding if, else, while, switch, etc. statements

Non-Random Random Numbers

I found once that my so-called random numbers were not random at all. This took ages to sort out, and I almost decided to do my own random number generator myself from the seconds on the time clock, but I eventually realized the problem. If you have a random number on an area OnEnter handle, such as determining which one of a set of random encounters will occur, the random number generator resets so you nearly always get the same number! I solved this by getting the OnEnter script simply to call another script that actually does the work. A DelayCommand() of 0.5 s was applied to an ExecuteScript() command. This seemed to be enough to randomize the clock that I think the random number generator works off.

Information icon.png Editor's Note: This information is out of date. Acaos has confirmed that the reseed for Random() is based on the server timestamp only.

Local Variable Ownership

Be careful when deciding who is the "owner" of a local variable. A common use of local variables is simply to determine if a PC has already spoken to an NPC or already completed a quest. The first conversation has a SetLocalInt() to set a local variable to 1, and the next time round the conversation changes if the GetLocalInt() reveals that the local variable is now 1. The default scripting wizard sets the PC as the"owner". However, especially in multiplayer games it is usually more appropriate to set the NPC as the owner, so for example in the script editor...

... needs to be changed to...

SetLocalInt(OBJECT_SELF, "done", 1)

A common scripting error is getting the variable's owners mixed up even when you know what you intended. This is one of the first things I check when conversations and quests are not working properly.

Another aspect of this is that you can't call a variable when the owner has been destroyed! So don't set the owner of a quest completion variable as the villain who is supposed to be killed! I often use the module as the owner for such purposes (i.e., GetModule()). I suppose some people will be using all sorts of new 1.30 database functions for plot states etc. Ask those clever people about such things, not a fellow scripting novice like me! As far as I can see, those things are mainly useful for persistent worlds so that some poor nutter doesn't have to DM a game 24 hours a day every day! Note the Baldur's Gate 2 tip: "Remember, your computer doesn't need any food or sleep, but you do. We don't want to lose any dedicated players!"

Plot Flags

When scripting objects such as placeables that function as levers or require PC interaction, PLEASE remember to check the plot flag in the object's properties. Otherwise, if the PCs cast area of effect damage, the object may be destroyed and prevent the PCs from continuing through the adventure. You may not even notice this nasty bug while play-testing, because you know the areas and may dispatch the enemies more efficiently than by blasting fireballs everywhere. Your players will certainly be frustrated if their whole game is ruined near the end! So anything where the useable flag is checked, and you don't want it to be destroyed, always check the plot flag. I also do it for any object that is not useable but the static flag is unchecked, just in case some invisible object or other trigger also gets affected. Even in the official campaign, you can see some strange invisible objects that have names sounding like triggers taking damage at odd points, which tends to ruin the atmosphere if nothing else.

"Don't Do What I Do, Do What I Say!"

I have uploaded about three adventures. (Hardly anyone votes for them. Boo hoo.) They have increased quite a bit in sophistication as I gradually learned the scripting. If you look at the scripts, they are a bit of a mess, and the naming conventions leave a lot to be desired. Probably, they still have some bugs in them. So don't be like me. Have a consistent system for naming and stick with it. This is particularly useful if you come back to the module after leaving it unfinished or transferring routines to another module. Don't ever change the standard BioWare scripts without saving them with a new name. This is all too easy to do if you are in a creature's script handle editor and you just want to change that OnSpawn script or OnHeartbeat script a little.

I hope you find that these points save you some time while scripting and debugging. Happy module-creating!


 author: John McAuley, editor: Charles Feduke