Vespers3D: Adventures in Cinematics, Part II
by Rubes · 06/17/2007 (7:57 pm) · 6 comments

Vespers3D: Adventures in Cinematics, Part II
Recap:
Vespers3D is our attempt to bring old-school text-based adventure games (interactive fiction) into the world of real-time first-person 3D - a new genre we are calling 3D/if (3D interactive fiction). It is based on Vespers, Jason Devlin's fantastic text IF game that won numerous awards from the IF community, including Best Game at the IFComp'05 and the 2006 XYZZY awards. Vespers provides a compelling setting and a powerful storyline for a game that will, in the end, be something akin to Myst but with a fully interactive 3D environment and good old-fashioned text command input and output.
We continue to make gradual progress as we ease into the complicated world of animation and sound, so I thought I'd outline some of the things we're trying as we implement a system for displaying scripted animation sequences.
Vespers3D and Cinematics, Continued
As I began to discuss in my last plan, Vespers3D is a very linear game, and in the world of 3D, linear means a lot of scripted action including short animated sequences and cutscenes. When players perform certain actions -- like conversing with an NPC, for instance -- we need to be able to produce a specific series of responses. In most cases, the responses consist of simple audio recordings, animation sequences, or combinations of the two. The challenge is to carry out these tasks in a specific order, and with appropriate timing. This is the job of our Sequence Manager.
In my last plan, I gave the following example of a very short, simple interaction between the player (the Abbott) and one of the NPCs (Matteo) from the text version of Vespers:
Brother Matteo leans against the rails, staring into the wind. [b]>TALK TO MATTEO[/b] "How are you Matteo?" you ask, leaning up on the rails beside him. He turns, a friendly smile on his face. "I don't even know anymore." He gives an exaggerated shrug.
Although that is a relatively simple thing to accomplish with text, in 3D we need to perform all of the following to produce this exchange:
1. Play an idle animation for Matteo, looking out over the countryside.
2. When the player command is received, stop Matteo's idle animation.
3. Play the audio recording of the Abbott asking the question.
4. When that finishes, play Matteo's response (animation plus audio).
5. When that finishes, resume Matteo's idle animation.
The Sequence Manager handles this through our SM_callSequence() command. It accepts arguments including which character to use, which sequence to run, and which step of the particular sequence is next. So for the above exchange, which we call the "talkA" sequence, the Sequence Manager does the following:
case "talkA":
switch(%index) {
case 0:
%npc.getDatablock().stopIdleAnimation();
SM_initSequence(%npc, ap_abbott_MatteoTalkA, "", "audio");
return;
case 1:
SM_initSequence(%npc, ap_matteo_TalkA, "talkA", "anim");
return;
default: SM_sequenceFinished(%npc);
}The first step of the sequence stops Matteo's idle animation and then starts playing the audio sequence of the Abbott asking the question (in the audio profile "ap_abbott_MatteoTalkA"). The "audio" argument in that statement tells the Sequence Manager that the audio clip should be used to detect the end of the sequence step; that is, the end of the audio clip should be the trigger to advance to the next step in the "talkA" sequence. When this trigger is received, it advances the step counter index and calls this routine again.
The second step then starts playing Matteo's response, which consists of an audio clip ("ap_Matteo_TalkA"), and an animation sequence ("talkA", which is defined in Matteo's TSShapeConstructor datablock). It also tells the Sequence Manager (via the "anim" argument) that the end of the animation sequence should be used as the trigger to move to the next sequence step.
The final step of the sequence ("default") calls the SM_sequenceFinished() routine, which cleans everything up, returns control to the player, and returns Matteo to his idle animation sequence.
Using this method, we get the following result (no, it's not embedded video, just click to see the video on Google):

Figure 1. Click image above to see Matteo in action.
So this is basically how the Sequence Manager is set up, and we need to basically define a series of sequence steps like these for every scripted action that takes place in the game. Most are very short, simple series like above, but the nice thing about this approach is that it provides a bit of flexibility in the various things we can do with each of the characters. It allows for much more complex, scripted interactions -- including longer cutscenes. Since the Sequence Manager handles scripted sequences for each individual NPC, we could set up sequences where one of the steps includes initiating a sequence for one or more other NPCs. This way, we could have one action set off a series of sequences, either simultaneously or in succession, which could produce a long series of interactions such as a conversation between multiple characters. Fun stuff!
Animation States
Setting up each individual NPC for animation like this is also a bit of a challenge. TGE was designed with some pretty decent animation functionality built in that allows for a lot of creativity, but it's also pretty persnickety and quite often grumpy. We really needed to spend some time experimenting with it to come up with a system that allows us to easily implement the sequence routines mentioned above.
In my last plan, I began to discuss some of the details of a "state machine" that we would use to track and implement the different animation states for each character. Since then, we've modified things to fit more with Torque's different animation features:
Figure 2. Basic NPC animation state chart.Each character is basically divided into two animation threads: one for "idle" animations, the other for "speech" animations. The former consists primarily of full-body animation sequences that are used to display animations when the character is not engaged, like Matteo looking out over the countryside, as above. Since these are generally full-body animations, we use setActionThread() to play them. They can be looping or non-looping, and when we want them to stop, we just call setActionThread() with the root sequence, and the NPC smoothly transitions back to his or her root position. We then use that root position as the starting point for the latter, the speech animations.
The speech animations are usually (but not always) simple animations that involve mostly the NPC's mouth and head. Because of this, we can set them up as blend animations and play them using playThread(). Thus, we can have the NPC talking while continuing their idle animation. If, however, we want a particular speech animation to be more involved than just the mouth and head, we can set it up so that the idle animation stops first, and then the speech animation is played (as with Matteo, above). After the speech animation finishes, this thread returns to the inactive state, while the idle animation thread either continues or returns to action.
Thus, we have a system set up where animation states work in conjunction with the Sequence Manager to produce some pretty decent NPC interactions, with the flexibility to produce highly scripted, complex cutscenes when we need to. Such as the following sequence from the text game, which I mentioned last time and which is part of the first big cutscene:
Constantin strides forward and smacks Lucca across the face, sending him sprawling into the snow. "Get away from him, murderer!" he shouts, eyes burning. "Constantin!" Ignatius shouts defiantly, stepping between the two. Although Constantin towers above him, Ignatius looks taller than you have ever seen him. "Leave the boy alone." Drogo giggles.
Should be fun putting that one together.
Next time, I'll start to introduce each of the six NPC's in the game one by one and discuss how we developed each one from a text description in Vespers to its concept drawing, modeling, voice recording, and animation. It's really interesting to see how each one has taken on an identity of its own, and I hope you will find it interesting as well.
#2
06/18/2007 (4:16 am)
Awesome work!
#3
06/18/2007 (5:16 am)
So I think I asked this before, but are you going to sell this mechanism as a add-on for Torque? I could really use this.
#4
06/18/2007 (7:10 am)
Wow, nice work Rubes!
#5
I look forward to seeing the progress of this... :0)
06/18/2007 (8:35 am)
Great! I left a rating and comment on google video and I'll quote most of what I said there, here: Nice stuff, really brings the code example to like!I look forward to seeing the progress of this... :0)
#6
I hadn't planned on releasing this as a resource, mostly because it still needs work and was not designed for network play, and a lot of it is specific for this project. But the code itself is pretty basic script, so once we have it more advanced and working well I can offer help for those who want to do something similar.
06/19/2007 (12:30 am)
Thanks everyone, I appreciate the comments and support.I hadn't planned on releasing this as a resource, mostly because it still needs work and was not designed for network play, and a lot of it is specific for this project. But the code itself is pretty basic script, so once we have it more advanced and working well I can offer help for those who want to do something similar.
Associate Tom Eastman (Eastbeast314)