Game Development Community

dev|Pro Game Development Curriculum

Simple speech for Players

by Daniel Buckmaster · 11/26/2012 (4:56 pm) · 7 comments

In working on AI stuff, I created a handy utility function to have AIPlayers make sounds based on a simple function call. Instead of remembering the names of SFXProfile datablocks and so on, I wanted to just call a function with a textual representation of what I wanted the character to say. I also wanted the system to be generic enough to handle different voice types.

The solution I came up with revolves around using an ArrayObject to define a character's 'voice'. This array maps text phrases like "ouch", "hello" or "must have been rats" to SFXProfile objects that contain the appropriate sound files for each phrase. Then, to make a Player say the appropriate phrase, you can use text rather than referring to the sound file directly.

Simply paste the following code into a new file 'game/scripts/server/speech.cs':
//-----------------------------------------------------------------------------
// Copyright (c) 2012 Daniel Buckmaster
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

// Players can play up to 4 sounds concurrently in slots 0-3. We'll use slot 0
// for saying things.
$Player::SpeechSlot = 0;

// Say a phrase defined in the voice object given.
function Player::say(%this, %phrase, %voice)
{
   if (!isObject(%voice))
      %voice = DefaultVoice;
   
   %this.stopTalking();
   
   %sound = %voice.getValue(%voice.getIndexFromKey(%phrase));
   if (isObject(%sound))
   {
      %this.playAudio($Player::SpeechSlot, %sound);
      %this.stopTalking = %this.getDataBlock().schedule(
         %sound.getSoundDuration() * 1000,
         "onFinishedTalking",
         %this);
   }
}

// Shut this Player up immediately!
function Player::stopTalking(%this)
{
   %this.stopAudio($Player::SpeechSlot);
   if (%this.stopTalking)
   {
      cancel(%this.stopTalking);
      %this.getDatablock().onFinishedTalking(%this);
   }
}

// Callback when the character finishes playing the speech sound. You can override
// this in other datablock classes, but remember to always call
// Parent::onFinishedTalking(%this, %obj);
function PlayerData::onFinishedTalking(%this, %obj)
{
   %obj.stopTalking = 0;
}

// Define the 'default' voice used.
new ArrayObject(DefaultVoice);
DefaultVoice.add("ouch", PainCrySound);
DefaultVoice.add("ouch!", DeathCrySound);
Note that I assume you're using the Full template, so the SFXProfiles PainCrySound and DeathCrySound already exist.

Then, in 'scriptExec.cs', add the following line somewhere:
exec("./speech.cs");

Away you go! Try calling this in the console:
LocalClientConnection.player.say("ouch");

You can define a new voice object by simply copying the example at the bottom of the script. I haven't included this, but you might want to, for example, add a voice object to each PlayerData block. Then, for example, in a damage script, you could write this:
function PlayerData::onDamage(%this, %obj, %delta)
{
   if(%delta > 0)
      %obj.say("ouch!", %this.voice);
}
Now by giving different datablocks different voice objects, you can change the way each character sounds without having to change any scripts.

About the author

Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!


#1
11/27/2012 (1:22 am)
nice one Daniel, just that it doesn't seem to make the player say ouch.
I tried
LocalClientConnection.player.say("ouch");
in console, doesn't say a word,
I added
if(%delta > 0)
%obj.say("ouch!", %this.voice);
to the players onDamage and dropped him from a great height, doensn't say a word.
am I missing something?
#2
11/27/2012 (3:39 am)
Oh, that ain't good. Sorry about that. Any console errors? Try adding some echoes to the say function to see how far it gets. In particular, echo the value of %sound.
#3
11/27/2012 (4:19 am)
works great - Thanks Daniel
#4
11/28/2012 (8:55 am)
I've got something similar going on, though I didn't think of just having the sound profile return the sound length(totally forgot it could do that) or abstracting the speech audio slot(muuuuch cleaner).

I also made more extensive use of the arrayObject. It's stupid powerful when you get a handle on the thing, but I set it up so you can have multiple designations of the same 'sound', such as "ouch", and it'll pick randomly from them:

From the talk function:

%idx = findAllIndexFromKey(%this.dialogList, %dialog);
   
%index = getWord(%idx, getRandom(0, getWordCount(%idx)));
   
%sound = %this.dialogList.getValue(%index);
And then play the sound as usual

dialogList obviously being our arrayObject.

the findAllIndexFromKey function being:
function findAllIndexFromKey(%array, %key){
   %keyCount = %array.countKey(%key);
   
   if(%keyCount > 1)
   {
      for(%i=0; %i<%keyCount; %i++)
      {
         %indexes = %indexes SPC %array.getIndexFromKey(%key);
         %array.moveNext();
      }
      return %indexes;
   }
   else
      return %array.getIndexFromKey(%key);
}

%key being our "ouch" or whatever.
What that does, is it checks if there's more than one instance of the 'key'(our "ouch") in the array. We then build a word list of the indexes, and then pick one at random in our talk function.

So you could easily do:
%this.dialogList.add("ouch", PainSound1);
%this.dialogList.add("ouch", PainSound1);

Then to call it, do like the regular
%this.say("ouch");

It'll go into the say function, randomly pick an instance of the "ouch" key, and use that sound.
#5
11/28/2012 (8:59 am)
Aha, sorry, I meant the example to be PainSound1 and PainSound2. Didn't want to edit it and have it possibly blow my post up ;)

Also, the talk function is an excellent way to centrally call facial animations on your player to match your characters talking. Small neat details like that go a long way :D
#6
11/28/2012 (12:27 pm)
Yeah, I did think of extending this to account for multiple values mapped to the same key - hadn't gotten around to working out how ArrayObject works that deeply. Adding facial animations is a great idea too.

In my own codebase, I'll probably just add a callback to ShapeBase that triggers when the audio stops playing, instead of having to rely on a schedule. On the other hand, this method isn't too bad.
#7
12/16/2012 (8:07 am)
I like that resource.