Tuesday, April 1, 2014

Second time's the charm; procedural NPC dialogue in Nostrum


Last time I tried some type of "procedural narrative" thing, my hubris got the better of me -- naming the system after one of the most famous and influential writers of all-time was, perhaps, just a little arrogant.

Despite my attempt to scope it properly, that system suffered greatly from trying to do too much stuff... It was so much stuff that it was difficult for me to write anything with it. So with the procedurally generated NPCs in Nostrum, I'm developing a much simpler system which will hopefully work better, to solve a smaller problem...

The basis is still the same: Elan Ruskin's GDC 2012 presentation on AI-driven dynamic dialogue in Left 4 Dead 2.

The main idea there, in my eyes, was that the annoying part of branching dialogue was managing the branching... if you wanted a conversation to go 10 different ways depending on 20 variables, each branch had to be manually authored. It mirrors how writers might think, but that doesn't mean it lends itself well to systemic complexity. What if the computer could automatically do the branching for us, and automatically figure out which phrases best connect to other phrases? What if we could feed the computer a pile of facts about the world, and then it decides what's most interesting to say depending on those facts?

With my prior attempt to adapt this theory for my own uses, I committed the following sins:
  • Clinging on to linearity. The core engine still had a concept of "threads" with events that proceeded in a certain order, and it was trying to balance between these authored plot lines. This kind of defeated the purpose of having the code connect the dots for me.
  • Not specific enough. It should've been more purpose-built, with more limitations, which would've helped me simplify my writing. When too many different things are possible, it becomes more difficult to figure out what exactly to do, or how to translate that into game scripting.
  • Interface envy. I thought I needed a Unity Editor interface with buttons and configuration panels, which also meant I needed asset serialization support and file management. I ended up spending a lot of time on the interface instead of actually writing things.
The old tool I developed... too many buttons to push and menus to navigate. It made writing much slower / harder.
I suspect the core problem here was that I wasn't using my own tool enough, which meant I couldn't iterate on it. For my Nostrum dialogue engine, I'm trying a different development process: (1) in simple text files, I will (2) write some narrative script with imaginary script syntax, then (3) actually implement that scripting syntax so I can (4) test the results earlier and (5) iterate on syntax / features.

I also figure that if I'm trying to write something that doesn't even work in my imagination, then it probably won't work when I try to make it real... The results are still very early and terrible, but they look promising to me. Here's how my NPC dialogue system currently works:

Each text file is a CORE CONCEPT made of many different variant CONCEPTS that all express that same core concept but in different ways; the world state and the speaker's TRAITS determine which concept gets chosen.

(A trait is just a simple multiple choice variable, an adjective to describe a certain aspect of that NPC; with the "nationality" trait, you're either Italian or American or Spanish or Ethiopian, etc. and that's it. It's also technically possible to be Italian and American and Spanish and Ethiopian all at the same time, and maybe I'll make use of that in the game narrative later.)

Here's an excerpt from the text file "nost_hey.txt":

// NOST_HEY.txt, saying hello when the NPC already knows you

#
Hello.
Hi.
Hey.

#italian
Buongiorno.
Ciao.
Piacere.

#italian_pessimist
Buong-... ah, what's the point?
The world sucks. BTW I'm Italian!

#friend_italian_communist
Buongiorno, comrade!

#!italian
Hello, I'm not Italian!

#muslim
As-salamu alaykum.

It contains the core concept #hey (the NPC saying hello) which could contain the following variant concepts:
  • #italian  (a set of hellos, if the speaker is Italian), e.g. "Buongiorno!"
  • #italian_pessimist  (a different set of hellos, if the speaker is a pessimistic Italian), e.g. "Buong-... ah, what's the point?"
  • #!italian  (another set of hellos, if the speaker is not Italian) e.g. "Hello, I'm not Italian!"
When the NPC is instructed to say #hey, they check to see which speech concept's criteria match their personal traits. If they're missing any criteria, they ignore that concept -- but if everything checks out, they choose the most specific concept with the most matching criteria.

If our NPC was a pessimistic Italian, #italian_pessimist would probably score highly with them; but if the NPC was an optimistic Italian, then they would only be able to access the less specific #italian concept.

I also have a few checks in-place to de-prioritize recently-used concepts... so maybe if #italian_pessimist already got uttered recently by someone else, then a pessimistic Italian would just say #italian for the sake of variety and differentiation, even though it's technically a less specific concept.

... Then, once they choose a concept, the NPC utters a random line from that concept. So sometimes they'll say "Hello", sometimes "Hey", and sometimes "Hi."

I think this is a good start, and I like how simple and unobtrusive it is.

My next step is to figure out how to get these speech concepts to self-branch into each other and interact, and how to do that as painlessly as possible within my existing workflow. Right now, I'm thinking I'll just be able to invoke concepts from inside other concepts, and I'm writing more dialogue that would use that.