10 Emotions – cAI
AIs can have emotions for happy/sad and anger/fear.
AI's store two emotions: Happy/sad and Angry/afraid. Each
emotion ranges from -10 (very sad or very afraid) to +10 (very happy
or very angry).
You can get the AI's emotions using AIEmotionsGet().
The emotions can be adjusted or set
with AIEmotionsSet(). This either sets the emotions
directly, or it adjusts the emotions, making the NPC happier, sadder,
angrier, or more afraid. The amount that a NPC's emotions
are changed is affected by pAIEmotionsChangable,
allowing for some NPCs to be more excitable than others.
Emotions gradually return to 0 over time. (Actually, they
eventually return to the AI's pAIEmotionsDefault over
time.) The speed of this return is controlled
by pAIEmotionsCalmDown.
You may have the AI's emotions changed based on conversations
the PCs are having with the AI, or good/bad news that the AI
hears, etc.
An AI's emotions are automatically adjusted during combat.
When the AI determines what its odds are for winning, this
is expressed on the AI's face as either anger (confident of
winning) or fear (almost ready to run).
If an AI sees an enemy killed or knocked unconscious it becomes
happier, and vice versa for friends.
11 NPC conversations – cAI
This tutorial provides the details about how conversations with NPCs work.
In Circumreality, players converse with NPCs using the chat window, the
same way that players talk with other players. The NPCs use
simple natural language processing (NLP) to understand what
the player is asking for.
Programatically starting up a conversation
When an AI hears speech (PerceiveSpeak()), or sees an emote
(PerceiveEmote()), or the PC identifies himself (PerceiveIdentifySelf()),
it's first task is to determine if the PC is talking to the AI
or another PC or NPC in the room. To do this, the AI
calls AIConversationIsTalkingToMe(). This method
uses various tricks to determine if the PC is talking with the AI.
If it is, a conversation is started up (see below) and the
method returns TRUE. If not, the method returns FALSE.
You will probably not need to call AIConversationIsTalkingToMe()
though.
If AIConversationIsTalkingToMe() starts up a conversation,
it calls AIConversationStart(). This method adds
an entry to pAIConversations so the AI knows
what characters it's talking to. It also causes
AILikeEmoteFirstMeet() to be called so that players can tell
how much the AI likes them. Finally, it adds
the oAIGoalConversation goal to the AI at
priority level 10. This causes the AI to stop doing what it
was doing until the conversation is finished (unless there
is a higher priority goal). The conversation will be finished
when the PC says "Goodbye" (or the equivalent), or leaves the
room.
You may wish to call AIConversationStart() if you wish your AI
to start up a conversation with a PC on its own. (See below.)
When the AI walks into a room, or when the AI sees the player
character walk into its room, AIConversationWantToStart() is
called. This lets the AI start up a conversation with PCs.
For example: An inn keeper might ask the player if they'd
like a room tonight without the player's ever approaching
the inn keeper. The default method will
have AI's who see well-liked PCs stop and say hello.
You may wish to write your own AIConversationWantToStart() for
chatty NPCs like inn keepers, friendly NPCs who know the PC,
or beggars. You will need to call AIConversationStart() to
start up the conversation though.
When a PC speaks, emotes, or introduces itself to an AI, the
speech (or emote or introduction) is passed
onto PerceiveConversation(). The method isn't
called immediately, but has a 1 second delay. This makes for
more realistic response times, as well as ensuring two AI's
in conversation don't get into an infinite loop.
The PerceiveConversation() method does this following:
-
Sets up all the rule-sets to be used by the NLPParse() function.
For more information on parsing and the NLPParseXXX() functions,
see the tutorials on command-line parsing.
-
Calls NLPParse().
-
Based on the parse results, it calls
into ParseC_XXX() to determine if the parse makes sense.
Command-line parsing calls into Parse_XXX() methods.
-
The ParseC_XXX() method with the highest score is chosen,
and its ParseActC_XXX() method is called to have
the AI react to what the player said.
-
The AI may respond to the player's question, and in turn
ask one of it's own by calling AIConversationAskQuestion(). For
example, the AI may ask the PC his name.
Now for some more details on this process:
For the natural language processor to work, it must have
a number of "rule sets" loaded into a "parser". In the case
of conversations, the parser name used is gConversationParser.
The rule sets can be customized for each NPC, but the
default behaviour is to include the following rule sets:
-
rParserCommon - These are rules that are common
to both the command-line parser and the conversations. They
do parse converstions like "colour" to "color", and
"we're" to "we are".
-
rParserConversation - Parsing rules that all
AI's understand. These include "What time is it?" and "Do
you know where NAME lives?".
-
Pronouns - Rules that convert pronouns, such
as "it" and "him", to object IDs. This allows conversations
to use the pronouns. For more information on
pronouns in a conversation
see AIConversationPronounSet() and AIConversationPronounSetString().
-
NPC and speaker possessions - The names of all
objects held by the NPC and the speaker (but not those in
containers). This allows conversations to reference objects
held by the NPC or the player.
-
Names that the NPC knows - A list of all the
PC names that the AI has learned when the characters
identify themselves. The rules convert the names into
the character's object ID. These are the names from CustomNameGet()
and CustomNameSet().
-
Names of all NPCs on the map - A list of all
NPCs in the NPC's original map, so that their names are
converted to object IDs. It's useful for villages and cities (which
are all one one map), so that NPCs will know about other
NPCs in the city. See pAnonymous for information
on making a NPC unknown to other NPCs.
-
Names of all rooms on the map - A list of all
rooms in the NPC's original map, so that their names are
converted to object IDs. It's useful for villages and cities (which
are all one one map), so that NPCs will know where the "inn" is,
and where the "church" is. See pAnonymous for information
on making a NPC unknown to other NPCs.
-
Names of all zones, regions, and mapps - Lets the NPC answer
questions about specific zones, regions, and maps.
-
Names of all factions - Lets the NPC answer
questions about specific factions.
-
Names of all skills - Lets the NPC answer
questions about specific skills.
-
pAIConversationParser - This property may
provide additional parsers for the NPC's AI. The extra
parsers can, in turn, add new rule sets. (See below.)
-
Factions - If the NPC is a member of any factions,
the NPC's factions' AIConversationParser() method will be
called. This can add rule sets of its own, which are specific
to a faction. For example: Members of the "Leatherworkers guild" might
include rule sets that allow players to ask about leatherworking.
-
AIConversationParser() - NPCs can provide their
own AIConversationParser() methods, which can add new rule
sets. (See below.) For example: AI's from the cMerchant class
might include rule sets for "How much does XXX cost?" and "I
want to buy XXX."
cAI.PerceiveConversation() adds these parser's by calling
into the AI's AIConversationParser(). This method
adds many of the above rules into the gConversationParser parser.
It also calls oParserConversation.AIConversationParser(),
which adds the rest. This object supplies most of the conversation
code and methods that are common to all AIs, so that each AI
doesn't have to keep track of the hundreds of conversation methods,
such as ParseC_XXX() and ParseActC_XXX(). It provides a similar
role to oParserVerb for commands. More rule sets are also
added by calling AIConversationParser() for each of the factions
that the AI is a member of, allowing the AI to "know" information
specific to the faction. Since AIConversationParser() is
layerable, sub-classes of the AI, such as cMerchant, can
also provide extra rule sets.
When AIConversationParser() is called in an object, it does
the following:
-
Identify the rule sets it wishes to activate,
such as rParserCommon from above.
-
See if the rule set is already loaded into the parser by
calling NLPRuleSetExists().
(In order to prevent
resources from piling up, rule sets will be deleted from time to
time, so it's always a good idea to make sure the rule set
still exists, even if you're sure it was created for the last
conversation.)
-
If the rule set exists, the method should
call NLPRuleSetEnableSet() to make sure the rule
set is active.
Before PerceiveConversation() calls the AIConversationParser()
methods, it will have disabled all of the rule
sets in gConversationParser.
-
If the rule set does not exist, create it and
call NLPRuleSetAdd(), followed
by NLPRuleSetEnableSet().
-
If the object provides any ParserC_XXX() methods, which
it probably does, then add the object to the ParserObjects
list that's passed into the method. (See below.)
Once AIConversationParser() has been called, and all the rule
sets have been added, PerceiveConversation()
calls NLPParse() to produce a list of possible
parses for what the player said.
The first word of each hypothesis is examined:
If it begins with a "`", then ParseC_XXX() is called for each
of the objects added to ParserObjects by AIConversationParse().
XXX is the string after the "`". Thus, if the character said,
"My name is Fred", one of the hypothesis would be "`identifyself Fred".
This would cause ParseC_IdentifySelf() to be called for
each of the objects in ParserObjects. (More about this later.)
If the first word has no "`", then the method(s) called are
determined by the current conversation state.
(See AIConversationStateSet()). The method
used will be "ParseC_State_XXX", where XXX is replaced with
the state name. Thus, if the NPC asked the player, "What is your
name?", it could set the converstion state to "AskedName". Then,
if the player replied, "My name is Fred", this would result in
a "`identifyself Fred" parse, that would call ParseC_IdentifySelf().
However, if the user replied with "Fred", the first word
of the parse wouldn't have a "`", so ParseC_State_AskedName() would
be called. (If the NPC had asked for the PC's favorite color,
it could use the "AskFavoriteColor" state, so as to not
accidentally think that the PC's name was "red" or that his
favorite color was "Fred".)
The ParseC_XXX() methods accept the following parameters:
-
Actor - The AI being spoken to.
-
Speaker - The character doing the speaking.
-
CommandTokens - A list of words spoken. Some of the
words may be converted to objects. Thus, if the user said,
"I like your lantern", this might be ["`ilike", oLantern],
whereas "My name is Fred" would be ["`identifyself", "Fred"].
-
BestParse - This is the best parse for the CommandTokens
so far. If the ParseC_XXX() method comes up with a higher score,
it should overwrite the elements of BestParse(). See below.
-
Returns - None.
The ParseX_XXX() method does the following:
-
It examines the CommandTokens to see if can
make any sense of them.
-
If it can't make any sense of the tokens then
the method should just return without changing BestParse().
-
If the tokens do make sense, then the method needs
to calculate a score for how much they make sense.
Use 1.0 for a statement that makes perfect sense,
such as ["`identifyself", "Fred"]. If it makes less sense,
such as ["`identifyself", "a", "new", "person", "here"] then
return a lower value, like 0.5. If it makes very little
sense then use an even lower number.
-
If the score is more than BestParse[0], then you'll need to
replace the contents of BestParse. If it isn't then
just return. (BestParse[0] will be Undefined if there hasn't
been a good parse yet.)
-
Replace BestParse[0] with the score you calculated.
-
BestParse[1] should contain the conversation state(s) that
this response is valid for. This is either a single lower-case string,
or a list of lower-case strings. They can contain
wildcards. (See ConversationStateCompare().)
If the conversation states in BestParse[1] do not match
any of the current conversation state the the AI is listening for
then the score will be automatically reduced. This way, if
a player's response is ambiguous, the AI can use the expected
conversation state to disambiguate. Example: If the AI asks the
player a question and the player answers, "Yes, I do", resulting
in in ["`answeryes"], the BestParse[1] state will allow the
AI to determine if the "Yes" belonged to "Do you like me?"
("askiflikeme" conversation state) or "Do you like eating turkey?"
("askifliketurkey" conversation state).
Furthermore, if the player responds with a question or answer
that isn't in the AI's current conversation state, this will
annoy the AI slightly and reduce the AI's like/trust for
the PC. A change in topic is determined by comparing the
BestParse[1] state with the AI's converastion state
set in AIConversationStateSet().
-
BestParse[2] should be filled with a callback
that acts on the speech . Usually, this will
be "this.ParseActC_XXX", where XXX is the first word
in the parse, such as "this.ParseActC_IdentifySelf".
-
BestParse[3] and higher are passed into
the BestParse[2] callback. Their meaning is up to the
BestParse[2] callback.
After all the ParseC_XXX() methods have been called, the
one that filled in the highest BestParse[0] will be kept.
NOTE: The values of BestParse[0] are adjusted down if
the conversation states in BestParse[1] do not match
the AI's current conversation state,
from AIConversationStateGet(). The BestParse[0]
score is also multiplied by the score produced by
the rule-set parsing.
If no parse was successful, then AIConversationDontUnderstand() is
called. This will cause the AI to answer with,
"Sorry, I don't understand.", or something to the effect.
Otherwise, the callback from BestParse[2] will be called.
It should accept the following parameters:
The ParseActC_XXX() method should do the following:
-
Have the NPC speak or act based on how it
interprets the speech.
-
The NPC might wish to call AILikeSet() if
what the player says causes the NPC to change his opinion
of the player.
Tip: If something that the player says positively
impaces the AI's view of the PC, make sure to use AIMemoryGet() and
AIMemorySet() to remember the event. Otherwise, players will discover
that every time the compliment a NPC about their hair, the
NPC's like/trust will go up. You'll find players repeated complimenting
a NPC about its hair until the NPC is super-friendly.
-
If the conversation sentence mentions an object that might
be used as a pronoun later, make sure to
call AIConversationPronounSet().
-
If the AI's converation state won't change based on the sentence,
then return NULL. This is particularly useful
for emotes, since many times an emote will just be ignored
by the AI, and it will sit waiting for some speech.
-
If the AI always responds to this parse with
a question, then code the question into this method and
call AIConversationStateSet() to set the new state.
Then, return TRUE.
Example: If this method handles the player asking, "Where is
the cave of Wigeywom?" and the NPC's reaction to this is
always, "Why do you want to know?", then this response should
be coded in, and the new conversation state should be set to
"npc_asks_why_want_to_know_wigeywom".
-
If the AI has no particular follow-up response then
return the new conversation state. This can either be a single
lower-case string, or list of lower-case strings. Normally,
the like/trust penalty for the PC jumping to a different
topic is [-0.5, 0]. If you wish a different pentaly, then
return a list of states, but set the first element of the
list to your [Like, Trust] change.
For example: The NPC asks the PC, "What's your name?". This
method interpret's the player's answer of "My name is Fred."
Since nothing logically follows, returning a new state
will allow the AI's intelligence to select a new topic
of conversation by calling AIConversationQuestionAsk().
See below for details.
Conversations are not just an interrogation where the player
asks the NPC a series of questions and the NPC dutifully answers.
NPCs have their own agendas... When a conversation is first
begun, the NPC AI code initializes a list of "questions" that the
NPC wants to ask the PC.
These "questions" can also include statements. Some examples
of questions are:
-
Greeting - When a conversation first starts the
NPC will say "Hello" (or similar) to the PC.
-
What is your name? - If the player has not
given his name, the NPC will ask for it. (Depending upon the
NPC's personality.)
-
My name is X. - The NPC may wish to identify itself
to the player.
-
Rumors - If there's a hot rumor going around,
or even just the latest sports trivia, the NPC may find a good
opportunity to relate it.
-
Friendly questions - The NPC may ask some friendly
questions, such as, "Where do you come from?", "Do you have
any family nearby?", etc.
-
Interrogating questions - The NPC may wish to learn
more about the player, and might have questions to ask
like, "To what duke does your allegiance lie?".
To programatically add a question to the NPC's list,
call AIConversationQuestionAdd(). You'll need to
pass in a few parameters. Some parameters to note are:
-
Priority - Higher priority questions are more likely
to be asked.
-
ConversationState - The NPC will try to bring
up questions that are appropriate to the current conversation
state. If the player brings up fishing, the AI is more likely
to mention the latest rumor about the giant turtle in the lake.
If the conversation states don't match then the effective
priority will be about 1/4 of the priority value.
-
Callback or speak string - If the question is chosen
to be spoken, then a callback will be called. (See below.) If
you don't need a callback, then just pass in a string to be
spoken.
The callback accepts the following parameters:
-
Actor - NPC/AI object.
-
Speaker - Character (PC) that the AI is speaking to.
-
Parameter - A parameter passed in
when AIConversationQuestionAdd() is called.
-
Returns - The callback needs to
return the new conversation state(s) to use after the
question (or rumor) has been spoken. This is either a lower-case
string, or a list of lower case strings. Normally, the non-sequiter
penality for [Like, Trust] is [-0.5, 0]. If you wish to change this,
then return a list of conversation states, with the first element
being the [Like, Truest] change for a non-sequiter.
You may wish to call AIConversationQuestionAdd() in some of
the ParseActC_XXX() methods if the player speaks something
that trigger's the NPC's memory or curiosity. For example: If the
player mentions "The order of the rising baloon", the NPC may
become suspicious of the PC and wish to ask questions that
allay or verify the suspicions. One way to do this would be
to use AIConversationQuestionAdd() to add a number of questions
about "The order of the rising baloon".
You can also call AIConversationQuestionRemove() to
remove a question from the list.
Most questions are added in
the AIConversationQuestionFill() method. This method
(part of the cAI class) is called whenever a conversation is
started up. It is passed +1 is the AI initiates the conversation,
and -1 if the PC initiates the conversation. (It's also called
if all the AI's questions have been used up and it needs new ones.
A value if 0 is then passed in.)
The default implimenation of AIConversationQuestionFill() calls
the same method in all the AI's factions, as well as those
objects listed in pAIConversationParser. It also calls
oParserConversation.AIConversationQuestionFill(). The method
is layerable, so sub-classes can include their own questions.
For example: You could have a cMobileLikesToTellJokes that would
occasionally tell a joke using AIConversationQuestionFill().
oParserConveration's version of AIConversationQuestionFill() will
set up some default "questions" that are common to all NPCs.
This includes a "question" that causes the NPC to say "Hello" (or
similar) and ask the player's name (depending on the AI's
personality properties).
The added questions are used when a ParseActC_XXX() method
returns a conversation state or list of conversation states. (But
not if the callback returns NULL or TRUE.)
To select a question to ask, AIConversationQuestionAsk() is
called.
This method finds the question with the highest score,
given the current conversation state. If the score is less than
a value calculated from the
AI's pAIConversationTalkativeness personality property
then the AI won't say anything. (You might be able to make the
talkativeness property for some AIs vary depending upon how
drunk they are.) Otherwise, the AI will remove the question from
the list (so it's not spoken again) and then execute the callback,
or speak the string.
In some cases, automatically removing the question may cause
some problems for a conversation. For example: If the AI asks
the PC for his name, but the PC evades the answer, the AI won't
ask for the PC's name until the next conversation. You may want
some AIs to keep asking the player character's name until the AI gets
an answer. To do this, you can play the following trick:
-
Have the question callback clear the list of questions
by calling AIConversationQuestionRemove(Actor, NULL).
-
The next time the AI has the opportunity to ask a question,
it will realize that there are no questions left. This
will cause it to call AIConversationQuestionFill().
-
Have the AI's AIConversationQuestionFill() code test to make
sure that the question was answered. If it wasn't, add it
back in with a very high priority (such as 4.0).
Built-in parsers and questions
oParserConversation includes many built-in parsers and questions
that the AI will ask. Some of them include:
-
ParseC_AskAIName allows the player to ask the
AI its name.
-
ParseC_Emote deals with emotes from the player.
-
ParseC_Goodbye lets the player say goodbye to the NPC.
-
ParseC_Hello allows the player to say hello.
-
ParseC_IdentifySelf is called when the player
identifies himself, so says, "My name is XXX."
-
ParseC_State_NameOnly is activated when the AI
asks the player his name. This allows a one or two word response.
-
QuestionC_AskPCName causes the AI to ask
the player's name.
-
QuestionC_GoAway tells the player to go away
and ends the conversation.
-
QuestionC_Goodbye causes the AI to say
goodbye and ends the conversation.
-
QuestionC_Hello lets the AI say hello.
-
QuestionC_SayName causes the AI to say
it's name.
-
The "anything" conversation state is used when
the AI is expecting any (normal) topic. The AI will expect this
state after the AI and PC have said hellos.
Share with your friends: |