Game Development Community

Yack Pack with AFX

by Mark Dynna · 07/18/2008 (7:46 pm) · 13 comments

* Follow standard Yack Pack merge guide, except wherever the instructions show a file in "starter.fps" replace this with "arcane.fx"

* Skip the entire section under "Activation" in the Merge instructions, except for the NPC Name
control that is added to the PlayGui. (Even that can be considered optional, it's really up to you)

That covers all the base components that the Yack Pack needs to run. Normally the Yack Pack is activated by placing the Crosshair over an NPC and pressing "f" for "use." Since AFX is a mouse-driven interface, some changes need to be made to get the Yack Pack to activate based on a mouse click.

First, we'll want the cursor to change to a "speech bubble" when the user mouses over an NPC that can talk to them. First we need to define a new cursor for this.
* In client/ui/customProfiles.cs add the following (or put this in a different script if it suits you):
new GuiCursor(YackCursor)
{
   hotSpot = "1 1";
   renderOffset = "0.5 0.5";
   bitmapName = "./crossHair_talk";   
};

That will give us a new Cursor that is our "speech bubble." Next we need code to switch to this cursor when a "talkable" NPC is "rolled over" by the mouse. AFX provides a nice array of script callbacks, and there happens to be one ready-made for this situation:
* Add the following function to the top of client/scripts/afxSelectionMgr.cs:
function GameConnection::onObjectRollover(%this, %objectID)
{
   if(%objectID $= "" || !isObject(%objectID) ) {
      Canvas.setCursor(DefaultCursor);
      return;
   }
   
   %data = %objectID.getDataBlock();
   if( %data.canTalk == true && %objectID.isUseEnabled() ) {
      Canvas.setCursor(YackCursor);
   }
}
That code will make the cursor turn into a "speech bubble" when it is over a "talkable" NPC (determined by the "canTalk" flag and the isUseEnabled() method).

Now we actually need to have the guy start talking to you when you click on him. Again, AFX has a script call-back perfect for this situation.
* In client/scripts/afxSelectionMgr.cs, in the [GameConnection::onObjectSelected function, insert the following before the line TargetStatusbarLabel.setName(%obj.getShapeName()):
// Check to make sure we can talk to this object
  if( %obj.getDataBlock().canTalk !$= "" && %obj.getDataBlock().canTalk == true && %obj.isUseEnabled() ) {
     // We're ok to talk, so let's tell the server we want to start talking
     commandToServer( 'YackStart', %obj.getGhostID() );
     return;
  }

To avoid some annoying script errors, you should get rid of the reference to CrossHair in the Yack Pack scripts.
* In client/scripts/yackGui.cs, in YackGUi::onSleep comment out this
line:
CrossHair.setVisible(true);

Finally, the Yack Pack has a strange interaction with AFX's system of "highlighting" a selected NPC (making them brighter). After talking to the NPC it will be permanently "highlighted" forever. To fix this issue, insert this code:
* In client/scripts/yackGui.cs, in clientCmdYackEnd() add the following
line to the end of the function:
ServerConnection.clearSelectedObj(false);

How to test: Assign the dialogue fields to the DemoPlayer datablock as specified in the Yack Pack instructions. Then run and try to catch that guy who's running around, or place a new (non-moving) NPC to test your conversation.

That's it! Everything from there on is stock Yack Pack or AFX code.

NOTE: Something you may want to fix in your code... the Yack Pack determines that the player can talk to an NPC through the canTalk flag on the datablock and through the isUseEnabled method (as defaulted above). AFX, by default, doesn't modify any of these values in some situations where it probably should. For example, when the Orc dies you'll probably want to call setUseEnabled(false) to avoid being able to talk to dead Orcs. Enjoy!

#1
07/18/2008 (8:56 pm)
Great info, thank you!

I just merged the yack pack into TGEA 1.7.1 with AFXA yesterday, so this came just in the right time! Lucky me.
#2
07/18/2008 (10:31 pm)
Woot! Great Contribution Mark, thanks!
#3
07/20/2008 (7:03 pm)
As mentioned earlier -great job Mark

And as I had some spare time today, I thought "hey why not try get this baby up running for the fun of it". I so love code, but as this brought more 'fun' than I had time for, anyways, was nice to chat a bit in Irc Mark ;)

Here goes!
Quote:* Skip the entire section under "Activation" in the Merge instructions,
This was a bit confusing, as the installation/merge guide seemingly does not have these 'headers'

Followed the rest of the sugs, and lastly added this one (now it was getting late) to the mission file.

new TSStatic() {
      canSaveDynamicFields = "1";
      position = "377.628 342.049 218.531";
      rotation = "0 0 1 194.806";
      scale = "1 1 1";
      shapeName = "~/data/shapes/player/orc_warrior.dts";
      receiveSunLight = "1";
      receiveLMLighting = "1";
      useAdaptiveSelfIllumination = "0";
      useCustomAmbientLighting = "0";
      customAmbientSelfIllumination = "0";
      customAmbientLighting = "0 0 0 1";
      useLightingOcclusion = "1";
      canTalk = "True";
      dialogImg = "~/data/shapes/player/Kork_Portrait.png";
      dialogScriptFile = "~/data/shapes/yack_examples/Stanley_Sunshine.dls";
      dialogScriptName = "Stanley_Sunshine_Dialogue";
      name = "Bent";
   };

All compiling went smooth, all startup went smooth, walking to the model went smooth

-clicking it had no effect!

(I might have made an error due to the unclear 'skip these steps', or in the model definition. Ill try get back to this again soon, unless someone else beats me to it.
#4
07/27/2008 (10:43 pm)
EDIT: I need to point out that I am using TGEA 1.7.1.

@Christian S: I have the same result as you. I suspect it is the GameConnection::onObjectRollover or the GameConnection::onObjectSelected but I haven't had time to do more debugging on the matter. I will post an update when I get a chance to look into this more.

EDIT: I need to point out that I am using TGEA 1.7.1.
#5
07/27/2008 (11:40 pm)
The Yack Pack code doesn't allow talking to non-ShapeBase objects. You'll need to define a datablock and place the canTalk, dialogScriptFile, and dialogScriptName lines there.
#6
07/28/2008 (8:09 am)
You might want to also check out the original discussion on the forums.
#7
07/28/2008 (8:16 am)
EDIT: I need to point out that I am using TGEA 1.7.1.

@Mark and Konrad: I am talking to a DemoPlayer that I placed on the mission file. My Datablock looks like this:

datablock PlayerData(HumanMaleVillagerBAI : HumanMaleVillagerBAvatar) {
   shootingDelay = 100;
   
   //START YACK
   canTalk = true;

   dialogScriptName = "Margold_Pegason";
   dialogScriptFile = "server/dialogue/Villager_B.dls";
   dialogImg = "data/ui/portrait/Villager_B.png";
   name = "Margold Pegason";
   //END YACK
}; //datablock PlayerData

I have now established the my onObjectRollover and onObjectSelected is definitely running but it seems to be failing on the: %obj.isUseEnabled() step of the process.

Unlike Christian S ... I am definitely using a ShapeBase object.

The code block above uses "datablock PlayerData(HumanMaleVillagerBAvatar : HumanMaleAvatar)" which in turn uses "datablock PlayerData(HumanMaleAvatar)" ... I have extended the DemoPlayer datablock and replaced it with the code block above.

When I do Echo checks in the script I can see that my onObjectSelected and onObjectRollover definitely do fire ... but not the part that actually starts the "Yacking" ... I am now looking into this.

EDIT: I have just double checked my merge guide and everything to do with isUseEnabled is definitely in the Engine Code (i.e. shapeBase.h and shapeBase.cpp).

EDIT: I have run a few more tests now and here are my findings thusfar.

In my "function GameConnection::onObjectRollover(%this, %objectID)" I added this code:
echo("YACK PACK - ON ROLLOVER");

   echo("YACK PACK - " @ %objectID);
   
   echo("YACK PACK - CAN TALK = " @ %data.canTalk);
   echo("YACK PACK - CAN USE = " @ %objectID.isUseEnabled());

Below is the output from the console:
YACK PACK - ON ROLLOVER
YACK PACK - 6779
YACK PACK - CAN TALK = 
YACK PACK - CAN USE = 1

In my "function GameConnection::onObjectSelected(%this, %obj)" I added this code:
echo("YACK PACK - OBJECT SELECTED");   

   echo("YACK PACK - CAN TALK = " @ %obj.getDataBlock().canTalk);
   echo("YACK PACK - CAN USE = " @ %obj.isUseEnabled());

Below is the output from the console:
YACK PACK - OBJECT SELECTED
YACK PACK - CAN TALK = 
YACK PACK - CAN USE = 1

In both instances my "CAN TALK" seems to be blank. I tried putting the canTalk option in the DataBlock as both:
canTalk = "true";
and:
canTalk = true;

So I seem to have narrowed down my problem but I am not sure how to fix this. The canTalk is definitely the problem ... if I remove the checkmy Dialogue pops up and the Yacking commences. :)

EDIT: I need to point out that I am using TGEA 1.7.1.

EDIT: I got it working now but only in Local Server mode ... when connecting to a server it still doesn't work. I suspect it is something with the %objectid variable ... this is fun. :)
#8
07/28/2008 (10:59 am)
"this is fun. :)"

He he, it sure is. Well following Marks original code, and skipping step 16,17 & 5 in the Yack merge guide everything works fine... Both with Aimanaged mobs and physically ones in the .mis

Except placing the yack stuff in the constructor makes every instance have the same file, name, pic, etc.. I tried to look closer at the AIplayer file and the AIManager. Beats me why the stuff is hardcoded 1 normal and 1 dead orc, (and whatever else you would like to add in) instead of having only 1 instance spawner routine catching it all as variables.

Even though I am hard pressed on time, I will try a rewrite of that AI file and the AIManager so it handles canTalk, %path, %Roamingtype, etc... in a more neat way, allowing for characters to be listed in a sort of array and then parsed through kinda like this (n ; n
Hopefully I will find a hole to work on it this week!
#9
07/28/2008 (11:03 am)
@Christian S ... can you confirm that this works in networked mode as well. When I run a network server and connect to it, mine doesn't work. I am in the process of going through my merge guide again. :)

EDIT:
Ok ... I can confirm that this doesn't work in a Networked Version of the game (i.e. connecting to a dedicated server).

Here is an output of my log in local mode:
YACK PACK - ON ROLLOVER
YACK PACK - 9030
YACK PACK - CAN TALK = 1
YACK PACK - CAN USE = 1

YACK PACK - ON SELECTED
YACK PACK - 9030
YACK PACK - CAN TALK = 1
YACK PACK - CAN USE = 1

As you can see ... unlike the above output ... the CAN TALK is now = 1 and it fires off correctly. I have tested this with both dedicated server while connecting to it from a client and by running one copy of the game as a local server and connecting to it from anotehr copy. In the second instance the local client can use the Dialogues and chat to the NPC but the networked client can't.

I suspect this is something to do with the way datablocks are transmitted to the clients or such ... still need to look into it some more.
#10
08/03/2008 (12:41 am)
I'm using TGEA 1.7.1 also, and I've got this working on a local server (which is fine for me).

But I'm seeing some oddities I'd like to address, perhaps someone has already done so and can set me on the right track.

1. I can talk to someone who is on the other side of the world. Is there anyway to check the distance between the player and the aiplayer, and ignore the click if it's too far away?

2. After ending the conversation, the aiplayer is frozen in place. Is it possible to start them back up, get them back on their paths?
#11
08/03/2008 (3:44 pm)
Grrr, GG's website crapped on my post. Let's try this again.

Jaimi, on the first issue, you can grab the VectorDist between player and selected object in GameConnection::onObjectRollover().

There's also a $pref::AFX::targetSelectionRange preference that governs how far into the world your click-selection will reach.

One or both of those might help.

On the second issue, I haven't found a solution yet, but I plan to dive into that later this week. I'll let you know if I turn up anything useful.
#12
08/03/2008 (4:57 pm)
Hi John -

I just ended up hacking around these issues -- This is what I ended up doing in GameConnection::OnObjectSelected, but I'll check into the targetSelectionRange, that might be a better answer.

// Check to make sure we can talk to this object
  if( %obj.getDataBlock().canTalk !$= "" && %obj.getDataBlock().canTalk == true && %obj.isUseEnabled() ) 
  {
	   %targetpos = %obj.getposition();
      %player = LocalClientConnection.getControlObject();
      if (%player.getClassName() $= "Player")
      {	  
		   %playpos = %player.getposition();
		   %dist = vectorDist(%playpos, %targetpos);
		   if (%dist <= 12.0) // hard coded.
		   {
            // We're ok to talk, so let's tell the server we want to start talking
            commandToServer( 'YackStart', %obj.getGhostID() );
            return;
		   }
      }
      return;
   }

For the other issue, I found what is causing the problem, and have solved it a goofy way. First to put them back on track, I added the following to aiPlayer.cpp:

void AIPlayer::restoreState()
{
    if (mMoveState_saved != -1)
    {
      mMoveState = (MoveState) mMoveState_saved;
      mMoveState_saved = -1;
	  pickActionAnimation();
    }
}

ConsoleMethod( AIPlayer, saveState, void, 2, 2, "()"
              "saves the state so you can restore it later.")
{
   object->saveMoveState();
}

ConsoleMethod( AIPlayer, restoreState, void, 2, 2, "()"
              "saves the state so you can restore it later.")
{
	object->restoreState();
}

Then, I saved and restored the state in Yack.cs in these functions:

function AIPlayer::onDialogStart(%this, %dialog, %actors)
{
    Player::onDialogStart(%this, %dialog, %actors);
    %this.saveState();
    %this.stop();
    %this.enableUse(false);
}

function AIPlayer::onDialogEnd(%this)
{   
    Player::onDialogEnd(%this);
    %this.clearAim();
    %this.restoreState();
    %this.enableUse(true);
}

Now, this has left some strange problems that I haven't been able to fix - the animation gets messed up, and though you call the PickActionAnimation in restoreState(), the aiPlayer will start "skating" in places (but not always! he'll skate, then walk, then skate, etc). To work around this, I had to comment out the following line in doPhrase() in Yack.cs:

if(%dialog.curPhrase.talkSequence !$= "")
	{
	   // JRM - This messes up the animation when they go back and start walking again...
 	   // %dialog.talkingActor.playThread($Pref::Server::Yack::TalkAnimThreadIdx, %dialog.curPhrase.talkSequence);
	}

This is obviously a hack - it disables any talk sequences you might want to play. Hopefully someone can figure out how to fix this permanently without messing this up.

This has left another big problem with the camera - once you exit the dialog, you can no longer switch between first and third person, or use the mouse wheel. To get around this, I commented out all of the camera switching code in OnDialogStart and OnDialogEnd. This fixes it, but again disables functionality (ability to set different cameras).
#13
08/07/2008 (2:40 am)
Quote:I suspect this is something to do with the way datablocks are transmitted to the clients or such ... still need to look into it some more.
Bingo, I see the problem now. The Yack Pack checks the "canTalk" field on the datablock to determine if the NPC can be selected to talk. This field was added to the datablock purely in script (a "dynamic" field), these fields are NOT automatically networked to the Client. So, we'll need to add some code in PlayerData. First, add the variable declaration somewhere in the PUBLIC section of the PlayerData header file (Player.h):
bool mCanTalk;
Then initialize it in the PlayerData constructor (PlayerData::PlayerData() ):
mCanTalk = false;
Then expose it to the console by adding this to PlayerData::initPersistFields:
addField("canTalk", TypeBool, Offset(mCanTalk, PlayerData));
Now, the crucial part, networking the field. We need to write it into the bitstream in the packData function. Note that the order that we read/write fields in the BitStream is super DUPER important, so I recommend sticking this right at the top of PlayerData::packData, like this:
void PlayerData::packData(BitStream* stream)
{
   Parent::packData(stream);
   stream->writeFlag(mCanTalk);
   ...
Finally, we read the value on the other side of the Bitstream in unpackData:
void PlayerData::unpackData(BitStream* stream)
{
   Parent::unpackData(stream);
   mCanTalk = stream->readFlag();
   ...