Author: beffy (c++programmer)

Here is a quick introduction to the Torque Conversation Editor, TGEConEd.

UPDATE 12/01/2002:
I forgot to mention the loading function for the conversation files. Make sure you follow this step!

The Conversation Editor is an in-game tool for creating and editing simple single-player NPC dialogs.

Some basic / new features:

New in v1.0:

This is how the conversations basically work... you've got one sequence per conversation file, and they are linked by either the TConNext fields for default sequences OR TConTarget[] entries for multiple choice sequences...


 
To end a dialog, put the string "end" there as value, like this:


In-game use

To talk to a NPC, add your AIPlayer to the game, and, depending on your "conversation trigger" set in the dialog sequences, switch to "object selection" mode (default ALT-M) and click on the bot ("onMouseClick") OR move close enough near the bot ("onEnterFOV"), and the GUI will pop up (if the bot's name matches your "TConParticipant1" entry in your dialog). You can either use the mouse and click on the buttons, or you can hit "enter" to continue in the current sequence, hit "esc" to close the dialog gui, use your up- and down-arrows to scroll through long dialogs and use "page-up" and "page-down" to scroll to the beginning or end of the current dialog sequence, respectively. If an NPC doesn't have to say anything to you (anymore), he will tell you to go away ;)

What happens (as a little, stupid demo...;) when you start a game and add a bot is that the bot/NPC spawns and runs to the player. When he is within a distance of 8 to the player, the dialog starts.
After the 3rd dialog is finished, "Bot0" will tell you: "Hm, I guess you have to do something else first...", which means that the 4th sequence won't play before a certain goal (objective) is reached. For your convenience, this goal is reached automatically after 15 seconds ;-) ... (this is achieved with $testSchedule = schedule(15000,0,setCondition,"magicStoneFound","true"); in function gotoNextSequence(%label) in "MultipleChoiceDialog.gui"). After these 15 secondes he will continue with the dialog if you select him.
If you spawn a second bot, "Bot1", he will also run to the player, but since his only dialog sequence is triggered "onMouseClick", you have to hit ALT-M and click on him to make him talk.

IMPORTANT:

The first sequence for EACH NPC who should be "talking" has to be labeled "BOTNAME_seq01", e.g. "Bot0_seq01", "Bot1_seq01", "BillyBoy_seq01", etc.!!! All the other sequences can be labeled however you want, e.g. "Ork01_askForFood" or whatever... ;)


 
Actions:

for multiple-choice dialogs, it is possible to specify custom functions as "targets", so that if the player selects that specific answer/reaction, a function is called. The syntax is:

  

Action: myFunction param1,param2,param3
e.g.
TConTarget[2] = "Action: teleportNPCRandom $currNPC";

 
Of course, the function must exist, and the number of parameters you specify here must match the function definition.
 

Special tags:

You can use the tags and in all your dialog text entries, they will be replaced with the current player and NPC/bot name in the dialog guis. You can also use special link tags: <a:Link seq03>jump somewhere else</a> changes the complete sequence <a:Explanation detail05>explain this further</a> pops up another sub-sequence (specified by the label "detail5") , e.g. with some detailed explanation in the upper right corner of the dialog gui


 
Adding objectives:

The basic demonstration objectives are located at the end of the file "MultipleChoiceConversationHud.gui" at the moment, the following represents one objective:
  

$playerObjectiveName[$playerObjectiveCounter] = "magicStoneFound";
$playerObjectiveValue[$playerObjectiveCounter] = "false";
$playerObjectiveCounter++;

 
You can add your own objectives here, of course you have to add some logic to change the status in-game, etc.


 

For further instructions on how to use the editor, please check out the help file that is provided with in the download.

Installation / setup

Ok, now here is the step-by-step howto (the same as in the readme file provided with the download)...
ATTENTION:
PLEASE MAKE A BACKUP OF YOUR EXAMPLE (AND EVENTUALLY OF YOUR ENGINE FOLDER, TOO)!!!
If you want, you can just copy the files from the zip into your Torque directory and replace some of yours with mine then, e.g. aiPlayer.cs. Then you will have a lot less work and you get rid of a lot of file adding/editing... In any case, read this document carefully... :D

Okay, first of all, YOU NEED THE HEAD VERSION WITH THE NEW AIPLAYER CLASS and you have to implement "object selection" into the engine. To do this, follow this tutorial: http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=2173


After that, you have to get bots/NPCs into Torque, of course if you don't have them already - you can see how to do it here: http://tork.zenkel.com/tutorials/bots/adding_bots.php

Here is how I am adding bots for test purposes: (the bot is added, its conversations are set to the default value ("BOTNAME_seq01") and he moves directly to the player...) I've added a function to initialize the conversations (which you need in any case!!) for each NPC which looks like this: Of course, the first dialog/sequence file for each NPC must have the label "<NPC_NAME>_seq01"!

So, place this in "server/scripts/commands.cs":
  

//--------------------------------------------------------------------
// init the bot's initial conversation sequence to "%npcName_seq1"
//--------------------------------------------------------------------
function initializeSequences(%client, %npcName)
{
	$NPCS = %client;
	$currNPC = %npcName;
	$NPCS.nextSequence[$currNPC] = %npcName @ "_seq01";
	error("INIT NPC:" SPC %npcName SPC $NPCS.nextSequence[%npcName]);
}

$botCounter = 0;
function serverCmdAddBot(%client) {
         MissionCleanup.add($bots);
	 %npcName = "Bot" @ $botCounter;
         $bots[$botCounter] = AIPlayer::spawnPlayer(%npcName);
         MissionCleanup.add($bots[$botCounter]);
	 
	 // init the conversations for this NPC
	 // NOTE: the first conversation must have the label: BOTNAME_seq01
	 initializeSequences(%client, %npcName);
	 
         serverCmdMoveBotToPlayer(%client,$bots[$botCounter]);
         $botCounter++;
}
function serverCmdMoveBotToPlayer(%client,%bot)
{
   if (isObject(%client.player) && isObject(%bot))
   {
	%bot.setAimLocation( %client.player.getPosition() );
	%bot.setMoveDestination( %client.player.getPosition() );
	echo("Moving to current position of the player!");
   }
}

function serverCmdMoveBotsToPlayer(%client)
{
   if (isObject(%client.player))
   {
         $daPlaya = %client.player;
         for(%i = 0; %i < $botCounter; %i++)
         {
		if(isObject($bots[%i])){
			$bots[%i].setAimLocation( %client.player.getPosition() );
			$bots[%i].setMoveDestination( %client.player.getPosition() );
			echo("Moving to current position of the player!");
		}
         }
   }
}
function serverCmdMoveBotsRandom(%client) {
	for(%i = 0; %i < $botCounter; %i++) {
		if(isObject($bots[%i])){
			%dest = getRandom(250) SPC getRandom(350) SPC getRandom(350);
			$bots[%i].setMoveSpeed(0.6);
			error("Bot speed:" SPC $bots[%i].getMoveSpeed());
			$bots[%i].setAimLocation(%dest);
			$bots[%i].setMoveDestination(%dest);
			echo("Bot" @ %i SPC "(" @ $bots[%i].getShapeName() @ ")" SPC "is moving randomly now!");
		}
	}
}

// add this to default.bind.cs

function addBot(%val) 
{
	if(%val)
	{
		commandToServer('AddBot');
	}
}
MoveMap.bind(keyboard, "ctrl b", addBot);

 
// and this also to client/config.cs
  

MoveMap.bind(keyboard, "ctrl b", addBot);

 

To toggle the editor, add this to "client/config.cs" and also to "client/scripts/default.bind.cs":
  

moveMap.bind(keyboard, "ctrl c", toggleConversationEditor);

 


 
Back to "server/scripts/commands.cs", replace the object selection function you've added before with this:
  

$currentDialogNum = 1;
$currentDialogId = 0;
$continueDialogs = true;
$proceedWithDialogs = true;
$addHistory = true;
function serverCmdSelectObject(%client, %mouseVec, %cameraPoint)
{
   //Determine how far should the picking ray extend into the world?
   %selectRange = 200;
   // scale mouseVec to the range the player is able to select with mouse
   %mouseScaled = VectorScale(%mouseVec, %selectRange);
   // cameraPoint = the world position of the camera
   // rangeEnd = camera point + length of selectable range
   %rangeEnd = VectorAdd(%cameraPoint, %mouseScaled);

   // Search for anything that is selectable below are some examples
   %searchMasks = $TypeMasks::PlayerObjectType | $TypeMasks::CorpseObjectType |
     				$TypeMasks::ItemObjectType | $TypeMasks::TriggerObjectType;

   // Search for objects within the range that fit the masks above
   // If we are in first person mode, we make sure player is not selectable by setting fourth parameter (exempt
   // from collisions) when calling ContainerRayCast
   %player = %client.player;
   if ($firstPerson)
   {
	  %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks, %player);
   }
   else //3rd person - player is selectable in this case
   {
	  %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks);
   }

   // a target in range was found so select it
   if (%scanTarg)
   {
      %targetObject = firstWord(%scanTarg);
      %client.setSelectedObj(%targetObject);
      // if it is a NPC, check if he has something to say...
      if(%targetObject.getClassName() $= "AIPlayer")
      {
         %npcName = %targetObject.getShapeName();
         $currNPC = %npcName;
         // now search conversation sequences in the RootGroup
	 %dataGroup = "RootGroup";
         for(%i = 0; %i < %dataGroup.getCount(); %i++)
         {
            %obj = %dataGroup.getObject(%i);
            if(%obj.getClassName() !$= "TGEConSequence")
            {
               continue;
            }
            if(isObject(%obj))
            {
	       if(%npcName $= %obj.TConParticipant1 && %obj.TConLabel $= $NPCS.nextSequence[%npcName] 
		   		&& %obj.TConTriggerType $= "onMouseClick" )
               {

		  // check if there is a condition and if so, if it is true
		  // remove it if you dont want the objective tests
		  if(%obj.TConCondition !$= "")
		  {
			  if(checkCondition(%obj.TConCondition) $= "false")
			  {
				  Canvas.pushDialog( MainConversationHud );
				  TGETextField.setText("Hm, I guess you have to do something else first...");
				  $proceedWithDialogs = false;
				  $addHistory = false;
				  return;
			  }
		  }
		  // end check
		       
		  $currentPlayingDialog = %obj;
		  $addHistory = true;
		  $NPCS.nextSequence[$currNPC] = $currentPlayingDialog.TConNext;
		  $continueDialogs = true;
		  if(%obj.getTConType() $= "DefaultSequence")
		  {
			  $tgeTextCount = 1;
			  Canvas.pushDialog( MainConversationHud );
			  TGETextField.setText("");
			  TGETextField.addText(replaceConversationNames(%obj.TConText[0]) @ "\n", true);
			  %currentSoundFile = %obj.TConAudioFile[0];
			  playConversationSound(%currentSoundFile);
			  break;
		  }
		  else if(%obj.getTConType() $= "MultipleChoiceSequence")
		  {
			  Canvas.pushDialog( MultipleChoiceConversationHud );
			  MultipleChoiceTGETextField.setText("");
			  MultipleChoiceTGETextField.addText(replaceConversationNames(%obj.TConText[0]) @ "\n", true);
			  %currentSoundFile = %obj.TConAudioFile[0];
			  playConversationSound(%currentSoundFile);
			  %answers = %obj.TConCounter;
			  for(%a=0; %a < %answers; %a++)
			  {
				  $targetArray[%a] = %obj.TConTarget[%a];
				  MultipleChoiceAnswerList.addRow(%a, %obj.TConAnswer[%a]);
			  }
			  break;
		  }
               }
	       // if it is a bot who doesnt have any dialogs or a bot who is finished talking to you...
	       else if(%obj.TConLabel !$= $NPCS.nextSequence[%npcName] || %npcName !$= %obj.TConParticipant1)
	       {
		  $continueDialogs = false;
		  Canvas.pushDialog( MainConversationHud );
                  TGETextField.setText("Leave me alone!\n");
	       }
            }
         }
      }
   }
}

 
IMPORTANT:

One again, the first sequence for EACH NPC who should be "talking" has to be labeled "BOTNAME_seq01", e.g. "Bot0_seq01", "Bot1_seq01", "BillyBoy_seq01", etc.!!! All the other sequences can be labeled however you want, e.g. "Ork01_askForFood" or whatever... ;)


To load all the dialogs, add this function to "client/init.cs":
  

function loadAllDialogues()
{
   %path = $TGEConEd::SaveFilePath @ "*.cs";
   for(%file = findFirstFile(%path); %file !$= ""; %file = findNextFile(%path))
   {
      if (isFile(%file))
      {
         echo("---> Found TGEConEdFile:" SPC %file);
         exec(%file);
      }
   }
}

 

and add these two lines in the function initClient() ( I've added it right after loadMainMenu(); ):
  

   $TGEConEd::SaveFilePath = "common/editor/dialogues/";
   loadAllDialogues();

 
Then, create the following folder(s) for TGEConEd:
- common/editor/dialogues
- engine/game/ai/conversation
and put tgeConEd.h and tgeConEd.cc into the latter.
Add the folder engine/game/ai/conversation and these two source files to your VStudio (or whatever compiler you are using) project, also!!

Sound:

TGEConEdGui.cs searches for a folder named "conversations" in your game directory and finds all wav files in there. If you want to use the provided example dialogs or audio files in general, create this folder:
- fps/data/conversations/audio
I've included a folder data/conversations/audio/mission1 with 3 wav test files in the zip file you've downloaded.


NPC Images:

Add images for all your NPCs to this folder:
- fps/data/conversations/images/Bot0.jpg
- fps/data/conversations/images/Bot1.jpg
...
They must be named <NPCNAME>.jpg or .png!

After that, add these files to your engine:
- common/editor/TGEConEdBrowser.gui
- common/editor/TGEConEdGui.cs
- common/editor/TGEConEdGui.gui
- common/editor/TGEConEdStartup.cs
- common/editor/coned_logo1.png
- common/help/12. Conversation Editor.hfl
(to access that help file, simply open the standard help window in-game with F1)
- fps/client/ui/MainConversationHud.gui
- fps/client/ui/MultipleChoiceConversationHud.gui
- fps/client/ui/coned_dn.png
- fps/client/ui/coned_up.png
- fps/client/ui/coned_bg2.png
- fps/server/scripts/aiPlayer.cs (I've added a couple of basic functions that are used to demonstrate some very basic "objective" stuff
 
I've also included some stupid test dialogs in the "dialogues" folder in the zip file... put them into "common/editor/dialogues" if you want to use them as examples...

Then, add these lines to client/init.cs
  

   exec("./ui/MainConversationHud.gui");
   exec("./ui/MultipleChoiceConversationHud.gui");

 
and also add
  

   exec("./editor/TGEConEdStartup.cs");

 
in commom/main.cs, (at the end of the function initBaseClient()).

The "dialog history" feature requires these gui elements in "client/ui/playGui.gui":
  

   new GuiScrollCtrl(HistoryScrollCtrl) {
      profile = "ConversationScrollProfile";
      horizSizing = "left";
      vertSizing = "top";
      position = "88 438";
      extent = "291 33";
      minExtent = "8 8";
      visible = "1";
      helpTag = "0";
      willFirstRespond = "1";
      hScrollBar = "alwaysOff";
      vScrollBar = "alwaysOff";
      constantThumbHeight = "0";
      childMargin = "1 1";

      new GuiMLTextCtrl(HistoryTextField) {
         profile = "ConversationHudMessageProfile";
         horizSizing = "center";
         vertSizing = "center";
         position = "1 1";
         extent = "286 105";
         minExtent = "8 8";
         visible = "1";
         helpTag = "0";
         lineSpacing = "3";
         allowColorChars = "1";
         maxChars = "-1";
      };
   };
   new GuiBitmapCtrl(HistoryUpArrow) {
      profile = "GuiDefaultProfile";
      horizSizing = "left";
      vertSizing = "top";
      position = "378 441";
      extent = "12 12";
      minExtent = "8 8";
      visible = "1";
      helpTag = "0";
      lockMouse = "0";
      bitmap = "./coned_up";
      wrap = "0";
   };
   new GuiBitmapCtrl(HistoryDownArrow) {
      profile = "GuiDefaultProfile";
      horizSizing = "left";
      vertSizing = "top";
      position = "378 462";
      extent = "12 12";
      minExtent = "8 8";
      visible = "1";
      helpTag = "0";
      lockMouse = "0";
      bitmap = "./coned_dn";
      wrap = "0";
   };

 
Then, open your VStudio project, go to gui/guiScrollCtrl.cc and add this:
  

// beffy: added for TGEConEd
ConsoleMethod(GuiScrollCtrl, scrollDelta, void, 4, 4, "(S32 deltaX, S32 deltaY) - scrolls the scroll control about the delta values.")
{
   argc; argv;
	GuiScrollCtrl* control = static_cast<GuiScrollCtrl*>( object );
	control->scrollDelta(dAtoi(argv[2]), dAtoi(argv[3]));
}

 
I've added it right before
  

void GuiScrollCtrl::initPersistFields()
{
...

 

After that, open gui/guiBitmapCtrl.h and add this on top:
  

#ifndef _GUIMOUSEEVENTCTRL_H_
#include "gui/guiMouseEventCtrl.h"
#endif

 
and also change
  

class GuiBitmapCtrl : public GuiCtrl
{
private:
	typedef GuiCtrl Parent;

 
to
  

class GuiBitmapCtrl : public GuiMouseEventCtrl
{
private:
	typedef GuiMouseEventCtrl Parent;

 
so that your gui bitmaps trigger mouseevents and you can use bitmap arrows for scrolling...

Finally, replace (or change) your engine/game/aiPlayer.* files with the ones provided in the zip (I've addes some functions)..

And now, do a CLEAN/BUILD or REBUILD ALL ... (shouldn't be necessary, but you never know... ;)
Once in game, you can trigger the object selection with ALT-M by default and toggle the TGEConEd with STRG-C.

Further suggestions/thoughts:
- to make the sequence handling persistant, a very simple way would be to save this var to some file: $NPCS.nextSequence[$currNPC] it holds the next sequence for each bot...

I've added an (yet unused) example to TGEConEdGui.cs:
  

//--------------------------------------------------------------------
// example how to save the label of the next sequence to play for each NPC
// not used/tested yet!
//--------------------------------------------------------------------
function saveNextSequenceForNPCs()
{
   %count = MissionCleanup.getCount();
   for(%j = 0; %j < %count; %j++)
   {
      %client = MissionCleanup.getObject(%j);
      %bot = %client.getClassName();
      if(%bot $= "AIPlayer")
      {
	      %botName = %client.getShapeName();
	      $Game::Dialogs::NPCS.nextSequence[%botName] = $NPCS.nextSequence[%botName];
      }
   }
}

 
Of course, you'd have to read in these vars when the NPC enters the game the next time! (e.g. in function initializeSequences(%client, %npcName))

If there are any problems or questions, contact me at beffy@gmx.de or on IRC, of course!

If you use this little tool, it would be nice to mention me in your credits! :))

Have fun!!! :)


 
Commenter - add your comment below
Be the first person to add comment to this page!
Add a Comment
name:
email: (optional)
URL: (optional)
comment:
 
Commenter v.0.82 by GreatNexus.com web site design and development