Sunday, May 4, 2008

Toolset Tidbit: Managing Multiple Meetings with NPCs

I've recently decided that I'm going to change the blog style a little from mere progress updates and teasers to offering little fragments of design and tips and tricks that I find help me and improve my work while I'm doing modding. Rest assured that I won't fail to post updates on how the mod is progressing (and you can expect one tomorrow!), but I thought I'd like to offer a bit more information to fellow builders in terms of things that I find useful. If there's enough enthusiasm, I might even end up collating all my useful scripts and blueprints into a prefab module for people to download. On this note, I'll discuss a set of scripts/triggers that I've found useful to have. I'll first describe the scenario, and then the solution.

Scenario
The player has a conversation with an NPC, at the end of which, the NPC leaves the area in which they are talking. The NPC leaves the area and moves to another area in the module, where the player will meet them later. The main issue here is the relevance of the NPC's dialogue - as the NPC's dialogue is different upon meeting them initially, and then changes once they meet the player for the second time.

Possible Solutions
The first option is create a separate blueprint for the NPC. Create a conversation that has the initial conversation as a "show once" starting node, then a sequence of barkstrings as they are leaving, in case the player catches up with them as they are walking/running away. These nodes also need a ga_move script attached so that the NPC keeps moving to its exit destination. Upon the ending node of the initial conversation, have a ga_create_object script call that creates a new instance of this blueprint at the desired location that PC will meet the NPC later, and assign it a new conversation.

The second option is to jump the NPC to a destination location, and use a local variable to keep track of its state. This is particularly handy if you're trying to maintain the state of the NPC in question. What do I mean by this? Imagine your NPC has an item that the PC can pickpocket off him/her. Your blueprint is likely going to have this object as part of its inventory. If the PC pickpockets the NPC the first they meet them, the item is gone, but when you recreate the NPC at the new location using ga_create_object, it will magically have another one of said item. If you jump the NPC, it maintains its state. There are other examples as well, but I think I've illustrated the point.

So, basically, we need a similar sort of dialogue set up, with a ga_move call at the end of the initial conversation and a sequence of barkstrings. However, we need a few extra things. Firstly, we need to create a trigger that will jump the NPC to a new destination when it gets to the exit. So place a trigger around the destination waypoint, and then use the following script for the onEnter action of the trigger.

//tr_en_jump_npc
/*
Description:
Jumps an object (typically an NPC) with a specified tag to a specific waypoint/object when it hits the trigger.

Local Trigger Variables:
sJumper - tag (or a substring in the tag) of the object to jump to the selected waypoint
sJumpDest - tag of the waypoint/object to jump the object to
bMultiUse - If this is set to 0, the trigger will be destroyed after the first use
Set this variable to 1 if you want the trigger to operate on multiple objects with the same tag

AmstradHero - 22/04/08
*/
void main()
{
object oPC = GetEnteringObject();
//Get the string first to avoid any weirdness
string sJump = GetLocalString( OBJECT_SELF, "sJumper");

//Use FindSubString because the StringCompare compare was giving inconsistent results
//Substring also allows us to jump multiple NPCs while also allowing for previous/later granularity in command assignment
if ( FindSubString( sJump, GetTag(oPC) ) != -1 )
{
//Grab the object because we've got the right one
object dest = GetObjectByTag( GetLocalString( OBJECT_SELF, "sJumpDest"));

//Jump it there
AssignCommand( oPC, ClearAllActions());
AssignCommand( oPC, JumpToObject( dest ));
//Now set its rotation to match the waypoint
DelayCommand(4.0f, AssignCommand( oPC, SetFacing( 180.0-GetFacing( dest ) ) ) );

//Destroy the trigger if it's not designed for multiple uses
if (GetLocalInt(OBJECT_SELF, "bMultiUse") == 0)
{
DestroyObject(OBJECT_SELF, 0.5f);
}
}
}

So all you need do is create the relevant variables on the trigger. So for argument's sake, let's say we have a NPC with the tag c_thief, who moves to a waypoint wp_thief_escapes, which when he leaves the current area will jump to a destination wp_thief_hideout in another area within the module. So we have the wp_thief_escapes waypoint inside this trigger, which has the variable sJumper = "c_thief" and sJumpDest = "wp_thief_hideout". Note that you could potentially set nUseMult to 1 if you wanted, but in many cases we don't need it.

So now we are good to go, right? Unfortunately no, because we need to set the NPC to talk differently upon reaching its destination. So switch to the destination area, and create another trigger around the jump destination waypoint that the NPC moves to. So we create a trigger aorund our waypoint wp_thief_hideout. and use the following script for it onEnter.

//tr_en_set_npc_int
/*
Description:
Sets an integer variable on an object (typically an NPC) with a specified tag to a specific value upon entering

Local Trigger Variables:
sNPC - tag (or a substring in the tag) of the object to jump to the selected waypoint
sVarName - name of the variable to set
nValue - value to set the variable to
bMultiUse - If this is set to 0, the trigger will be destroyed after the first use
Set this variable to 1 if you want the trigger to operate on multiple objects with the same tag

AmstradHero - 22/04/08
*/
void main()
{
object oPC = GetEnteringObject();
//Get the string first to avoid any weirdness
string sNPC = GetLocalString( OBJECT_SELF, "sNPC");

//Use FindSubString because the StringCompare compare was giving inconsistent results
//Substring also allows us to jump multiple NPCs while also allowing for previous/later granularity in command assignment
if ( FindSubString( sNPC, GetTag(oPC) ) != -1 )
{
//Grab the object because we've got the right one
SetLocalInt(oPC, GetLocalString(OBJECT_SELF, "sVarName"), GetLocalInt(OBJECT_SELF, "nValue"));

//Destroy the trigger if it's not designed for multiple uses
if (GetLocalInt(OBJECT_SELF, "bMultiUse") == 0)
{
DestroyObject(OBJECT_SELF, 0.5f);
}
}
}

Again, create the relevant variables on the trigger. Where sNPC = "c_thief" and we're going to use sVarName = "talkState" and nVarValue = "1". Now comes the final step of creating our next dialog chain above our old one, with the initial condition of gc_local_int - with the parameters "talkState" and "1". And voila! We now have an NPC that we can move around.

Even better, we can simply repeat the steps in order (increasing the value talkState again) to have the NPC move yet again if desired... As an example, perhaps you are pursuing an enemy through a maze. At different stages during the chase, you have a small exchange, then can deal a small amount of damage to the enemy before they leave. Of course, you want to keep track of any items/abilities the NPC has used and any damage they have taken along the way - and these two triggers/scripts allow you to do that as well as creating a series of taunting conversations at each stage. You could also use either trigger in isolation for various purposes too.

While I've used the second option in some cases where it's not necessary in my module, I like the approach because I've got that consistency option there if I want to use it, and also, I get to keep a single conversation for the same NPC, rather than the first option which means having multiple different conversations used for the "same" NPC (at least it the same one as far as the player is concerned, even though might know better).

I hope that someone finds this a useful tidbit that they might be able to use. Any feedback on it or whether you like the idea of more posts like this would be appreciated!

No comments: