Creating a Flexible Dialog System

January 27, 2022, 11:15 am

One of my more ambitious goals for system Fault was generating lots of dialog for various situations. I wanted the robots to tell you what they were up to in their own voices, and to talk amongst themselves in naturally flowing chatter. My requirements were so steep that I wasn't sure how to pull it off, until I fairly recently settled on a new design that debuted in version 22.1.24.1.

My requirements:

  • Dialog should be easy to write. A simple text format is ideal.
  • Dialog shouldn't be embedded in shell scripts, or some other context where quoting or other common punctuation may break something.
  • Audio clips should be generated, converted, and normalized for each robot type from the script. Adding more robot types should only involve tweaking the build script.
  • Each line of dialog can be associated with arbitrary metadata. I need to know whether a line is a taunt, something a robot says while investigating, or a snarky quip tossed off when an enemy gets bored and gives up on searching.
  • Additionally, I need some means of tagging a given segment of dialog as a multi-character interaction. In that case, each separate line of a dialog interaction needs to be generated for each robot type. I also need a mechanism for indicating line order by filename so each character queues up the right line at the right time.
  • And a unicorn. I'd like a unicorn, please. Preferably one that flies, and runs on nuclear power.

If you're like me, you learn how to operate on a budget. Time and money are almost always in short supply, and I'm always looking out for ways to repurpose tools in new ways. I wondered if one of the languages for writing interactive stories might make a good tool for dialog generation and, after a bit of research, I settled on Twee. After a few hours of mad hackery, I made great progress on a system I'm happy with. Here is a sample of my current dialog script.

:: intruder_alert {"type": "taunt"}
Intruder alert! Intruder alert!

:: intruder_must_not_escape {"type": "taunt"}
The intruder must not escape!

:: hold_my_beer {"type": "investigate"}
Here, hold my beer.

Here, I:

  • Create Twine passages for each piece of dialog. Only single-line interactions are supported for now. The passage name corresponds to the filename.
  • My build script, written using cargo-make, uses an embedded Rust script to parse the dialog script.
  • I then iterate through each passage, checking for the existence of each dialog snippet for each robot type.
  • If a snippet doesn't exist for a given robot type, generate it with Espeak-ng, convert it to Flac, the normalize it via Sox.
  • When it's time to speak a dialog line in-game, filter asset paths by line and robot types, selecting one at random.

Future directions

I'm very happy with what this system lets me do, but I have more planned. Instead of referencing assets by path, I'd like to load the Twee story directly into the engine, filtering passages in a single asset rather than across all sound assets. I threw together a Bevy plugin to help with this. In the future, I might use this to present story files directly from a game. Twee seems like an interesting language for creating scripted encounters, and careful use of variables/tags could let stories interact with the engine itself (I.e. spawning enemies/powerups based on story choices.)

Next I need to work on multi-character dialog. It may look something like this:

:: shoot_foot {"type": "in_gunsight", "characters": 2}
1: Watch where you point that thing! You'll shoot your foot off!
2: Is that actually a thing?
1: Yeah, humans apparently shoot themselves in the feet all the time.

This:

  • Tells the game/build script that the given dialog passage should be interpreted as a multi-character dialog requiring 2 characters.
  • Adds a different dialog condition, in_gunsight, selected from when a robot is actively targeted by a player.
  • Generates shoot_foot.1.flac, shoot_foot.2.flac, and shoot_foot.3.flac for each robot type.
  • If a robot isn't visible to another robot, treat the passage like a single-line dialog, only presenting the first line.
  • If a robot is visible to another robot, assign the role of the second character to the viewer, queuing up dialog lines so they chat back and forth.

Theoretically this could expand out to multiple, dynamically-assigned characters, but I'll probably cap it at 2. I've also got some implementation work ahead of me, since my current systems only support single lines of dialog with no higher-level coordination.

Conclusion

I'm thrilled at how easy it is to add new dialog now. I can even add new types of dialog by introducing a new type tag value, and writing a separate system to select appropriate clips for different situations. Multi-character dialog is probably a few versions off, but I hope to expand the existing chatter beyond the same few tired lines over the coming weeks.

Previous Post