| 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:
- create/edit/save any number of conversation files
- associate dialog sequences with sound files
- supports basic dialogs and multiple choice dialogs with unlimited number of
answers
- "live-editing" of conversation files, conversations are updated without
restarting the engine
- support for multiple trigger events
New in v1.0:
- conversations can now be started/stopped based on the FOV of the NPC
- "personalization": you can use the vars <PlayerName> and <NPCName>, they are replaced in the in-game
dialogs with the "real" names then
- you can also cross-link words/phrases to jump to another dialog sequence, or you
can specify an 'explanation link' which then pops up a sub-sequence with the
explanation text in the upper right corner of the GUI - this explanation is just
a normal conversation sequence and you can use the editor to create/edit it, of
course
- you can specify an image for the current NPC in the GUI
- you can specify function names and parameters instead of text answers now, too
and start custom actions if the player selects specific answers in
a multiple-choice dialog
- I've also added a history feature which lets you scroll through all previous
dialogs...
- a basic objectives/condition system which lets you specify certain
conditions/objectives that must be reached by the player before
a certain dialog sequence is played - you can change the status of objectives by calling
setCondition("magicKeyFound","true"); in-game, e.g.
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! |
|