One great example that ships with the game (and most people would be familiar with) is the SpeakTrigger. Simply paint the trigger, edit the variables to set the correct NPC to speak, whether it's a cutscene, whether they talk immediately or if they have to run to you first, whether it's a one shot or able to be used multiple times, and you're done. You don't have to open up the script and save a new copy each time you want a speak trigger.
It's significantly easier to enter in these variables rather than recoding the script everytime - have you delved inside the gtr_speak_node and the call it makes? Recoding that everytime you wanted a different speak trigger would be fraught with danger (you could break the script by typing in the wrong place) not to mention inefficient. Each separate speak trigger would have its own script that the NWN2 engine would have to load up (small processing overhead) and store (small storage overhead). Sure, a few fractions of a second or kilobytes here or there don't seem like much, they add up. But enough harping on about how great local variables are - let's get on to some examples of WHY!
I've got another example that I created for my module that enables the use of skills in an "invisible" fashion. What I wanted was a trigger blueprint that would do a skill DC check on the PC the first time the PC enters the trigger. I want to be able to specify the following things:
* The DC to check against
* The skill that is being test
* A script to run upon "success"
* Whether the script is run when I succeed the check, or when I fail the check
So my blueprint looks like this:
Now, while I'm espousing the qualities of local variable to cut down on scripting, you'll notice that I've I'm running another script when the test "succeeds". So you might argue I'm not saving much in terms of scripting, but the bonus is that I don't have to duplicate my skill dc check code everytime I want a new skill dc trigger.
Basically, I yanked the code from the gc_skill_dc script and modified for use on a trigger. The code looks like so:
I've included all the comments from the original script with a few additions - that way I can easily open up the script to remind me which value corresponds to which skill. Also, please note the point at the top where it says every trigger must have a unique tag - if you don't, then running over one tag will result in another tag not reacting the first time you run over it.
// tr_en_skill_dc
/*
NOTE: You *MUST* use a unique tag for every object that uses this script.
Will do a skill DC test for the PC *ONCE ONLY*
The parameters below should be set as the variables of the object.
If the check succedes, the script will execute the script specified by the "script" variable on the object.
ALTERNATIVELY:
It is possible to set "nFailCheck" to 1, and then the script will execute if the PC FAILS the check
Multiplayer note: It also may be desirable to give the option to only fire once - ie destroy self afterwards.
Parameters:
int nDC = dc check value
int nSkill = skill int to check
string script = script to run upon "success"
int nFailCheck = whether to run the check on passing or failing the check
0 = run the script if the check succeeds (default)
1 = run the script if the check fails
Remarks:
skill ints
0 APPRAISE
1 BLUFF
2 CONCENTRATION
3 CRAFT ALCHEMY
4 CRAFT ARMOR
5 CRAFT WEAPON
6 DIPLOMACY
7 DISABLE DEVICE
8 DISCIPLINE
9 HEAL
10 HIDE
11 INTIMIDATE
12 LISTEN
13 LORE
14 MOVE SILENTLY
15 OPEN LOCK
16 PARRY
17 PERFORM
18 RIDE
19 SEARCH
20 CRAFT TRAP
21 SLEIGHT OF HAND
22 SPELL CRAFT
23 SPOT
24 SURVIVAL
25 TAUNT
26 TUMBLE
27 USE MAGIC DEVICE
*/
// BMA-OEI 9/02/05
//AmstradHero - 16/05/08 : Modified to deal with multiple PCs and companions
#include "ginc_param_const"
void main()
{
object oPC = GetEnteringObject();
if(!GetIsPC(oPC))
return;
int nDC = GetLocalInt(OBJECT_SELF, "nDC");
int nSkillVal = GetSkillConstant(GetLocalInt(OBJECT_SELF, "nSkill"));
object oOwner;
if (GetIsOwnedByPlayer(oPC))
{
oOwner = GetControlledCharacter(oPC);
}
else
{
oOwner = oPC;
}
int nDoOnce = GetLocalInt(oOwner, "DO_ONCE" + ObjectToString(OBJECT_SELF)); // a unique do-once
if(nDoOnce == 1)
return;
SetLocalInt(oOwner, "DO_ONCE" + ObjectToString(OBJECT_SELF), 1);
SetLocalObject(OBJECT_SELF, "oActivator", oOwner);
if (GetIsSkillSuccessful(oPC, nSkillVal, nDC) == TRUE)
{
if (GetLocalInt(OBJECT_SELF, "nFailCheck") != 1)
{
string script = GetLocalString(OBJECT_SELF, "script");
ExecuteScript(script, OBJECT_SELF);
}
}
else
{
if (GetLocalInt(OBJECT_SELF, "nFailCheck") == 1)
{
string script = GetLocalString(OBJECT_SELF, "script");
ExecuteScript(script, OBJECT_SELF);
}
}
}
Two interesting things from this script. Firstly, it only operates on a party leader, and only takes their skill into consideration. That's what the oOwner logic is doing - making sure that any companion is ignored for the purposes of the skill check. It wouldn't be too hard to add in another local variable that would allow you to take advantage of companion skills as well, and then add the appropriate logic in for that. (If anyone is particularly interested in this script, I could code up and post a modified version that would allow just that.)
The second is that you'll also note that I've got a SetLocalObject call that stores the PC (oOwner) as an object on the trigger. This is to allow future access of the PC (if required) for any script that is activated by this trigger.
With those things in mind, using this trigger is practically complete. All you need to do is paint the trigger, fill in the relevant variables and then code up the secondary script. I've found it a useful way to code up little encounters and skill checks.
If you want something a little more specific, then I made a modified version of the above that performs a "tracking" test on the PC, and can get the PC to comment based on whether they have succeed the check or not.
All you need to do is create a new trigger blueprint, add in the relevant variables to the blueprint (I'd suggest adding a default XP reward value on the blueprint), then attach the script. These are fairly simple scripting examples, but they're useful tools for providing some extra skill checks into a module. Players like their skills and choices to have an effect, so make life easier for yourself by using little scripts like these to make using them a simple affair!
// tr_en_track
/*
Description:
Modified version of tr_en_skill_rank.
Checks search and Survival to determine success and displays floating text on success.
NOTE: You *MUST* use a unique tag for every object that uses this script.
Will do a skill rank test for the PC *ONCE ONLY*
The parameters below should be set as the variables of the object.
If the check succedes, the script will execute the script specified by the "script" variable on the object.
Multiplayer note: It also may be desirable to give the option to only fire once - ie destroy self afterwards.
Parameters:
int nRank = minimum rank to return TRUE
string sText = text to display as floating yellow text if check is successful
string sFail = text to display (if any) upon failure
int xpReward = amount of XP to give to the player as a reward for succeeding the check
Checks Search and Survival to determine success
*/
//AmstradHero - 16/05/08 : Modified to cope with multiple PCs and companions
#include "ginc_param_const"
void main()
{
object oPC = GetEnteringObject();
if(!GetIsPC(oPC))
return;
int nRank = GetLocalInt(OBJECT_SELF, "nRank");
object oOwner;
if (GetIsOwnedByPlayer(oPC))
{
oOwner = GetControlledCharacter(oPC);
}
else
{
oOwner = oPC;
}
int nDoOnce = GetLocalInt(oOwner, "DO_ONCE" + ObjectToString(OBJECT_SELF)); // a unique do-once
if(nDoOnce == 1)
return;
SetLocalInt(oOwner, "DO_ONCE" + ObjectToString(OBJECT_SELF), 1);
if ( (GetSkillRank(19, oOwner) >= nRank) || (GetSkillRank(24, oOwner) >= nRank) )
{
string sText = GetLocalString(OBJECT_SELF, "sText");
AssignCommand(oOwner, SpeakString(sText));
GiveXPToCreature(oPC, GetLocalInt(OBJECT_SELF, "xpReward"));
}
else
{
string sText = GetLocalString(OBJECT_SELF, "sFail");
if (sText != "")
{
AssignCommand(oOwner, SpeakString(sText));
}
}
}
4 comments:
Great post!
I'll admit I'm VERY paranoid about uising the toolset speak triggers -after i lost a load of data due to area corruption caused by them (could replicate it) - ever since I've been REALLY wary about using them! Think that boils down the the unique tag you mention but never seen a breakdown of why and what this applies to.
But you're right, there's some nice stuff that can be done with local variables and one core script - that's how my foraging alchemy/plant gathering works.
Yeah I'm still a little leary of them myself, but that issue hasn't jumped up to bite me since release, so I'm pretty sure they must have pinned it down.
That said...
AmstradHero, I forgot to add your blog to my listings, sorry about that. I missed it.
Speak triggers have seemed to me pretty stable for the last while. Haven't had bad experiences or heard of any lately, as we used to early on. May have something to do with the object variables' setting now automatically assigning a variable type value.
In any case, yes, getting values off the object is great and is a way as I see it to have "parameter-based-like" scripts outside of convos. If a script is "generic", that's the way to go IMO.
Glad to see people liked this post, and that I'm not the only person who loves locals. Plus, I really like being able to throw in those skill checks easily. :-)
Post a Comment