WelcomeWhat's NewsORLibraryDownloadsLinksWebMaster@OnyxRing.com

This site
produced
656719
pageviews since
8/19/2004

Today's date:
11/20/2017






Author: Jim Fisher
Title: Advanced NPCs, Part 2b: Conversation and Learning

Creation Date: 8/9/2004 6:09:53 PM
Last Updated: 8/9/2004 6:09:53 PM


Introduction to Section 2b
Uniting Knowledge and Information
Shared Knowledge
Learning and Teaching Knowledge
Separating Information and Conversation
Concluding Section 2b
Note: Because of the length, the "Knowledge and Conversation" article was written in three sections (of which this is the second) listed here for convenience:
Part A (§1): The Basics
Part B (§2): Advanced Techniques
Part C (§3): More Advanced Techniques
Knowledge and Conversation
§2 Advanced Techniques
(Advanced NPC Implementation, Part 2b)


n the previous section we discussed various methods for developing NPC conversation and reviewed techniques for implementing the most widely used of these. In this section we will expand upon those methods with much more powerful techniques and begin to develop reusable classes for NPCs in the process.

Uniting Knowledge and Information

In the previous example, a character's knowledge of something and his ability to talk about it are not as tightly related, as they ought to be. Coded into the guard's life routine is the same information about Snow White that the mirror has. The guard may not recognize her name, but he was coded, wastefully, to talk about her anyway. The reverse of this is a more probable circumstance, considering the existence of common knowledge. In the above example, a new NPC would also need to be coded with responses to the 'beauty' topic. Uniting what is said with what is known is the second major benefit to using conversation objects.

To start off with, let's modify our KnowledgeTopic base class a little to default a property, which we will arbitrarily call "TopicInformation:"

     
 class KnowledgeTopic 
   with TopicInformation "~Yes, I know about that but I think I'll keep it   
      to myself.~"
 ;

Now we can tie the text to the actual knowledge objects:

     
 KnowledgeTopic beauty CommonKnowledge with name 'beauty'
   , TopicInformation "~Ah yes. That is only skin deep.~"
 ;

 KnowledgeTopic snowwhite magicmirror with name 'snowwhite' 'girl'  
   , TopicInformation "~Yes, I remember her.  But that was long ago.~"
 ;

And remove the text from the life property (of both characters) entirely. Instead, we can print out the TopicInformation value:

     
 object -> magicmirror "magic mirror" 
   has animate
   with name 'magic' 'mirror'  
   , life [;ask:
      if(second ofclass KnowledgeTopic)  {
         PrintOrRun(second, TopicInformation);
         return true; 
      }
 ];
 object -> guard"guard" 
   has animate
   with name 'guard'
   , life [;ask:
      if(second ofclass KnowledgeTopic)  {
         PrintOrRun(second, TopicInformation);
            return true; 
      }
 ];

Note that the example runs exactly as it did previously (i.e.: The guard will respond when asked about 'beauty', but not 'snowwhite.' The magic mirror will talk about both.)

An NPC'a ability to converse about a topic is now tied directly to the scope of the topic itself. If the character "knows" the information, he can talk about it. No changes to his life property need to be made to add this knowledge. This technique benefits us in several ways:

  • The life property now has code that is consistent across all NPCs. This enables us to implement a NPC base class to share the common code in the life routine.
  • We now have the ability to add knowledge and to take away knowledge without coding them in each and every NPC. We simply have to move the knowledge object around.
  • We are now one step closer to implementing a feature in our NPCs that is somewhat uncommon: the ability to learn and teach. Of course, this will require a few changes to fully accomplish. The next step in that direction is...


  • Shared Knowledge

    So far we've developed our scope rule to allow a knowledge object to be known by one character or all characters. This doesn't cover all cases, though. Rarely do we have an all-or-nothing circumstance; often we have a group of people that know a specific topic of conversation, and another group of people that do not. What is needed is a way to keep track of who knows what.

    The most intuitive way to accomplish this would be to add a property to each NPC that records what he knows. This isn't really the best solution, however, since knowledge does not have to be limited to NPCs. It could also be found in a book. Instead we can attach a property list to the knowledge object that indicates who or what "knows" it:

    
     ...from KnowledgeTopic class
       , KnownBy 0 0 0 0 0 0 0 0 0 0 !added to KnowledgeTopic  
    

    Having done that, it is now not such a complicated task to check this list for a specific object. We'll call this routine IsKnownBy() and attach it as a property to the KnowledgeTopic class as shown below:

    
     class KnowledgeTopic 
       with TopicInformation "~Yes, I know about that but I think I'll keep   
          it to myself.~"
       , KnownBy 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
       , IsKnownBy[obj t;
          for(t=0:t<(self.# KnownBy)/2:t++) (
             if(self.& KnownBy -->t==obj) return true;
          }
          return false;
       ]
     ;
    

    And, of course, our scope rule can leverage this as well:

    
     [TopicInTarget o; !isolate information known by the target
       switch(scope_stage){
          2: objectloop(o ofclass KnowledgeTopic) 
             if(parent(o)==CommonKnowledge || 
                   o.IsKnownBy(inputobjs-->2)) 
                PlaceInScope(o); 
                rtrue;
          3: "I did not understand who or what you were referring to.";  
       }
     ];
    

    Note that our scope rule no longer looks for the parent of the topic anymore. This is actually a preferable arrangement since we can now avoid placing the knowledge topics in the game world at all. This keeps us from having to deal with sticky commands like:

    
     >drop beauty  
    
    or
    
     >examine beauty  
    

    To demonstrate our new capacity for selectively shared knowledge, we will need to create a third NPC. This one, like the mirror will also know the 'snowwhite' topic, making this knowledge shared by two characters, but still unknown by the third (the guard.)

    Since we are beginning to duplicate code, this is a good opportunity to implement an NPC base class:

    
      Class NPC
       has animate      
       with life[;ask:
             if(second ofclass KnowledgeTopic) {  
                if(noun==actor) {
                   L__M(##Tell, 1);
                   rtrue;
                }
                PrintOrRun(second, TopicInformation);   
                rtrue;
             }
          ]
     ;
    

    The two existing NPC's, by inheriting from this class, are now reduced to:

    
     NPC -> magicmirror "magic mirror" with name 'magic' 'mirror';  
     NPC -> guard "guard" with name 'guard';
    

    And a third can be created just as easily:

    
     NPC -> dwarf "dwarf" with name 'dwarf';
    

    The snowwhite topic can now be shared between the MagicMirror NPC and the Dwarf NPC by filling in values in the KnownBy property (the beauty topic, being a child of the CommonKnowledge object, does not need the KnownBy property):

     
     KnowledgeTopic snowwhite magicmirror 
       with name 'snowwhite' 'girl'
       , TopicInformation "~Yes, I remember her.  But that was long ago.~"  
       , KnownBy magicmirror dwarf 0 0 0 0 0 0 0
     ;
    

    Now we can verify that the same KnowledgeTopic is shared by two NPCs, but not all NPCs:

    
     >ask mirror about snowwhite
     "Yes, I remember her. But that was long ago."  
    
     >ask guard about snowwhite
     "There is no reply."
    
     >ask dwarf about snowwhite
     "Yes, I remember her. But that was long ago."
    

    Now we're are ready to give our NPCs (and PCs, too) the ability to learn...

    Learning and Teaching Knowledge

    Learning and teaching are the same action described from two perspectives. Since implementing the 'shared knowledge' functionality, the act of learning a new topic translates exactly into manipulating the topic's KnownBy list. The following routine can be added to the KnowledgeTopic class to do just this:

    
     ...from KnowledgeTopic class
       , MemorizeFor[obj t;
          if(self.IsKnownBy(obj)) return true; !already known  
          for(t=0:t<(self.# KnownBy)/2:t++) {
             if(self.& KnownBy -->t==0){
                self.& KnownBy -->t=obj;
                return true;
             }
          }
          print "Run-Time Error!  Object is known by too many sources and  
             cannot be learned.  Increase the size of the KnownBy property.";  
          return false;
       ]
    

    Now, letting the guard in on the 'snowwhite' secret can be accomplished with a simple:

    
     snowwhite.MemorizeFor(guard);  
    

    Thus far, the 'ask' portion of the 'life' routine has called the TopicInformation property to print out the topic. Often, as with the case of topics being taught and learned, there is more to be done than simply printing text. Morphing topics is an example of this that will be discussed in a moment, as well as adding character personality by separating information from conversation. For now though, we can just recognize that the teaching/learning of topics should be tied to the printing of dialog and a driving function should be added to call both, without blurring the two into the same routine. A new property, we'll call it 'TellAbout,' will be added to the KnowledgeTopic class to do both:

    
     Attribute learnable;
    
     ... rest of KnowledgeTopic class code here   
       , TellAbout[to from;
          PrintOrRun(self,TopicInformation);
          if(self has learnable) Self.MemorizeFor(to);  
       ]
    

    The TellAbout routine is where the logic behind the conversation occurs. The 'learnable' attribute has been introduced to govern whether or not to "teach" this topic. Some topics are simply small talk and don't need to be shared between characters. 'Learnable' allows the developer to designate which topics can be "learned."

    Astute readers may catch that the 'from' variable is never used and so will generate a compiler warning. This is okay. The 'from' variable is actually a parameter that will become useful to us later. We've just introduced it early.

    We can now change the life routine of our NPC class to call the TellAbout property:

    
     ...from NPC class
       , life [;ask:
          if(second ofclass KnowledgeTopic) {
             second.TellAbout(actor,self);
             return true;
          }
       ]  
    

    That's all that's needed for the player character to learn from an NPC. Of course, learning a topic is nothing if you cannot speak about that topic. In order to do that, we need to extend the 'tell' verb. First, a new scope rule to check and see what the actor (usually the player) knows:

    
     [TopicInActor o; !isolate information known by the actor  
       switch(scope_stage) {
          2: objectloop(o ofclass KnowledgeTopic && 
                   (o.isknownby(actor) || 
                   parent(o)==CommonKnowledge)) 
                PlaceInScope(o); 
             return true;
          3: "Who did you want me to say that to?";
       } 
     ];
    

    Next we will extend the 'tell' verb appropriately:

    
     [TellTopicSub; 
       if(noun==actor) {
          L__M(##Tell, 1);  
          return;
       }
       second.TellAbout(noun,actor); 
     ];
     Extend "tell" first  * creature 'about' scope=TopicInActor -> TellTopic;
    

    And that's it. All the functionality for teaching and learning topics is in place. For the sake of example, let's give the 'snowwhite' topic the 'learnable' attribute:
    
     KnowledgeTopic snowwhite magicmirror 
       has learnable     
       with name 'snowwhite' 'girl'
       , TopicInformation "~Yes, I remember her.  But that was long ago.~"  
       , KnownBy magicmirror dwarf 0 0 0 0 0 0
     ;
    

    and see our new functionality in action:

    
     >ask guard about snowwhite
     There is not reply.
       
     >tell guard about snowwhite
     This provokes no reaction.
       
     >ask dwarf about snowwhite
     "Yes, I remember her. But that was long ago."  
       
     >tell guard about snowwhite
     "Yes, I remember her. But that was long ago."
    
     >ask guard about snowwhite
     "Yes, I remember her. But that was long ago."
    

    Separating Information and Conversation

    A drawback to uniting information and knowledge is that we lose the ability to tailor a response to an NPC's personality. Separating the text that qualifies as topic information from the text that is character specific addresses this and creates dialog that is much more fluid and believable. Consider the ways in which each of the NPCs may differ in their answers regarding 'snowwhite,' as well as the differing ways that the topic could be broached:

    
     You hesitate for a moment, then: "Do you remember Snow White?"  
     "Yes, I remember her," replies the dwarf with a dreamy 
     expression. "But that was long ago."  For a moment you think you   
     see a tear in his eye.
    

    or

    
     "Do you remember Snow White?" you ask.
     The guard looks at you with disdain. "Yes, I remember her," he   
     says smugly. "But that was long ago."
    

    or

    
     You look around, making sure no one can see you talking to the 
     mirror. "Do you remember Snow White?"  you ask it, feeling   
     somewhat foolish. For a moment the mirror pauses, as though 
     trying to recall something almost forgotten.  Then it speaks: 
     "Yes, I remember her.  But that was long ago." 
    

    The narrative that accompanies dialog occurs in many different forms, but breaking the topic's information into, for example, two pieces can facilitate the use of most of these forms. The above examples can be accomplished by doing this. Text for asking about a topic also needs to be introduced:

    
     KnowledgeTopic snowwhite magicmirror 
       has learnable
       with name 'snowwhite' 'girl'
       , Query "Do you remember Snow White?" 0 !zero: don't replace punctuation   
       , TopicInformation "Yes, I remember her" "." "But that was long ago" "."  
       , KnownBy magicmirror dwarf 0 0 0 0 0 0
     ;
    

    Notice that the ending punctuation has been separated from the rest of the text. In the English language (and shown in the above examples), periods are often substituted with commas when joining a piece of dialog with a piece of narrative. This separation gives us the option to replace the punctuation programmatically. It would be nice if the substitution always occurred, but with some punctuation, such as the question mark, the punctuation remains and is not replaced by a comma. To signify this, we can choose a zero value for the punctuation list element. Additionally, you can see that there are no quotation marks included in the information text. These will be included or not, by the narrative text.

    Adding all of this new diversity to the information objects greatly increases the complexity. Since we may not always want the complexity we should be able to opt out of it if we so desire. A fairly good indicator that we don't want to personalize the text to the NPC is having only one value in the given property. That is, if the ending punctuation wasn't provided then just print the text and move on. The following KnowledgeTopic TellAbout routine, together with a new property, does a basic form of this conversation "blending" provided the telling NPC doesn't provide a means to do this:

    
     ... part of the KnowledgeTopic base class
       , TellAbout[to from;
          if((self.#topicinformation/2)>1){
             if(from provides ProcessDialog) {
                from.ProcessDialog(to, PersonalizeTell,
                   self, TopicInformation);
             }
             else{
                if(from==player) print "You say, ";  
                else print (The)from, " says, ";
                FlushDialog(self,TopicInformation,0);
             }
          }
          else PrintOrRun(self,TopicInformation);
          if(self has learnable) self.MemorizeFor(to);
       ]
    

    Note that the ProcessDialog routine is called if the NPC provides it. This routine will parse the topic information and print it through a property we've specified, in this case PersonalizeTell. We've made a presumption that if the object provides ProcessDialog, then it also provides PersonalizeTell. We could check for both, but if we add them to our NPC base class, then it is really not such a large leap of faith to presume that the existence of one implies the existence of the other.

    
     ... part of the NPC base class
       ProcessDialog[talkto procedure knobj info s1 p1 s2 p2;  
          s1=(knobj.&info-->0);
          p1=knobj.&info-->1;
          s2=0; !default to nothing
          if((knobj.#info/2)>2){
             s2=knobj.&info-->2;
             p2=knobj.&info-->3;
          }
          self.procedure(talkto, s1,p1,s2,p2);
          FlushDialog(knobj,info,4); !print out remaining text
       ]   
       , TellLine[before s1 canreplace p1 after;!print a single portion of text  
             if(s1){print (string)before, (string)s1;
                if(canreplace)print (string)p1; !can replace punctuation?
                print (string)after;
                return true;
             }
             return false;
          ]
       , PersonalizeTell[talkto s1 p1 s2 p2; !override for npc formating
             if(self.TellLine("~",s1,p1,",","~ "));
             if(self==player) print "you say to ";
             else print (the)self," says to ";
             if(talkto==player) print "you. ";
             else print (the)talkto,".";
             self.TellLine("~",s2,p2,".","~ ");
          ]
    

    The routine for wrapping all remaining text in quotes and printing it is best allocated globally since multiple classes will use it.

    
     [FlushDialog knobj info start t ;
       if((knobj.#info/2)<=start) return;
       print"~";
       for(t=start:t<(knobj.#info/2):t=t+2){
          if(t>start) print " ";
          print (string) knobj.&info-->t;
          if(knobj.&info-->(t+1)) print (string) knobj.&info-->(t+1);  
       }
       print"~";
     ];
    

    So why did we pass in PersonalizeTell rather than just calling it from the ProcessDialog? Doing so enables us to reuse the code in ProcessDialog and specify other routines for the text customization. We'll use this in a moment when our characters begin to "ask" as well as "tell."

    Now we have the entire framework in place. Alone, we have far better than just quoted text.

    
     >ask dwarf about snowwhite
     "Yes, I remember her," the dwarf says to you. "But that was long   
     ago."
    
    

    But we can also personalize the telling of topics with regard to the speaking NPC:

    
     NPC -> dwarf "dwarf" 
       with name 'dwarf'
       , PersonalizeTell[talkto s1 p1 s2 p2;
          talkto=0; !disable warning msg
          self.TellLine("~",s1,p1,",","~ replies the dwarf with 
             a dreamy expression. ");
          self.TellLine("~",s2,p2,".","~ For a moment you think you see  
             a tear in his eye.");
     ];
    
     NPC -> guard "guard" 
       with name 'guard'
       , PersonalizeTell[talkto s1 p1 s2 p2;
          print "The guard looks at ";
          if(talkto==player) print "you";
          else print (the)talkto;
          self.TellLine(" with disdain.  ~",s1,p1,",",
             "~ he says smugly. ");
          self.TellLine("~",s2,p2,".","~ ");
     ];
    
     NPC -> magicmirror "magic mirror" 
       with name 'magic' 'mirror'
       , PersonalizeTell[talkto s1 p1 s2 p2;
          talkto=0; !disable warning msg
          self.TellLine("For a moment the mirror pauses, as though 
             trying to recall something almost forgotten.  
             Then it speaks: ~",s1,p1,"."," ");
          if(self.TellLine("",s2,p2,".","~ ")==false) print"~";
       ]
     ;
    

    Of course, the PersonalizedTell routine works better if stocked with several variations of text, but this is a good starting point. To completely implement our examples, we still need to implement a PersonalizedAsk routine. Since the act of asking is done by the player rather than the NPC, it needs to be implemented somewhere other than the NPC's life routine. Replacing the standard library's AskSub routine is the obvious choice:

    
     replace AskSub;
         
     [AskSub;
       if(second ofclass KnowledgeTopic) 
       second.AskAbout(noun, actor);
       if(RunLife(noun,##Ask)~=0) rfalse;  
        L__M(##Ask,1,noun);
     ];  
    

    Now we can implement the AskAbout routine just as we did TellAbout:

    
     ...in the KnowledgeTopic class
       , AskAbout[askwho askby;
          if((self provides query)==false) return;
          if((self.#query/2)>1 && askwho has animate or talkable){  
             if(askby provides ProcessDialog) {
                askby.ProcessDialog(askwho, PersonalizeAsk,
                   self,query);
             }
          }
          else{
             if(askby==player) print "You ask, ";
             else print (The)askby, " asks, ";
             FlushDialog(self, query,0);
          } 
       }
       else PrintOrRun(self,query);
       print "^";
     ]
    

    And add the default PersonalizeAsk routine to the NPC base class:

    
     ... NPC class     
       , PersonalizeAsk [talkto s1 p1 s2 p2; !over-ride for npc formatting  
          if(self.TellLine("~",s1,p1,",","~ ")){
             if(self==player) print "you ask ";
             else print (the)self," asks ";
             if(talkto==player) print "you. ";
             else print (the)talkto,".";
          }
          self.TellLine("~",s2,p2,".","~ ");
       ]
    

    At this point, we've accomplished everything we need to print out the query text when the player asks about a topic. We've also laid the framework for our NPCs to ask questions. Personalizing the narrative is accomplished for "ask" in the same way that it was for "tell." For example, let's add the following personalization to the guard:

    
     ...guard instance
       , PersonalizeAsk [talkto s1 p1 s2 p2;
          talkto=0; !disable warning
          if(self==player) print "You hesitate";
          else print (The)self," hesitates";
          self.TellLine(" for a moment, then:  ~",s1,p1,".",
             " ");
          if(~self.TellLine("~",s2,p2,".","~ ")) print"~";  
       ]
    

    The following code will now cause the guard to ask the dwarf about the snowwhite topic:

    
     actor=guard;
     <ask dwarf snowwhite>;  
    

    Upon execution, the generated text reads:

    
     The guard hesitates for a moment, then:  "Do you remember  
     Snow White? "
     "Yes, I remember her," replies the dwarf with a dreamy   
     expression. "But that was long ago." For a moment you think   
     you see a tear in his eye.
    

    To top it all off, when all of this narrative diversity is said and done, the guard has now learned something that he can talk about if asked.

    As you look over the guard's PersonalizeAsk routine, you might be wondering about the "You hesitate" text and the corresponding check to see if the guard and the player are one and the same. Of course, this is only necessary if you plan of letting the player "body jump" into the guard, and in a real game, we would want to make similar changes to the PersonalizeTell routine. As it happens, we can utilize the standard library's ChangePlayer() routine to demonstrate giving the player the same conversation abilities that we have given our NPCs:

    
     <ask dwarf snowwhite>;
     print "^     <Player becomes the guard...>^";  
     ChangePlayer(guard);
     <ask dwarf snowwhite>;
    

    Notice that when the player character becomes the NPC, he also inherits the personalization routines:

    
     "Do you remember Snow White?"
     "Yes, I remember her," replies the dwarf with a dreamy
     expression. "But that was long ago." For a moment you think  
     you see a tear in his eye.
          <Player becomes the guard...>
     You hesitate for a moment, then:  "Do you remember 
     Snow White?"
     "Yes, I remember her," replies the dwarf with a dreamy 
     expression. "But that was long ago." For a moment you think 
     you see a tear in his eye.
    

    A useful technique, even for games where the player does not change, is to create an NPC that will be the player. This way, questions asked by the player will have the same customized formatting as questions asked by NPCs.

    It should be noted that the pronoun print rules discussed in the previous article "
    Pronouns On Steroids" really shine in this area and make coding dialog for these sorts of dual-purpose NPCs much simpler.

    On to More Advanced Techniques

    In this section we have covered several advanced topics, that can be used to customize text to a specific NPC. There's more to come in section 3...

    Next:
    §3c: More Advanced Techniques

    Previous:
    §1: The Basics



    Copyright 2004 - 2017 : Jim Fisher
    OnyxRing.com has been given the potentially non-exclusive right to display the content of this article. However the original author retains all rights. Permission to reproduce this article -- either in part or in whole -- is left strictly to the discretion of the original author.

    Table of ContentsAuthorsSearchIndex
    Would you
    recommend this
    article to
    someone else?
    You bet I would! Heck No!