This feature describes how we added a RPG/Adventure-style nonlinear Dialogue System to the First-Person-Shooter Crysis Wars.
The technically interested reader may also find some codesamples at the end.
The CryEngine 2 isÂ undoubtedly one of the most advanced game engines today offering great graphics, good scaling on weaker and stronger systems, amazing physics and not to forget a feature rich system for character animation.
What is missing a bit are classical RPG/Adventure game elements like a nonlinear Dialogue System. We believed that it's possible to extend Crysis Wars with such a system.
Sounds too good to be true? Got curious how we did it? Read along and see for yourself ...
1.) Under the hood
In this section you will learn what was needed to create the dialogue shown in the Dialogue Video presented in the corresponding news posting.
The first question when designing a Dialogue System is how to define a dialogue and where to put that data. We decided to use XML files since they're common for such things (the native Dialogue System of Crysis also uses XML by the way) and there exist many libraries and tools to manipulate them.
I won't be listing our specifications here, but just to get an impression here is an excerpt from the XML defining the dialogue shown in the video:
<dialogue name="Molaar / Oasis">
<label name="start" />
<logic type="gametoken" name="Level.Molaar.C01.MolaarShownCutterToShrine" is="true">
<jump to="helped" />
<say speaker="player" timeout="4">You talan really are something. How can I help, Molaar?</say>
<say speaker="npc1" voice="mods/OpenOutcast/Game/Sounds/Dialog:Oasis_test:c01_molaar_molaarisnotknowing" facial="c01_molaar_molaarisnotknowing">Uhh, Molaar does not know. Shamaz Zelum knows! He is at the shrine.</say>
<set gametoken="Level.Molaar.C01.InitialConversationDone" to="true" />
<label name="choice" />
<choose listener="player" speaker="npc1" />
<option>Take me to Shrine</option>
<jump to="locate" />
<jump to="takemeto" />
<say speaker="player">Could you take me to the shrine?</say>
<say speaker="npc1" voice="mods/OpenOutcast/Game/Sounds/Dialog:Oasis_test:c01_molaar_yesulukai" facial="c01_molaar_yesulukai">Yes, Ulukai! Molaar knows the way.</say>
<action id="2" />
<set gametoken="Level.Molaar.C01.MolaarShownCutterToShrine" to="true" />
To point out some interesting tags:
- the dialogues are organized using label tags
- normal conversation is initiated by the say tag
- selections can be performed using the choose tag
- you can jump around between labels using the jump tag
- external actions can be triggered using the action tag
Anyone of you who tried out Oasis, our first technology demo released in February, probably noticed that we already had some kind of Dialogue System back then. So did it actually pay off to invest so much time in creating a new one? Why not move along with the old version? Let the pictures talk:
The image above shows the Flow Graph (for those uncommon with the CryEngine 2 terminology: that's a visual script you can modify by adding and connecting different nodes) of the initial Molaar-dialogue. And now the same with the new system:
The latter version looks much simpler and less chaotic - this has two implications:
- Our Level Designers and Game Designers can focus on their work instead of connecting thousands of nodes again and again (since we are going to have LOTS of NPCs!)
- Our coders can focus on their work instead on debugging this nightmare
To find a way for adding more itensity to dialogues it's always a good idea to take a look at movies. And what do they do to accomplish this among other things? Right - using different camera settings according to the current situation.
An important aspect of the camera part of the Dialogue System was the easy configuration. You can setup any mode (shown in the Camera Video) via an XML-file that looks like:
<mode name="extremecloseup" offset="-0.04f,0.45f,0.07f" toffset="0.0f,0.0f,0.17f" zoomspeed="2.5f" fov="26.0f" />
<mode name="closeup" offset="-0.2f,0.5f,0.10f" toffset="0.0f,0.0f,0.12f" fov="40.0f" />
<mode name="mediumcloseup" offset="-0.3f,0.6f,0.15f" toffset="0.0f,0.0f,0.05f" fov="50.0f" />
<mode name="mediumshot" offset="-0.4f,0.9f,0.1f" toffset="0.0f,0.0f,0.0f" fov="60.0f" />
<mode name="fullshot" offset="-0.5f,1.35f,0.45f" toffset="0.0f,0.0f,-0.4f" fov="68.0f" />
<mode name="mastershot" offset="-14.0f,7.0f,6.0f" toffset="0.0f,0.0f,6.8f" fov="70.0f" />
<mode name="objectfocus" offset="0.3f,-3.0f,0.5f" toffset="0.0f,0.0f,1.5f" zoomspeed="6.0f" fov="70.0f" />
<mode name="zoomin" offset="-0.3f,0.93f,0.1f" toffset="0.0f,0.0f,0.0f" zoomspeed="4.0f" fov="55.0f" />
<mode name="overshoulder" offset="0.3f,-0.7f,0.1f" toffset="0.0f,0.0f,0.1f" fov="45.0f" />
<mode name="medium2shot" offset="-1.2f,0.0f,0.1f" toffset="0.0f,0.0f,1.5f" fov="70.0f" />
<mode name="overhead" offset="-0.3f,0.7f,0.1f" toffset="0.0f,0.0f,0.1f" rotspeed="4.0f" fov="90.0f" />
<mode name="roundabout" offset="-2.2f,-1.0f,0.1f" toffset="0.0f,0.0f,1.8f" rotspeed="6.0f" fov="90.0f" />
The camera is placed and oriented dynamically according to the position of the current speaker and listener as shown on this sketch:
Every bigger game comes fully lip-synced nowadays. But what is with Mods? Does it pay off? Animating line by line seems to be extremely time-consuming. So no lipsync after all?
Fortunately there are some tricks to lower the workload by generating lipsync automatically. The animations seen in the Dialogue Video have been autogenerated from the sound files first, and tweaked later a little manually. But let's hear some details on this process from doCHtor, our head of animations:
doCHtor wrote:For lipsync, first of all morph targets were made by the 3d artists. Then phonemes and many expressions (which you can't see in the demo video yet) were build by the animators in the facial editor (that is included in Sandbox 2). Then timings for phonemes were pulled out of the sound files by a free tool called "Quick'n'Dirty Phoneme Extractor". For the future we also want to add some head motions, gesticulation and facial expressions.
2.) Probably FAQ
A list of questions that may probably be asked frequently.
- Do I have to wait for the final release of open Outcast to see the Dialogue System in action?
Answer: No. We want your feedback on the Dialogue System to improve it's usabilty. Therefore we will release an improved version of Oasis after the implementation of the Questsystem and some additional HUD-elements (Journal and Lexicon) in Q2 2010 (most likely around June).
- Is the Dialogue System compatible with the FGPS?
- Will the Sourcecode of the Dialogue System be released?
Answer: That's not going to happen in the near future.
Answer: We don't have enough time for maintaining a separate public release of the Dialogue System. Some parts of it definetly will be changed after the implementation of the Quest- and advanced AI-system.
- That is the only reason? But I just want to have some code to play with and I don't care for it's quality!
Answer: Releasing parts of the mod along with the corresponding sources before the main release has been discussed in the team and the majority was against it.
The main reason is that we don't want to see our work reused by other mods before we used it ourselvels in the release version of open Outcast. You can take a look at the Code snippets below and you may still find what you've been looking for.
3.) Code snippets
Some handy pieces of code that proved themselves valuable during the development of the Dialogue System. I didn't find much about the camera and lipsync handling on the net so those points may be particularly of interest.
With the following code you can create a simple entity from C++ code. You can use it to keep track of a position or a rotation (or attach some view to it and use it as camera!).
// Create a dummy entity
// Will be placed at vPosInit(Vec3) and rotated with qRotInit(Quat)
spawn.sName = "DummyEntity";
spawn.pClass = gEnv->pEntitySystem->GetClassRegistry()->GetDefaultClass();
spawn.vPosition = vPosInit;
spawn.qRotation = qRotInit;
pDummyEntity = gEnv->pEntitySystem->SpawnEntity(spawn);
Cameras and Views
This code will create a new view, attach it to an entity (could be created like described above), rotate it in a way that it looks to a target vector and finally change the field of view:
// Make the dummy entity look to vTarget(Vec3)
q.SetRotationVDir(vTarget - pDummyEntity->GetPos());
// Attach a view to the dummy entity and make it the default view
IView* NewView = g_pGame->GetIGameFramework()->GetIViewSystem()->GetViewByEntityId(pDummyEntity->GetId(), true);
// Change field of view to 60Â°°
SViewParams sViewPar = *(g_pGame->GetIGameFramework()->GetIViewSystem()->GetActiveView()->GetCurrentParams());
sViewPar.fov = 60.0f*gf_PI/180.0f;
Parsing XML files
Let's say you want to parse a config file for camera modes with the syntax showed higher above (surprising example, isn't it?). Reading the name of the available modes could be done as:
// Print the list of supported modes
XmlNodeRef ROOT = gEnv->pSystem->LoadXmlFile("mods/OpenOutcast/Game/config/cameramodes.xml");
int iChildren = ROOT->getChildCount();
for (int i=0; i < iChildren; i++)
CHILD = ROOT->getChild(i);
CryLogAlways("Camera mode %s found!", CHILD->getAttr("name"));
If you always asked yourself how to trigger lipsync from code, then here is your answer:
// Set the facial animation of an Entity whose Entity Id is stored in iEntId
CActor* pActor = reinterpret_cast<CActor*>(g_pGame->GetIGameFramework()->GetIActorSystem()->GetActor(iEntId));
You shouldn't forget to register that "grin" animation for your model properly. We are using CAT for our animations and the config file used for the Dialogue Video looks like:
#filepath = Animations\talan\facial\test
c01_molaar_hail = c01_molaar_hail.fsq
c01_molaar_ifoundyou = c01_molaar_ifoundyou.fsq
c01_molaar_molaarhelpedyoufindshamaz = c01_molaar_molaarhelpedyoufindshamaz.fsq
c01_molaar_molaarisnotknowing = c01_molaar_molaarisnotknowing.fsq
c01_molaar_rightoverthere = c01_molaar_rightoverthere.fsq
c01_molaar_wehaveabigproblem = c01_molaar_wehaveabigproblem.fsq
c01_molaar_what = c01_molaar_what.fsq
c01_molaar_yesulukai = c01_molaar_yesulukai.fsq
c01_molaar_theyodsaresmiling = c01_molaar_theyodsaresmiling.fsq