Chapter 6. Pretending to Be an Individual: Synthetic Personality

Believable Characters

As can be seen by the definitions of engagement and interaction presented in Chapter 2, “The Interaction Hierarchy” (and held up by research in game development as well as other media), some sense of story is critical in the creation of engagement and immersion in a video game. As mentioned previously, this sense of story can be provided by believable characters that support what the game designer has in mind for the game’s story.

It is theorized that we engage in fictional media by simulating the dramatic events therein as if we were an actual participant in the events. An alternate theory suggests that we place these fictional elements into a functionally distinct role characterized as a “possible world box.” In either case, what we are doing here is running a mental model of the fiction, and any disruption of the fiction destroys our suspension of disbelief and our feelings of affinity for the characters in the fiction, and thus ultimately destroys our immersion in the fictional world.

Further, the emotional responses generated by fiction are most often sympathetic in nature; we’re not sad because we’ve suffered a dramatic loss, we’re sad because a character suffered the loss, and we can identify with the character. Crucial to this identification with the character is belief in the character (whether it is simply a suspension of disbelief—i.e., engagement—or active creation of belief—i.e., immersion). In traditional media, creating believable characters is facilitated by dramatic roles or archetypes. Without putting too fine a point on it, these archetypes are merely generalizations (or iconic representations, if you will) of actual individuals.

Why do these stereotypical character types make it easier? The answer is that these roles come with baggage. We expect characters in certain roles to behave in certain ways. We expect the hero to mostly do what is right, while the villain mostly acts in his own interest without regard to whether his actions are right or wrong. These expectations turn out to be really important. Remember that intentionality is defined as the perceived link between a character’s actions and its mental state; in order for this link to be believable, the character must be able to not only behave in ways that conform to its dramatic role, but also to consistently express emotional and social cues. Wouldn’t Jentsch and Freud be proud?

When playing multiplayer games, the emotional and social aspects of the role each player has adopted for himself or herself are the glue that holds the game world together. To be successful, however, games must include non-player characters for about a million and one reasons, so it stands to reason that these characters must be able to fill in for missing human players. That’s a problem in current games.

In her book Programming Believable Characters for Computer Games, Penny Baillie-de Byl suggests that the solution is to create an intelligent and freely acting NPC capable of perceiving and adapting to changes in its environment through behaviors that appear rational yet are emotionally driven. That is certainly a difficult task. It’s a goal that we can’t achieve by brute force; the interactions between humans and NPCs are too complex to be statically scripted and remain believable.

Research has shown that humans are very willing to respond to machines and other inanimate objects that have been endowed with human-like personality characteristics as though the objects actually have personalities. It has also been shown that the more interesting the personalities are in a piece of fiction, the higher the engagement and immersion. Adding both of those concepts together, we can see that the addition of personality to an NPC is probably a good thing, provided we can maintain the required balance in the fidelity hierarchy.

It is interesting to note that the very word “personality” can be linked back to prototypical dramatic role behavior. The etymology of the word extends back to the Latin persona, or mask. In the dramatic venues of the time, masks were not used to obfuscate an actor’s identify, but actually to convey the character’s dramatic role. How nice of them to use a word that supports my way of thinking! Twinkies for them!

Creating a believable mapping from personality traits to behaviors, however, is probably as hard as creating believable behaviors strictly from synthetic cognition. Many researchers over the years have explored various aspects of modeling emotion-based behavior with some success, but generally speaking, these explorations have ignored the big picture—that is, the underpinning psychological mechanisms that drive the human emotion engine, the personality. In other words, most projects merely attempt to model some emotional behavior in isolation—another brute-force methodology—such as having a state machine pick which expression to plaster on a vaguely egg-shaped, frog-like creature from outer space.

Most people attribute emotion as behavior expressing personality, so let’s define emotional fidelity as the quality of intentionality and personality modeled by the character. This definition allows us to complete the fidelity hierarchy we started in Chapter 5, “Gidgets, Gadgets, and Gizbots,” shown in Table 6.1.

We know from research in cognitive science that human cognition is heavily influenced by our emotions. Emotions, however, are merely an expression of personality. In fact, the term personality dynamics describes the combination of emotions, mood, and disposition, and this combination describes the personality

Table 6.1. Full Fidelity Hierarchy

Fidelity

Required Level

Success

Failure

Modeling

Appropriate to design

Looks right

Uncanny

Animation

Higher than modeling fidelity

Moves right

Zombie

Cognitive

Higher than animation fidelity

Acts right

Puppet

Emotional

Higher than cognitive fidelity

Feels right

Robot

through expressive behavior. Since personality dynamics are the expression of individuality through behavior, we don’t observe personality directly. Rather, we observe behavior, generalize and abstract these behaviors, and then use our observations to classify other people in stereotypical roles, like Type A, Driven, Creative Type, Mad Scientist, Evil Genius, etc. (Wait a minute! Those things sound suspiciously like dramatic roles with less dramatic names!)

Psychologist and cognitive scientists have been observing behavior in an attempt to classify people for years, just with a bit more objectivity. Scientific personality analysis hinges on one of many personality theories. These personality theories are concerned with the structure of the human psyche, which subsumes the explanation of how individual psychological processes are organized. Further, the study of personality can be divided across two central camps: nomothetic versus idiographic theories. Idiographic theories are concerned only with the unique identifiers of a specific individual. Since that isn’t very helpful in our current pursuit, we get to ignore them. Nomothetic theories, on the other hand, are more concerned with identifying those traits or laws that apply to large groups (or cultures) of individuals.

Like many, many things, there is no consensus among psychologists on the definition of personality. Most would probably agree, however, that personality can be defined as a set of attributes—behavioral, temperamental, emotional, cognitive, etc.—that characterizes a unique individual. That’s a great definition, in that we can simply put “synthetic” in front of “personality” and derive a nice definition for modeled personality in artificial characters. Since an individual’s beliefs, desires, emotions, goals, reactions to social cues, behaviors, ways of thinking, etc. are certainly part of what creates individuality, synthetic personality is clearly part of what dictates a character’s actions (and possibly should be the overriding force in action selection) and must be considered part of the character’s state of mind. That’s good news in terms of believability and intentionality.

How can we model one of these personality things, then? The place to start is where any budding personality psychologist would start: by selecting a theory to follow. There are many to choose from, but few that work well with computational modeling.

  • Psychodynamic theories. These theories, espoused by Freud (evidently, he didn’t spend all his time reading horror fiction), Jung, Adler, Eriksson, etc., are very prevalent in psychological research. We can safely ignore these theories simply because they emphasize psychic determinism, psychosexual development, and other unconscious processes that determine personality and behavior. These things would require more work to model and get right than the personality itself.

  • Humanistic theories. These theories find their roots in the research of Rogers, Maslow, and others, and are also unsuitable for synthetic personality because they are often subjective or lack a method of differentiating archetypical behaviors.

  • Behavioral theories. These theories stem from the works of Skinner, Staats, and others. The work of Staats and his students can be easily adapted to personality modeling, although the work does not seem adequate on its own to fully differentiate an individual. Other behavioral scientists’ work in this area cannot be easily adapted, although it might make excellent synthetic personalities if the effort is made to adapt them.

  • Trait-based, or taxonomic, theories. These theories have been hypothesized by Allport, Catell, McRae, Costa, and others, and represent the easiest fit for computational modeling. Trait-based theories are the best fit because of their very nature. These theories claim that personality can be differentiated by a finite list of traits. Traits are viewed as relatively stable over time, but mutable given consistent effort to change. They differ from one individual to the next, and they can definitely be linked to behavior. In computational terms, using a trait-based theory means our synthetic personalities can be described by a group of values, some way of interpreting those values, and some method of influencing action selection based on those values. Also, trait-based personality theory has perhaps the most clinical research of any of the theories (an argument can be made for including trait-based diagnostic criteria for mental health), so if your work requires photo-realistic modeling, you will have resources for creating hyper-realistic personalities as well.

Of the trait-based theories, the Five Factor Model (FFM), also called The Big Five, is perhaps the best known and certainly the most researched. The FFM breaks personalities across five continua:

  • Openness. Openness involves the active seeking and appreciation of experiences for their own sake. Open individuals are independent, imaginative, and willing to entertain novel ideas and unconventional values. They experience the whole gamut of emotions more vividly than do closed individuals (those with low openness measures). Closed individuals are often practical, conventional, dogmatic, rigid in their beliefs, set in their ways, and emotionally unresponsive.

  • Conscientiousness. Conscientious individuals are more organized, persistent, disciplined, and motivated in goal-directed behavior. Low measures of conscientiousness often indicate careless, unreliable, disorganized, negligent, and hedonistic individuals.

  • Extroversion. This refers to the quantity and intensity of preferred interpersonal interactions, activity level, need for stimulation, and capacity for joy. A person measuring high on the extroversion scale is likely to be sociable, active, talkative, people oriented, fun loving, and optimistic. Conversely, a person measuring low on the extroversion scale is likely to be reserved, sober, independent, and quiet.

  • Agreeableness. This interpersonal dimension refers to the kinds of interactions a person prefers along the continuum from compassion to antagonism. Highly agreeable individuals are often soft hearted, good natured, trusting, helpful, forgiving, and altruistic. They tend to be responsive and empathic, and believe most others are cut from the same cloth. Individuals with a low agreeableness measure are likely to be cynical, rude, abrasive, suspicious, uncooperative, and irritable. They can be manipulative, vengeful, and ruthless.

  • Neuroticism. Neuroticism is defined as a chronic level of emotional adjustment and instability. High measures of neuroticism identify subjects who are prone to psychological distress. These individuals commonly display a negative affect, unrealistic ideas, excessive cravings, difficulty tolerating frustration caused by not acting on their cravings, and a maladaptive coping response. Low levels of neuroticism indicate a calm, secure, and self-satisfied individual.

In the clinical research, these individual rankings across these continua are typically reduced to normal, abnormally high, and abnormally low values, but in a computational model, it is handy to express individual values across a range.

These are great for a general model of personality, but we are certainly not limited to using only these (unless, of course, you insist on those photo-realistic character models …). In fact, it may be wise to allow the traits to be defined specifically for the context in which they will be used. For example, in a combat game, character traits might be leadership, courage, drive, camaraderie, etc., while those specific traits mean little in the Suma Sumi game we’re developing. What’s important to realize is that the architecture itself is the important piece of the puzzle. The implementation is merely that: implementation for content delivery.

The hard part is, as AI guru John Laird said, the mapping of personality to behavior, including a suitable manner of expressing emotion. Let’s come back to this point after doing some design and coding to start adding synthetic personality to Suma Sumi. Instead of just coming up with a few traits at random, let’s pursue a top-down design process by starting with the roles we would like characters to fill in this game. If it helps, we can think of these roles as personality types that are completely defined by traits. Here are six general types to play with, which will be plenty for our current needs.

  • Absent-minded despot (AMD). Given that the Suma are bent on destroying worlds but seem to have problems with boredom and focus, I think our first type should be absent-minded despot. AMDs are aggressive, tyrannical, territorial, and ascetic; they live for conquest. They would hold a grudge if they could remember to do so, but they are also very forgetful.

  • Timid underling (TU). TUs are timid (bet you didn’t see that one coming, Valued Reader), shy, indecisive followers. This type of Suma wants to avoid even the appearance of conflict, and will go along with anything that comes from a strong-willed Suma. They have good memories and may hold a grudge, but no one would ever know it unless they are backed into a corner—in which case they turn vicious.

  • Lone wolf (LW). The lone wolf type of Suma is solitary, self-sufficient, agoraphobic, undependable in social situations, and aloof. They don’t care enough about other people to hold any kind of grudge, and it takes a lot to earn a LW’s liking and respect.

  • Sumacidal explorer (SE). SEs are the homicidal maniacs of the Suma. SEs are aggressive, blood thirsty, and violent, yet driven to pursue novelty. Instead of holding grudges, SEs just try to murder anyone who offends their sensibilities. Once that’s over, it’s over, and they seem to forget all about the confrontation.

  • Egocentric narcissist (EN). Egocentric narcissists are mainly interested in themselves and their own pleasure. They may occasionally notice others, and may even interact with them for a short period of time, but always believe happiness is a self-generated quality. That said, they are not driven to be solitary like LWs, and can maintain close proximity to other Suma. ENs care so little about anyone other than themselves that they can’t be bothered to hold a grudge.

  • Plural hedonist (PH). Plural hedonists care only about pleasure, but are egregiously gregarious. PH types want constant company; if they can’t have it, they tend toward melancholy and a sour outlook on life. If snubbed socially, they will hold a grudge for an extended period of time.

What defining attributes can we distill from this list of types? First, we can see the need for some kind of social continuum—we have a range of avoiding others to always wanting a partner. LW Sumas are on the extreme negative end of the scale, while PH types are on the extreme positive end. AMDs probably define the center (or close to it), with TUs being more positive and ENs being slightly more negative. Because SEs are driven toward novelty, we will say that SEs are slightly more positive than center. We will call this trait sociability. This continuum is shown in Figure 6.1.

Populated sociability continuum.

Figure 6.1. Populated sociability continuum.

Next, we can see that some of our types are driven by the pursuit of pleasure, while others are driven by aggression. While this may seem like a weird continuum to put together, it illustrates a very important point: Traits can be defined any way we need or want them to be defined in order to best support the design and implementation. We don’t need to stick to psychologically defined traits. That said, let’s define this continuum such that aggression is on the extreme negative scale (i.e., as the pursuit of the opposite of pleasure) and hedonism is on the extreme positive end. Obviously, AMDs and SEs populate the extreme negative side, and both PHs and ENs are on the extreme positive range. TUs are on the negative side of center, while LWs could probably be defined as center or slightly positive. We will call this trait hedonism; its continuum is shown in Figure 6.2.

Populated hedonism continuum.

Figure 6.2. Populated hedonism continuum.

The next range of behaviors I can see is affected by respect and discipline. AMDs demand maximum respect and are very self disciplined. LWs also demand that others respect their space, but they are less self disciplined. TUs demand little respect and show marginal self discipline (e.g., if an AMD tells them they are doing it wrong, they will follow the AMD’s instructions). ENs demand no respect and show no self discipline. PHs demand a little respect socially but also show no self discipline. SEs demand maximum respect but show less self discipline than AMDs. Let’s call this trait asceticism. Figure 6.3 shows the types in relation to this trait.

Populated asceticism continuum.

Figure 6.3. Populated asceticism continuum.

We need three traits to adequately describe the whole “holds a grudge” bit. First, we need to know how vindictive a Suma is. We can see that AMDs, SEs, and TUs are vindictive, while PHs are only marginally so. Both LWs and ENs are negative on the scale. The vindictiveness continuum is shown in Figure 6.4.

Populated vindictiveness continuum.

Figure 6.4. Populated vindictiveness continuum.

Second, we need a forgetfulness trait. AMDs are maximally forgetful, as are SEs. PHs and TUs, on the other hand, have long memories. ENs have adequate memories, as do LWs. Figure 6.5 displays the populated forgetfulness continuum.

Populated forgetfulness continuum.

Figure 6.5. Populated forgetfulness continuum.

Third, the trait we need represents the willingness to act or change their actions. SEs are on the extreme positive range. AMDs are on the positive side, with both ENs and PHs representative of center, although EN is slightly negative and PH is slightly positive. TUs are extremely negative, with LWs moderately positive. The initiative trait is populated in Figure 6.6. Table 6.2 shows an overview of these traits.

Populated initiative continuum.

Figure 6.6. Populated initiative continuum.

Table 6.2. Personality Types Defined by Trait

Type

Sociability

Hedonism

Asceticism

Vindictiveness

Forgetfulness

Initiative

AMD

1

−10

+10

+10

+10

+8

TU

+7

−7

−2

+7

−10

−10

LW

−10

1

+2

−10

1

+5

SE

+2

−10

+7

+10

+9

+10

EN

−2

+10

−10

−10

1

−10

PH

+100

+10

−8

+4

−10

+10

We will start a new project for this chapter named SumaSumiV2. Import everything from the SumaSumi project we completed in Chapter 5. In essence, we’ll be picking up where we left off, and adding our synthetic personalities to our existing characters.

To codify the information contained in Table 6.1, we need to add a couple of classes. To keep everthing organized, we need a new folder inside the Character folder named Personality. Inside the new folder, create a new class, named PersonalityType, and populate it thusly:

namespace _2DSpriteGame.Character.Personality
{
    public class PersonalityType
    {
        #region members
        public enum PersonalityTypes { AMD = 0, TU, LW, SE, EN, PH };
        private float s; //sociability trait value
        private float h; //hedonism trait value
        private float a; //asceticism trait value
        private float v; //vindictiveness trait value
        private float f; //forgetfulness trait value
        private float i; //initiative trait value
        private PersonalityTypes type;
        
        #endregion
        
        #region properties
        public float Sociability
        {
            get { return s; }
        }
        public float Hedonism
        {
            get { return h; }
        }
        public float Asceticism
        {
            get { return a; }
        }
        public float Vindictiveness
        {
            get { return v; }
        }
        public float Forgetfulness
        {
            get { return f; }
        }
        public float Initiative
        {
            get { return i; }
        }
        #endregion
        
        #region constructor / init
        public PersonalityType(PersonalityTypes t)
        {
            type = t;
        
            switch ( type )
        
            {
                case PersonalityTypes.AMD:
                   s = 1f;
                   h = -10f;
                   a = 10f;
                   v = 10f;
                   f = 10f;
                   i = 8f;
                   break;
               case PersonalityTypes.TU:
                  s = 7f;
                  h = -7f;
                  a = -2f;
                  v = 7f;
                  f = -10f;
                  i = -10f;
                  break;
              case PersonalityTypes.LW:
                  s = -10f;
                  h = 1f;
                  a = 2f;
                  v = -10f;
                  f = 1f;
                  i = 5f;
                  break;
              case PersonalityTypes.SE:
                  s = 2f;
                  h = -10f;
                  a = 7f;
                  v = 10f;
                  f = 9f;
                  i = 10f;
                  break;
              case PersonalityTypes.EN:
                  s = -2f;
                  h = 10f;
                  a = -10f;
                  v = -10f;
                  f = 1f;
                  i = -10f;
                  break;
              case PersonalityTypes.PH:
                  s = 10f;
        
                  h = 10f;
                  a = -8f;
                  v = 4f;
                  f = -10f;
                  i = 10f;
                  break;
              default:
                  s = 0f;
                  h = 0f;
                  a = 0f;
                  v = 0f;
                  f = 0f;
                  i = 0f;
                  break;
            }
        }
        #endregion
    }
}

All of that was very simple, and very, uh … what’s the right word … oh, yes: meaningless. It’s meaningless for all the obvious reasons (not hooked up to anything in the game, this is just a random collection of integers, etc.), but more to the point, because these values currently have no impact on behavior or mental state. Let’s start with an easy one in the easier of the two problems I just mentioned.

The easiest way we can use this system to affect the mental state of a character is to use the initiative trait in the current Character class’s changeDesire calculation. If you remember from Chapter 5, we set this value up to help make our little green … er, frog men look like they were picking new actions at different times from one another. I also promised at the time that we would change that calculation later (it is currently just a random number roll).

To get started, we will add a private PersonalityType member to the Character class and initialize the object in the Character class’s constructor so that there is always at least one of each type. That code looks like this:

//add personality model
if (index < 6)
{
    pt = new Personality.PersonalityType((Personality.PersonalityType.
         PersonalityTypes)index);
}
else
{
    int prob = rand.Next(100);

    if (prob < 16)
    {
        //16 percent chance to be an absent-minded despot
        pt = new Personality.PersonalityType(Personality.PersonalityType.
             PersonalityTypes.AMD);
    }
    else if (prob < 32)
    {
        //15 percent chance to be an egocentric narcissist
        pt = new Personality.PersonalityType(Personality.PersonalityType.
             PersonalityTypes.EN);
    }
    else if (prob < 48)
    {
        //16 percent chance to be a lone wolf
        pt = new Personality.PersonalityType(Personality.PersonalityType.
             PersonalityTypes.LW);
    }
    else if (prob < 64)
    {
        //16 percent chance to be a sumacidal explorer
        pt = new Personality.PersonalityType(Personality.PersonalityType.
             PersonalityTypes.SE);
    }
    else if (prob < 80)
    {
        //16 percent chance to be a plural hedonist
        pt = new Personality.PersonalityType(Personality.PersonalityType.
             PersonalityTypes.PH);
   }
   else
   {
        //20 percent chance to be a timid underling
        pt = new Personality.PersonalityType(Personality.PersonalityType.
             PersonalityTypes.TU);
   }
}

Again, this code so far is just basic stuff we’ve done before. To start using this model, we will use the initiative trait to alter the changeDesire member, like this:

changeDesire = ((float)rand.NextDouble() * 15000f) 1500f * pt.Initiative;

To use this system (which now may produce a negative value for changeDesire that is higher than gameTime.TotalGameTime.Milliseconds will ever be), we need to change the way we use it. Before, we could get away with this:

updateMillies += gameTime.TotalGameTime.Milliseconds + changeDesire;
int testVal = (int)(updateMillies / MILLIES_INTERVAL);

With these changes, however, changeDesire might be negative, and it will almost certainly always be larger than gameTime.TotalGameTime.Milliseconds, which means that updateMillies will continually go more and more negative. We need to do this instead:

updateMillies += gameTime.TotalGameTime.Milliseconds;
int testVal = (int)(updateMillies / (MILLIES_INTERVAL + changeDesire));

What this simple line of code does is change the guard time for the behavior cycle from 33,500–35,000 milliseconds to 20,000–65,000 milliseconds. We also need this changeDesire to affect the whole stateAge limitation on selecting new behaviors, so we will change this:

//check to see if we should change activities based on boredom
if (stateAge > STATE_INTERVAL)

to this:

//check to see if we should change activities based on boredom
if (stateAge > (STATE_INTERVAL + changeDesire))

With that done, we can really see a difference in how quickly these guys change states, and can immediately see that 500,000 milliseconds is too long an interval on new behavior selection. For now, I’ve settled on these values (and may need to retune them after other synthetic personality additions):

private const float MILLIES_INTERVAL = 35000f;
private const float STATE_INTERVAL = 150000f;
private const float POS_INTERVAL = 75000f;

Next, we need to think about other areas of low-hanging fruit for applying these values. It seems to me that the random way we initialize likes and dislikes could be affected by these personality traits. For instance, someone who is asocial will probably like others a lot less than someone who is social, so it stands to reason that when we are initializing the attraction array, we could take a big step toward meaningful individuality by using the sociability factor in some way. We want the range to continue to be somewhat random, and still to fall between −100 and + 100, so we can change this:

neg = r.Next(100);

val = (float)r.NextDouble() * 100f;

if (neg < 50)
{
    attraction[i] = val;
}
else
{
   attraction[i] = -1 * val;
}

to the following and maintain our design:

for (int i = 0; i < tc; i++)
{
    attraction[i] = ((float)rand.NextDouble() * 10f) * pt.Sociability;
}

Further, the way the characters change their attraction to other characters should be affected by this value. So instead of

float attract = -1 * ATT_CHANGE;

we should use the following:

float attract = pt.Sociability / 10f * ATT_CHANGE;

However, that change will pretty much take out the rest of the way we process attraction changes. To avoid that, we need to do this:

//check to see if we should make a positive change because we like this activity
if (stateAge < POS_INTERVAL)
{
    if (activities[actID] > -10f)
    {
        delta *= -1;
    if (pt.Sociability < 0f)
    {
        attract *= 10f / pt.Sociability;
    }
    else
    {
        attract *= pt.Sociability;
    }
  }
}

We need to do this as well so that the character’s sociability trait also affects how much the affinity value is changed. What we want here is for asocial characters to really dislike social activities and vice-versa. Here is the change:

if (charID > -1)
{
    //if we dislike this guy, affinity change gets worse
    if (attraction[charID] < -10f)
    {
        delta *= pt.Sociability / 3f;
    }
    ChangeAttraction(charID, attract);
}

Making changes to how we calculate activity affinities is a bit more complicated (but not much, given our design). We’ll start by adding this code to the PersonalityTypes class:

public float GetChaseAffinity()
{
    //positively affected by Asceticism, Vindictiveness, and Initiative,
    //negatively affected by Forgetfulness
    return ((a * 2f) + v + i * 3f - (f * 5f));
}
public float GetEvadeAffinity()
{
   //positively affected by Asceticism and Initiative, negatively affected by
   //Forgetfulness, Vindictiveness, and Sociability
   return ((a * 5f) + (i * 5f) - (f * 3f) - (v * 2.5f) - (s * 2f));
}
public float GetForgetAffinity()
{
   //only affected by Forgetfulness
   return (f * 8f);
}
public float GetMoveAffinity()
{
    //only affected by Initative and Asceticism, and both in the positive
    return ((i * 5f) + (a * 5f));
}
public float GetRandomWalkAffinity()
{
    //affected positively by low Asceticsm and high Initiative.
    //Marginal Forgetfulness impact and negative Sociability impact
    return ((50f / a) + (i * 2f) + f - (s * 8f));
}
public float GetSleepAffinity()
{
    //positively affected by Hedonism, negatively affected by Asceticism,
    //Sociability, and Initiative
    return ((h * 8f) - (a * 3f) - (s /2f) - i);
}
public float GetStopAffinity()
{
    //positively affected by Asceticism, negatively affected by Forgetfulness
    //and Initiative
    return ((a * 6f) - (f * 3f) - (i * 2f));
}
public float GetGoToAffinity()
{
    //positively affected by Initiative, Asceticism, and Sociability
    return (GetMoveAffinity() + (s * 3f));
}
public float GetDanceAffinity()
{
    //positively affected by Hedonism and Initiative, negatively affected by
    //Asceticism and Sociability
    return ((h * 7f) + (i * 3f) - (a * 3f) - (s * 5f));
}
public float GetDanceWithAffinity()
{
    //positively affected by Hedonism, Sociability, and Initiative, negatively
    //affected by Asceticism
    return ((h * 5f) + (s * 4f) + i - (a * 5f));
}
public float GetFightAffinity()
{
    //positively affected by Asceticism, Initiative, and Vindictiveness,
    //negatively affected by Hedonism (this may be social if both parties
    //enjoy fighting!!)
    return ((a * 5f) + (i * 3f) + (v * 2f) - (h * 5f));
}
public float GetChatAffinity()
{
    //positively affected by Sociability and Hedonism, negatively affected by
    //Forgetfulness and Asceticism
    return ((s * 5f) + (h * 5f) - (f * 3f) - (a * 5f));
}
/// <summary>
/// The master affinity update method
/// </summary>
/// <param name="actID">The action id to update affinity for</param>
/// <returns>The personality trait starting affinity value</returns>
public float GetAffinity(int actID)
{
    switch (actID)
    {
        case 0:
            return GetChaseAffinity();
            break;
        case 1:
            return GetEvadeAffinity();
            break;
        case 2:
            return GetForgetAffinity();
            break;
        case 3:
            return GetMoveAffinity();
            break;
        case 4:
            return GetRandomWalkAffinity();
            break;
        case 5:
            return GetSleepAffinity();
            break;
        case 6:
            return GetStopAffinity();
            break;
        case 7:
            return GetGoToAffinity();
            break;
        case 8:
            return GetDanceAffinity();
            break;
        case 9:
            return GetDanceWithAffinity();
            break;
        case 10:
            return GetFightAffinity();
            break;
        case 11:
            return GetChatAffinity();
            break;
        default:
            return 0f;
            break;
     }
}

Then we’ll change the Character constructor with this:

for (int i = 0; i < sc; i++)
{
    activities[i] = pt.GetAffinity(i);
    if (activities[i] > 100f)
    {
        activities[i] = 100f;
    }
    else if (activities[i] < -100f)
    {
        activities[i] = -100f;
    }
}

With regard to updating the affinities for each activity, we also want that to be a personality-affected process. Therefore, we need these methods (yeah, they look familiar) in PersonalityType:

public float GetChaseAffinityUpdate(float dChase)
{
    //positively affected by Asceticism, Vindictiveness, and Initiative,
    //negatively affected by Forgetfulness
    return (dChase + (a / 5f) + (v / 10f) + (i / 3f) - f);
}
public float GetEvadeAffinityUpdate(float dEvade)
{
    //positively affected by Asceticism and Initiative, negatively affected by
    //Forgetfulness, Vindictiveness, and Sociability
    return (dEvade + (a / 3f) + (i / 3f) - (f / 3f) - (v / 7f) - (s / 5f));
}
public float GetForgetAffinityUpdate(float dForget)
{
    //only affected by Forgetfulness
    return (dForget + (f / 3f));
}
public float GetMoveAffinityUpdate(float dMove)
{
    //only affected by Initiative and Asceticism, and both in the positive
    return (dMove + (i / 3f) + (a / 5f));
}
public float GetRandomWalkAffinityUpdate(float dRandMove)
{
    //affected positively by low Asceticsm, and high Initiative.
    //Marginal Forgetfulness impact and negative Sociability impact
    return (dRandMove + (5f / a) + (i / 2f) + (f / 10f) - (s / 5f));
}
public float GetSleepAffinityUpdate(float dSleep)
{
    //positively affected by Hedonism, negatively affected by Asceticism,
    //Sociability, and Initiative
    return (dSleep + h - (a / 3f) - (s / 20f) - (i / 10f));
}
public float GetStopAffinityUpdate(float dStop)
{
    //positively affected by Asceticism, negatively affected by Forgetfulness
    //and Initiative
    return (dStop + (a / 3f) - (f / 5f) - (i / 10f));
}
public float GetGoToAffinityUpdate(float dGoto)
{
    //positively affected by Initiative, Asceticism, and Sociability
    return (GetMoveAffinityUpdate(dGoto) + (s / 3f));
}
public float GetDanceAffinityUpdate(float dDance)
{
    //positively affected by Hedonism and Initiative, negatively affected by
    //Asceticism and Sociability
    return (dDance + h + (i / 3f) - (a/3f) - s);
}
public float GetDanceWithAffinityUpdate(float dDanceWith)
{
    //positively affected by Hedonism, Sociability, and Initiative, negatively
    //affected by Asceticism
    return (dDanceWith + (h / 5f) + (s / 5f) + (i / 10f) - (a / 5f));
}
public float GetFightAffinityUpdate(float dFight)
{
    //positively affected by Asceticism, Initiative, and Vindictiveness,
    //negatively affected by Hedonism (this may be social if both parties enjoy
    //fighting!!)
    return (dFight + (a / 5f) + (i / 10f) + (v / 5f) - (h / 5f));
}
public float GetChatAffinityUpdate(float dChat)
{
    //positively affected by Sociability and Hedonism, negatively affected by
    //Forgetfulness and Asceticism
    return (dChat + (s / 2f) + (h / 5f) - (f / 3f) - (a / 5f));
}
/// <summary>
/// The master affinity update method
/// </summary>
/// <param name="actID">The action id to update affinity for</param>
/// <param name="delta">the starting update value</param>
/// <returns>The personality trait adjusted affinity value</returns>
public float GetAffinityUpdate(int actID, float delta)
{
    switch (actID)
    {
        case 0:
            return GetChaseAffinityUpdate(delta);
            break;
        case 1:
            return GetEvadeAffinityUpdate(delta);
            break;
        case 2:
            return GetForgetAffinityUpdate(delta);
            break;
        case 3:
            return GetMoveAffinityUpdate(delta);
            break;
        case 4:
            return GetRandomWalkAffinityUpdate(delta);
            break;
        case 5:
            return GetSleepAffinityUpdate(delta);
            break;
        case 6:
            return GetStopAffinityUpdate(delta);
            break;
        case 7:
            return GetGoToAffinityUpdate(delta);
            break;
        case 8:
            return GetDanceAffinityUpdate(delta);
            break;
        case 9:
            return GetDanceWithAffinityUpdate(delta);
            break;
        case 10:
             return GetFightAffinityUpdate(delta);
             break;
        case 11:
            return GetChatAffinityUpdate(delta);
            break;
        default:
            return 0f;
            break;
    }
}

To make that stuff worth typing, we need to change the way we update the activity affinities in the Character class like this:

//get the current activity code
int actID = fsm.GetStateIdKey(fsm.CurrentState.Identifier);
float delta = ACT_CHANGE;
float attract = pt.Sociability / 10f * ATT_CHANGE;

//restrict attribute changes and AI processing
if (updateMillies == -1f)
{
    updateMillies = 0f;

    delta = pt.GetAffinityUpdate(actID, delta);

    //check to see if we should make a positive change because we like this activity
    if (stateAge < POS_INTERVAL)
    {
        if (activities[actID] > -10f)
        {
            if (delta < 0)
            {
               delta *= -1;
            }
            if (pt.Sociability < 0f)
            {
                attract *= 10f / pt.Sociability;
            }
            else
            {
                attract *= pt.Sociability;
            }
          }
       }
       else
       {
       if (pt.Sociability > 0f)
       {
           attract *= 10f / pt.Sociability;
       }
       else
       {
           attract *= pt.Sociability;
       }
   }

The rest of the code works nicely with respect to the activity affinities, but the boredom calculation is now insanely fast. Good news, that, because as a result, we get to drastically simplify the boredom code. Yep, we’re going to replace this:

if (activities[actID] > 75f)
{
    //75 .. 100
    if (delta < 0)
    {
        boredom += -3 * delta;
    }
    else
    {
        boredom += 3 * delta;
    }
}
else if (activities[actID] > 50f)
{
    //50 .. 75
    if (delta < 0)
    {
        boredom += -2 * delta;
    }
    else
    {
         boredom += 2 * delta;
    }
}
else if (activities[actID] > 10f)
{
    //10 .. 50
    if (delta < 0)
    {
        boredom += -1.5f * delta;
    }
    else
    {
        boredom += 1.5f * delta;
    }
}
else if (activities[actID] > -10f)
{
    //-10 .. 10
    if (delta < 0)
    {
        boredom += -1 * delta;
    }
    else
    {
        boredom += delta;
    }
}
else if (activities[actID] > -50f)
{
    //-15 .. -10
    boredom += 1.5f * delta;
}
else if (activities[actID] > -75f)
{
    //-75 .. -50
    boredom += 2 * delta;
}
else if (activities[actID] < -74.9f)
{
    //-100 .. -75
    boredom += 2 * delta;
}

with this single line of code:

boredom += delta / 3;

Hurray for personality-driven frogs! The last little bit we need to upgrade is this:

//cause all hatred of activities and characters to degrade over time
if (boredom >= 0f)
{
    for (int j = 0; j < Game1.GameStateMan.CharManager.ToonCount; j++)
    {
         attract = ATT_CHANGE;
         if (pt.Sociability > 0f)
         {
             attract *= 10f / pt.Sociability;
         }
         else
         {
             attract *= pt.Sociability;
         }

         if (GetAttraction(j) < -85f)
         {
             ChangeAttraction(j, attract);
         }
         else if (GetAttraction(j) < -10f)
         {
             ChangeAttraction(j, (attract / 10));
         }
         else if (GetAttraction(j) < 10f)
         {
             ChangeAttraction(j, (attract / 100));
         }
    }
    for (int j = 0; j < fsm.StateCount; j++)
    {
         delta = ATT_CHANGE;
         delta = pt.GetAffinityUpdate(j, delta);
         if (delta < 0)
         {
             delta = -7.5f / delta;
         }

         if (GetActivityAffinity(j) < -85f)
         {
             ChangeActivityAffinity(j, delta);
         }
         else if (GetActivityAffinity(j) < -10f)
         {
             ChangeActivityAffinity(j, (delta / 10));
         }
         else if (GetActivityAffinity(j) < 10f)
         {
             ChangeActivityAffinity(j, (delta / 100));
         }
    }

This last bit of code uses negative values that are calculated by the personality-type code to reduce the speed of the changes. In other words, if the affinity for a certain activity is negative, it still moves toward positive, just at a slower rate than an activity that the character feels strongly positive about.

Modeling personality dynamics turns out to be pretty easy when viewed in isolation. Figuring out what to model, however, is not quite as easy. Let me explain: In the field of psychology, there are two main models of mood, emotion, and the relationship between them. The first and most popular view contends that mood and emotion are conceptually equivalent and are therefore completely interchangeable. The second model differentiates mood and emotion, mostly by the specification and duration of the event. In this second model, emotions are considered to be brief, event driven, object related, and relatively intense; emotions are also associated with distinct expressions and are seen to influence action rather than cognition. In contrast, mood is generally considered to be broader in scope, longer acting, and of significantly lower intensity. Mood is also considered to influence cognition rather than action.

Both of these definitions are insufficient. In the first approach, if we assume that mood and emotion are conceptually equal, they should not be distinguishable from one another. If this is true, then it must not be possible to distinguish intense anger from a bad mood or to identify joy versus a positive outlook, but experience with the human condition rapidly disproves this argument. Therefore, the first approach to differentiating mood and emotion must be flawed. If the second approach, which focuses on specification and deviation, is correct, then there cannot be a single example that fits the criteria for both categories. By counterexample, grief can be catastrophic in terms of intensity and duration. It can influence both action and cognition. It can also be associated with distinct expressions, and is event driven. It may be object related (such as the grief felt at the loss of a loved one) or not (for example, the grief derived from failing at an important life goal, such as completing a book on time—sorry Emi [my editor]!). Grief must be both an emotion and a mood; therefore, the second approach is also flawed.

In synthetic characters, mood and emotion can be modeled as discrete states provided a low-fidelity model is sufficient for the game’s needs, because a discrete model requires that emotions be mutually exclusive. A fuzzy model seems more appropriate, however, as the fidelity demands increase. In humans, we can experience multiple emotions simultaneously. For example, one might be angry enough to chew nails at the destruction that occurred in the living room while one’s attention was devoted to writing some silly book while at the same time amused by the silly antics of a six year old. Ask me how I know…. Ahem.

In my opinion, the Suma Sumi game design requires that emotions be modeled as fuzzy states because emotions are not mutually exclusive in this game, but moods can be modeled as discrete states. In other words, I want the Suma to be capable of being a little disgusted, moderately scared, and really, really, psychotically angry, all at the same time. While I can hear the sighs of relief from some of you, fuzzy states bring their own complications. For example, how does a fuzzy state calculate which actions are appropriate given the values of its states?

If I want to classify the overall emotional state, how do I resolve the combination of states into a single value? Yeah, I realize that is at odds with the whole multiple–emotions-at-once deal, but there are plenty of times when we, as humans, boil our complex emotional state down into a simple word or phrase—such as happy enough to dance, despite actually being surprised and happy.

The simple solution to some of these problems is to create a fuzzy state machine back-seat driver—in other words, to keep track of the state values and perform appropriate transitions, but to avoid direct action control in favor of allowing other code to access state values and determine behaviors based on the combination of values. For instance, one could, say, create a state machine that controls atomic behaviors, and then use values from a fuzzy state machine as conditionals inside the behavior machine’s action methods. One could also use the values from these fuzzy states in selecting new actions from within existing states in the behavior machine. And you thought I wanted a state machine for primitive behaviors just so I could talk about Greek letters….

So, Valued Reader, here’re a few somewhat important questions: How many emotions do we need to model to achieve sufficient fidelity? Are there any canonical or atomic emotions that we must include? I wish I had better news, but there is very little agreement among psychologists about the answer to these questions. The problem is that we all have emotions, so we all (including me) think we are all right; further, emotions are described with terms from everyday language, which come with all their connotations intact.

I do have marginally good news, however: There are researchers who investigate the so-called prototypical emotions, and three important features of atomic emotions have been identified. First, emotions arise when an individual evaluates some event or situation to be significant. Second, emotions are multifaceted beasties that involve changes in subjective experience, behavioral expression, and both central and peripheral physiology. Finally, emotions can be described as either dimensions or categories (which seems to fit nicely either with our fuzzy state methodology or with a discrete state model).

For our purposes, we can simply create six fuzzy values and treat them as equivalence classes that are differentiated by intensity. For example, the difference between mild pique and outright fury can be explained as a difference in intensity with which anger is being experienced. How shall these six equivalencies be named? Let’s use Paul Ekman’s six basic emotions: joy, anger, fear, sadness, disgust, and surprise.

Let’s create a new class in the Personality folder called EmotionModel.cs. The job of this class will be to maintain emotions and mood. In order to accomplish that task, we need to keep track of the fuzzy states themselves, the mood value, a disposition value, a lability value, and a decay rate. In addition, we will need various helper data and a few constants. Namely, we need these members:

using System;
using Microsoft.Xna.Framework;

namespace _2DSpriteGame.Character.Personality
{
    public class EmotionModel
    {
    #region members
    public enum Emotions { JOY = 0, ANGER, FEAR, SADNESS, DISGUST, SURPRISE };
    public enum Mood { BAD = 0, NEUTRAL, GOOD };

    private float[] fuzzyStates;
    private float mood;
    private float disposition;
    private float lability;
    private float decayRate;

    //helper members
    private float[] lastUpdate;
    private int[] rankOrder;
    private float moodUpdate;
    private float emoTime;
    private float moodTime;

    //constants
    private const int NUM_EMOTIONS = 6;
    private const float EMO_INTERVAL = 2500f;
    private const float MOOD_INTERVAL = 30000f;
    private const float MAX_INTENSITY = 75f;
    #endregion

    #region properties
    public float Joy
    {
        get { return fuzzyStates[(int)Emotions.JOY]; }
        set
        {
            fuzzyStates[(int)Emotions.JOY] += value * lability;
            lastUpdate[(int)Emotions.JOY] = 0f;
        }
    }
    public float Anger
    {
        get { return fuzzyStates[(int)Emotions.ANGER]; }
        set
        {
            fuzzyStates[(int)Emotions.ANGER] += value * lability;
            lastUpdate[(int)Emotions.ANGER] = 0f; ;
        }
    }
    public float Fear
    {
        get { return fuzzyStates[(int)Emotions.FEAR]; }
        set
        {
            fuzzyStates[(int)Emotions.FEAR] += value * lability;
            lastUpdate[(int)Emotions.FEAR] = 0f;
        }
    }
    public float Sadness
    {
        get { return fuzzyStates[(int)Emotions.SADNESS]; }
        set
        {
            fuzzyStates[(int)Emotions.SADNESS] += value * lability;
            lastUpdate[(int)Emotions.SADNESS] = 0f;
        }
    }
    public float Disgust
    {
        get { return fuzzyStates[(int)Emotions.DISGUST]; }
        set
        {
            fuzzyStates[(int)Emotions.DISGUST] += value * lability;
            lastUpdate[(int)Emotions.DISGUST] = 0f;
        }
     }
     public float Surprise
     {
     get { return fuzzyStates[(int)Emotions.SURPRISE]; }
     set
     {
         fuzzyStates[(int)Emotions.SURPRISE] += value * lability;
         lastUpdate[(int)Emotions.SURPRISE] = 0f;
     }
  }
  public Mood CurrentMood
  {
     get
     {
         if ( mood < -10f )
         {
             return Mood.BAD;
         }
         else if ( mood > 10f )
         {
             return Mood.GOOD;
         }
         else
         {
             return Mood.NEUTRAL;
         }
     }
  }
  #endregion

In the constructor, we will mainly accept the initial values for things, initialize our helper members, and create our first rankOrder of the emotions, like this:

#region constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="j">Initial Joy value</param>
/// <param name="a">Initial Anger value</param>
/// <param name="f">Initial Fear value</param>
/// <param name="s">Initial Sadness value</param>
/// <param name="d">Initial Disgust value</param>
/// <param name="su">Initial Surprise value</param>
/// <param name="l">Lability (rate of emotional change) value</param>
/// <param name="disp">Disposition (default mood)</param>
/// <param name="decay">Rate of decay for both emotions and mood -- decay should
/// be points per second</param>
public EmotionModel(float j, float a, float f, float s, float d, float su, float l,
    float disp, float decay)
{
    fuzzyStates = new float[NUM_EMOTIONS];
    fuzzyStates[(int)(int)Emotions.JOY] = j;
    fuzzyStates[(int)Emotions.ANGER] = a;
    fuzzyStates[(int)Emotions.FEAR] = f;
    fuzzyStates[(int)Emotions.SADNESS] = s;
    fuzzyStates[(int)Emotions.DISGUST] = d;
    fuzzyStates[(int)Emotions.SURPRISE] = su;

    lastUpdate = new float[NUM_EMOTIONS];
    for (int i = 0; i < NUM_EMOTIONS; i++)
    {
        lastUpdate[i] = 0f;
    }

    rankOrder = new int[NUM_EMOTIONS];

    CalculateRank();

    lability = l;
    disposition = disp;
    mood = disposition;
    decayRate = decay;

    emoTime = EMO_INTERVAL - (EMO_INTERVAL * lability);
    moodTime = MOOD_INTERVAL - (MOOD_INTERVAL * lability);
}
#endregion

Those last two lines convert a class-wide constant into type-specific values. By taking lability into account for the emotion and mode-decay intervals, we are making high-lability personalities get more excited more quickly but also cool off very rapidly, which is appropriate.

The CalculateRank() method is a simple bubble sort of a copy of the fuzzyStates array. It is true that bubble sort is not the most efficient sort we could implement, but given the size of the array (6) and what we really need to do here, it is the best algorithm to use—easy to implement and modify for our needs, and efficient enough for the current use. Here’s the code:

/// <summary>
/// Bubble sorts a copy of fuzzyStates to rank order the rankOrder array of indices
/// (largest first). rankOrder[0] is the index of the highest-value fuzzyStates
/// member.
/// </summary>
public void CalculateRank()
{
    float[] temp = new float[NUM_EMOTIONS];
    for (int i = 0; i < NUM_EMOTIONS; i++)
    {
        rankOrder[i] = i;
        temp[i] = fuzzyStates[i];
    }

    bool ranked = false;

    //basic bubble sort (which isn't the best efficiency but this is a small
    //search, so it doesn't much matter
    while (!ranked)
    {
        ranked = true;
        float t;
        int ti;
        for (int i = 0; i < NUM_EMOTIONS - 1; i++)
        {

            if (temp[i] < temp[i + 1])
            {
               t = temp[i];
               temp[i] = temp[i + 1];
               temp[i + 1] = t;
               ti = rankOrder[i];
               rankOrder[i] = rankOrder[i + 1];
               rankOrder[i + 1] = ti;
               ranked = false;
            }
        }
    }
}

Lastly, we need an Update() method to take decay into account for both the emotions and the mood. Also, we need to call CalculateRank() often to keep it maintained (we will use these rankings a lot later on). Update() needs to look like this:

/// <summary>
/// Maintains the decay of emotions, calculates the new rank order,
/// calculates the new value of mood, calculates mood decay toward the
/// disposition
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
    Boolean ud = false;
    for (int i = 0; i < NUM_EMOTIONS; i++)
    {
        lastUpdate[i] += gameTime.TotalGameTime.Milliseconds;
        if (lastUpdate[i] > emoTime)
        {
           if (fuzzyStates[i] > 0f)
           {
              fuzzyStates[i] -= decayRate * emoTime / 1000f;
           }
           ud = true;
           lastUpdate[i] = 0;
        }
        if (fuzzyStates[i] > MAX_INTENSITY)
        {
            fuzzyStates[i] = MAX_INTENSITY;
        }
        else if (fuzzyStates[i] < 0f)
        {
            fuzzyStates[i] = 0f;
        }
    }

    if (ud && (moodUpdate > (moodTime / 10)))
    {
         //normalize the emotion values for mood computations
         float norm = fuzzyStates[(int)Emotions.ANGER] +
             fuzzyStates[(int)Emotions.SADNESS] +
             fuzzyStates[(int)Emotions.FEAR] +
             fuzzyStates[(int)Emotions.DISGUST] +
             fuzzyStates[(int)Emotions.JOY] +
             fuzzyStates[(int)Emotions.SURPRISE];
         if (norm <= 0)
         {
             norm = 1;
         }
         if (mood > 10f)
         {
            //mood is positive, all negative axis emotions will
            //decrement, but positive and neutral will increment
            mood -= fuzzyStates[(int)Emotions.ANGER] / norm +
                fuzzyStates[(int)Emotions.SADNESS] / norm +
                fuzzyStates[(int)Emotions.FEAR] / norm +
                fuzzyStates[(int)Emotions.DISGUST] / norm;
            mood += fuzzyStates[(int)Emotions.JOY] / norm +
                fuzzyStates[(int)Emotions.SURPRISE] / norm;
          }
          else if (mood < -10f)
          {
              //mood is negative, all positive axis emotions will
              //increment, but negative and neutral will decrement
              mood -= fuzzyStates[(int)Emotions.ANGER] / norm +
                  fuzzyStates[(int)Emotions.SADNESS] / norm +
                  fuzzyStates[(int)Emotions.FEAR] / norm +
                  fuzzyStates[(int)Emotions.DISGUST] / norm +
                  fuzzyStates[(int)Emotions.SURPRISE] / norm;
              mood += fuzzyStates[(int)Emotions.JOY] / norm;
          }
          else
          {
              //mood is neutral, all negative axis emotions will
              //decrement, but positive and neutral will increment
              mood -= fuzzyStates[(int)Emotions.ANGER] / norm +
                  fuzzyStates[(int)Emotions.SADNESS] / norm +
                  fuzzyStates[(int)Emotions.FEAR] / norm +
                  fuzzyStates[(int)Emotions.DISGUST] / norm;
              mood += fuzzyStates[(int)Emotions.JOY] / norm +
                  fuzzyStates[(int)Emotions.SURPRISE] / norm;
          }
      }
      CalculateRank();
      moodUpdate += gameTime.TotalGameTime.Milliseconds;
      if (!ud && (moodUpdate > moodTime))
      {
         if (mood < disposition)
         {
             mood += decayRate * moodTime/1000f;
             if (mood > disposition)
             {
                 mood = disposition;
             }
         }
         else if (mood > disposition)
         {
             mood -= decayRate * moodTime / 1000f;
             if (mood > disposition)
             {
                 mood = disposition;
             }
          }
          moodUpdate = 0f;
      }
      else if ( ud )
      {
          moodUpdate = 0f;
      }
      if (mood < -100f)
      {
          mood = -100f;
      }
      else if (mood > 100f)
      {
          mood = 100f;
      }
    }
    #endregion
  }
}

There you have it! A whiz-bang, custom emotion model. Well, at least part of one ….

Given our architecture, part of the maintenance for the emotion model will happen in the PersonalityType class where it will live. Of course that means we should add an EmotionModel object to PersonalityType. We will initialize the objects based on the type member like this:

switch ( type )
{
    case PersonalityTypes.AMD:
        s = 1f;
        h = -10f;
        a = 10f;
        v = 10f;
        f = 10f;
        i = 8f;
        em = new EmotionModel(0f, 30f, 15f, 0f, 25f, 0f, 0.5f, -10f, 5f);
        break;
    case PersonalityTypes.TU:
        s = 7f;
        h = -7f;
        a = -2f;
        v = 7f;
        f = -10f;
        i = -10f;
        em = new EmotionModel(0f, 50f, 75f, 45f, 30f, 0f, 0.15f, -15f, 2.5f);
        break;
    case PersonalityTypes.LW:
        s = -10f;
        h = 1f;
        a = 2f;
        v = -10f;
        f = 1f;
        i = 5f;
        em = new EmotionModel(0f, 0f, 45f, 50f, 35f, 15f, 0.1f, -25f, 0.25f);
        break;
    case PersonalityTypes.SE:
        s = 2f;
        h = -10f;
        a = 7f;
        v = 10f;
        f = 9f;
        i = 10f;
        em = new EmotionModel(60f, 50f, 0f, 0f, 45f, 10f, 0.95f, 10f, 75f);
        break;
    case PersonalityTypes.EN:
        s = -2f;
        h = 10f;
        a = -10f;
        v = -10f;
        f = 1f;
        i = -10f;
        em = new EmotionModel(50f, 0f, 25f, 0f, 12f, 15f, 0.5f, 25f, 25f);
        break;
    case PersonalityTypes.PH:
        s = 10f;
        h = 10f;
        a = -8f;
        v = 4f;
        f = -10f;
        i = 10f;
        em = new EmotionModel(35f, 0f, 15f, 75f, 25f, 52f, 0.65f, 35f, 30f);
        break;
    default:
        s = 0f;
        h = 0f;
        a = 0f;
        v = 0f;
        f = 0f;
        i = 0f;
        em = new EmotionModel(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f);
        break;
}

You may find yourself wondering where I got those initial values. The answer is I looked back at our design from a few pages ago and kind of made up what seemed to make sense. The only critically important values are the last three—lability, disposition, and decay rate—and I just made those match the descriptions from above. The brutal truth is, the values only need to match the behaviors you say are valid for the personality type; at the end of the day, the validity of the values you use is up to you, as long as you are consistent.

We also need to add a few properties to the personality type class—mostly pass-through accessors for the EmotionModel properties. Here they are:

public float Joy
{
    get { return em.Joy; }
    set { em.Joy = GetJoy(value); }
}
public float Anger
{
    get { return em.Anger; }
    set { em.Anger = GetAnger(value); }
}
public float Fear
{
    get { return em.Fear; }
    set { em.Fear = GetFear(value); }
}
public float Sadness
{
    get { return em.Sadness; }
    set { em.Sadness = GetSadness(value); }
}
public float Disgust
{
    get { return em.Disgust; }
    set { em.Disgust = GetDisgust(value); }
}
public float Suprise
{
    get { return em.Surprise; }
    set { em.Surprise = GetSurprise(value); }
}

Yes, I added all these new GetEmotion(value) kind of methods out of the ether. Basically, we want our personality type to influence how emotions are updated, and the easiest way to do this is by adding these in-between methods. For example, in the Sumacidal Explorer type, anything that generates fear, disgust, or surprise should also generate a high amount of anger. At the same time, almost no amount of sadness should have any impact.

Hold on there, Mr. Authorman! Are you making all this up? Exactly! I am making all this up based on the design. How can I get away with that? Simple, it’s my book! Er … but you probably want a real answer. We are not using a human-realistic level of emotional fidelity because we intentionally kept the modeling and animation fidelity at low enough levels that we can get away with this (and plus, human-realistic levels are probably inappropriate for green blobs that don’t even have arms or noses).

Remember that this is fiction and we are the authors of this fiction. All we have to do is be consistent. We could create close-to-human levels of fidelity if the game design required it (I have yet to see a case where such a requirement is not dictated by photo-realistic model textures), and only if we implemented a very realistic synthetic personality based off of the Five Factor Model following the empirical research on that model. I have done this in two test beds but found that I did not have the skills or resources to create an adequately media-rich simulation that the details produced by this level of personality modeling could even be seen by the user.

There are games that could use high-fidelity synthetic personalities. Massively multiplayer online role-playing games spring immediately to mind, but also any narrative-based game where the intent of the characters is to create emotional impact for the player. In these cases, the implementation doesn’t get much more complicated, but the design of the system becomes exponentially more complex. Designing human-like characters is hard. That’s why truly good fiction writers and actors are so hard to find.

Back to the code…. These six methods will get slightly mushy. Each one may have to perform tasks based on the type member in PersonalityType, and those tasks may feel completely wrong to you. That’s fine; in my opinion, to get the most out of this book, you should create your own personality types and set up your own emotion model based on the examples I’m providing here. I’d love to see them!

Before we can dig into these six methods, however, we will also need a bit of extra data in the PersonalityType class to make our adjustments match the design. We need to know what state the emotion is being generated from, so we need to add a String member and property to the class named behavior and Behavior. This member will need to be updated whenever we are executing a new action. If you recall, the Character class already knows this information, so once we’ve added the members to the PersonalityType class, we can make sure it is always updated with a single line of code. In Character, do this:

public String LastStateId
{
    get { return lastState; }
    set
    {
        lastState = value;
        pt.Behavior = value;
    }
}

Three cheers for ultra-easy-new code stuffs!

Now we are ready to move on. Let’s take a look at Joy first (mainly because it is first on the list, but also, and somewhat strangely, because it is the most complicated of the lot). None of our types have a complete restriction against Joy, but there is some variety. Both the EN and PH types will take every little bit of Joy they can get, so for those two types, we will adjust the values of the change like this:

/// <summary>
/// Performs type-specific value adjustments to the value passed in
/// </summary>
/// <param name="delta">Value by which to change the Joy motion state</param>
/// <returns>Adjusted value by which to change the Joy motion state</returns>
public float GetJoy(float delta)
{
    switch ( type )
    {
        case PersonalityTypes.PH:
            if ( behavior.Equals("DANCE_WITH") || behavior.Equals("CHAT") ||
                 behavior.Equals("CHASE") || behavior.Equals("GOTO") )
            {
                 //this type loves positive social interaction
                 delta += h + s;
            }
            else if (behavior.Equals("FIGHT"))
            {
                //adjust Anger according to Vindictiveness, absence of
                //Forgetfulness, and presence of Sociability
                em.Anger = delta + v + (-1 * f) - s;
                //adjust Fear by absence of Forgetfulness (doesn't remember the
                //pain or loss, etc.)
                em.Fear = delta + (-1 * f);
                //adjust Surprise by the level of Hedonism and Sociability, minus
                //the level of Vindictiveness and the absence of Forgetfulness
                em.Surprise = delta + h + s - v + (-1 * f);
                //that's it, but fighting doesn't make us happy!
                delta += h + s;
                delta *= -1;
                break;
            }
            else if ( behavior.Equals("DANCE") || behavior.Equals("SLEEP") )
            {
                //these are marginally enjoyable to the PH type, but not as much
                //as partner states
                delta + h;
            }
            else if (behavior.Equals("EVADE"))
            {
                delta -= delta + h + s;
            }
            //none of the rest merit a change
       case PersonalityTypes.EN:
            if ( behavior.Equals("DANCE_WITH") || behavior.Equals("CHAT"))
            {
                //these are minimally enjoyable to the EN type, but not much
                delta + h/5f;

            }
            else if (behavior.Equals("FIGHT") || behavior.Equals("EVADE"))
            {
                //adjust Anger according to Vindictiveness, absence of
                //Forgetfulness, and presence of Sociability
                em.Anger = delta + v + (-1 * f) - s;
                //adjust Fear by absence of Forgetfulness (doesn't remember the
                //pain or loss, etc.)
                em.Fear = delta + (-1 * f);
                //adjust Surprise by the level of Hedonism and Sociability, minus
                //the level of Vindictiveness and the absence of Forgetfulness
                em.Surprise = delta + h + s - v + (-1 * f);
                //that's it, but fighting doesn't make us happy!
                delta += h + s;
                delta *= -1;
                break;
            }
            else if ( behavior.Equals("DANCE") || behavior.Equals("SLEEP") )
            {
                //this type loves doing its own thing
                delta += h + s;
            }
            else if (behavior.Equals("CHASE") || behavior.Equals("GOTO"))
            {
                //this guy really wants to be left the heck alone
                em.Disgust = delta + (-1 * s);
                delta -= delta + h + s;
            }
            //none of the rest merit a change
            break;

The LW type won’t really take pleasure from anything except being alone, but he absolutely hates being called on socially. This type’s chunk of Joy processing should look like this:

case PersonalityTypes.LW:
    if ( behavior.Equals("EVADE") )
    {
        //what? someone is invading his space??
        em.Anger = delta + (-1 * s) + f/v;
        em.Disgust = delta + (-1 * s);
        //but he's marginally happy about getting away from others
        delta /= (-1 * s)/5f;
    }
    else if (behavior.Equals("CHASE") || behavior.Equals("GOTO"))
    {
        //this guy really wants to be left the heck alone
        em.Disgust = delta + (-1 * s);
        //he's a little afraid of others as well
        em.Fear = delta + (-1 * s)/5f;
        //and not happy…
        delta -= delta + h + s;
    }
    else if ( behavior.Equals("CHAT") || behavior.Equals("FIGHT") ||
        behavior.Equals("DANCE_WITH"))
    {
        //he really hates social activities
        em.Anger = delta + (-1 * s) + f + (v/2f);
        em.Disgust = delta + (-1 * s);
        em.Fear = delta + (-1 * s) + i;
        delta += (-1 * s) + (-1 * h) + f;
        delta *= -1;
    }
    else
    {
        //everything else is "just okay"
        delta += h + a;
    }
    break;

You can see how this is going. I’m just playing with the personality type’s emotions as seems to be called for based on the activity and our design. The other three types are handled like this:

case PersonalityTypes.TU:
    if (behavior.Equals("DANCE_WITH") || behavior.Equals("CHAT") ||
        behavior.Equals("CHASE") || behavior.Equals("GOTO"))
    {
        //this type loves being led…
        delta += a + s + (-1 * i);
    }
    else if (behavior.Equals("FIGHT"))
    {
        //he hates controntation
        em.Anger = delta + v + (-1 * f) - s;
        //adjust Fear by absence of Forgetfulness (doesn't remember the
        //pain or loss, etc.)
        em.Fear = delta + (-1 * f);
        //adjust Disgust by the level of Hedonism and Sociability, minus
        //the level of Vindictiveness and the absence of Forgetfulness
        em.Disgust = delta +  h + s - v + (-1 * f);
        //that's it, but fighting doesn't make us happy!
        delta += h + s;
        delta *= -1;
        break;
    }
    else if (behavior.Equals("EVADE") || behavior.Equals("STOP") ||
        behavior.Equals("SLEEP"))
    {
        //adjust Disgust by the level of Hedonism and Sociability, minus
        //the level of Vindictiveness and the absence of Forgetfulness
        em.Disgust = delta + h + s - v + (-1 * f);
        //this will also make the TU a bit sad
        em.Sadness = delta;
        //this will not make a TU happy…
        delta += -delta + i + a;
    }
    //nothing else merits a change
    break;
case PersonalityTypes.AMD:
    if (behavior.Equals("DANCE_WITH") || behavior.Equals("CHAT") ||
        behavior.Equals("FIGHT") || behavior.Equals("EVADE"))
      {
           //this type loves being in charge
           delta += a + s + (-1 * i);
      }
      else if (behavior.Equals("CHASE") || behavior.Equals("GOTO") ||
            behavior.Equals("STOP") || behavior.Equals("SLEEP"))
      {
             //but HATES being ignored
             em.Anger = delta + a + v + (-1 * f) + s;
             //adjust Disgust by the level of Hedonism and Sociability, minus
             //the level of Vindictiveness and the absence of Forgetfulness
             em.Disgust = delta + h + s - v + (-1 * f);
             //this will also make the AMD a bit sad
             em.Sadness = delta;
             //this will not make an AMD happy…
             delta += -delta + i + a;
          }
          //nothing else merits a change
          break;
       case PersonalityTypes.SE:
          if (behavior.Equals("MOVE") || behavior.Equals("FREE_ROAM"))
          {
              //hey, he's an explorer
              delta += i + f + s - h;
          }
          //nothing else merits a change
          break;
    }

    return delta;
}

The rest of the methods are just more of the same, so I’ll leave you to review them on your own.

The next step is to hook up the Update() method in Character, like this:

public void Update(GameTime gameTime)
{
    pt.Update(gameTime);

We want it to run every update cycle, as it has its own time-based guardians for execution.

Next, we can move on to adding emotional input from the State.Action() methods themselves. Some states, like the stop and forget states, don’t really have an emotional impact, so they don’t need any changes. DanceState is so simple that it only requires a single line of code:

//guarded update of the ai attribute for increasing boredom
if (self.LastStateId != Identifier)
{
    self.LastStateId = Identifier;
}

if (self.WasSleeping)
{
    return;
}

//who doesn't like dancing??
self.PersonalityType.Joy = Personality.PersonalityType.LargeInput;

Other states, like MoveState and RandomWalkState, are slightly more complicated in that we need to do different things, depending on whether or not the character likes the activity:

//guarded update of the ai attribute for increasing boredom
if (self.LastStateId != Identifier)
{
    self.LastStateId = Identifier;
}

if (self.GetActivityAffinity(fsmRef.GetStateIdKey(Identifier)) > -10f)
{
    //this might be a pleasent stroll
    self.PersonalityType.Joy = Personality.PersonalityType.MedInput;
}
else
{
    //this is probably annoying
    self.PersonalityType.Anger = Personality.PersonalityType.SmallInput;
}

GoToState, ChaseState, and EvadeState add slightly more complexity in that we also check to see if we like the target character:

if (target != null)
{
    //do we like the person we are chasing?
    if (self.GetAttraction(Game1.GameStateMan.CharManager.GetToonId(target))
        > -10f)
    {
        //this is a good thing
        self.PersonalityType.Joy = Personality.PersonalityType.MedInput;
        //there is some budding anticipation
        self.PersonalityType.Surprise = Personality.PersonalityType.SmallInput;
        //however, I like you and you are making me chase you
        self.PersonalityType.Anger = Personality.PersonalityType.MinInput;
    }
    else
    {
        //oops, we're not happy
        self.PersonalityType.Joy = -Personality.PersonalityType.LargeInput;
        //but at least we're disgusted
        self.PersonalityType.Disgust = Personality.PersonalityType.MaxInput *
            self.GetAttraction(Game1.GameStateMan.CharManager.
            GetToonId(target)) / 100f;
        //and, in fact, we're downright mad at you
        self.PersonalityType.Anger = Personality.PersonalityType.MaxInput *
            self.GetAttraction(Game1.GameStateMan.CharManager.
            GetToonId(target))/100f;
    }

    if (self.GetActivityAffinity(fsmRef.GetStateIdKey(Identifier)) > -10f)
    {
        //this might be a pleasent chase
        self.PersonalityType.Joy = Personality.PersonalityType.MedInput;
    }
    else
    {
        //this is probably annoying
        self.PersonalityType.Anger = Personality.PersonalityType.MedInput;
    }

The StopState code is also a little complicated, but only because I want the characters to be annoyed and grumpy when they wake up. In this state, the Action() method gets a single line of code:

//guarded update of the ai attribute for increasing boredom
if (self.LastStateId != Identifier)
{
    self.LastStateId = Identifier;
}

//sleeping is good
self.PersonalityType.Joy = Personality.PersonalityType.MedInput;

So do both Transition() methods:

public override string Transition(int inputKey, bool set)
{
    //waking up is hard…
    self.PersonalityType.Anger = Personality.PersonalityType.MinInput;

As usual, the difficult cases are the binary interaction states. These three states (ChatState, FightState, and DanceWithState) all get changes in both Transition() methods:

public override string Transition(int inputKey)
{
    if (target != null)
    {
        if (target.GetAttraction(Game1.GameStateMan.CharManager.
            GetToonId(self)) > -10f)
        {
            if (target.GetActivityAffinity(fsmRef.
               GetStateIdKey(Identifier)) > -10f)
            {
               //I liked this!
               target.PersonalityType.Sadness = Personality.
                   PersonalityType.SmallInput;
               target.PersonalityType.Joy = -Personality.PersonalityType.
                   SmallInput;
            }
            else
            {
                 //what a relief
                 target.PersonalityType.Joy = Personality.PersonalityType.
                     SmallInput;
            }
        }
        else
        {
           //what a relief
           target.PersonalityType.Joy = Personality.PersonalityType.
               SmallInput;
        }
   }

There will also be fairly significant changes to the Action() method:

else if (((target != null) && target.Name.Equals(self.Name)) || (target == null))
{
    if (self.GetActivityAffinity(fsmRef.GetStateIdKey(Identifier)) > -10f)
    {
        //well that was a little rude…
        self.PersonalityType.Surprise = Personality.PersonalityType.MedInput;
        self.PersonalityType.Joy = -Personality.PersonalityType.SmallInput;
    }
    else
    {
        //oh what a relief it is
        self.PersonalityType.Joy = Personality.PersonalityType.SmallInput;
    }

    fsmRef.Token = "STOP";
    fsmRef.ExecuteTransition(false);
    fsmRef.CurrentState.AIControl = AIControl;
    return;
}

//guarded update of the ai attribute for increasing boredom
if (self.LastStateId != Identifier)
{
    self.LastStateId = Identifier;
}

if (target.FSM.CurrentState.Identifier != "FIGHT" &&
    Initiator.Equals(self.Name) && target.FSM.CurrentState.AIControl)
{
    if ((target.AvailablePartner) || (!AIControl))
    {
        target.Partner = Game1.GameStateMan.CharManager.GetToonId(self);
    }
    else if ((target.Partner != Game1.GameStateMan.CharManager.
        GetToonId(self)) && AIControl)
      {
          if (self.GetActivityAffinity(fsmRef.GetStateIdKey(Identifier)) > -10f)
          {
              //I've been snubbed!
              self.PersonalityType.Sadness=Personality.PersonalityType.MedInput;
              self.PersonalityType.Joy = -Personality.PersonalityType.SmallInput;
          }
          else
          {
              //oh what a relief it is
              self.PersonalityType.Joy = Personality.PersonalityType.SmallInput;
          }


          self.Partner = -1;
          target = null;
          return;
      }
      if (target.FSM.CurrentState is Logic.CharacterFSMStates.SleepState)
      {
          target.FSM.Token = "SLEEP";
          target.FSM.ExecuteTransition(false);
          target.FSM.Update(gameTime);
      }
      target.FSM.Token = "FIGHT";
      target.FSM.ExecuteTransition(false);
      target.FSM.CurrentState.AIControl = AIControl;
      target.FSM.CurrentState.Target = self;
      (target.FSM.CurrentState as CharacterFSMStates.FightState).
           InitCharacter = self;
      target.FSM.Update(gameTime);

  }
  else if ((init != null) && (!init.Name.Equals(self.Name) &&
      !(init.FSM.CurrentState is FightState)))
  {
       Boolean a = fsmRef.CurrentState.LastAIControl;
       Character i = fsmRef.CurrentState.LastInit;
       Character t = fsmRef.CurrentState.LastTarget;

       fsmRef.Token = LastState;
       InitCharacter = null;
       Target = null;
       fsmRef.ExecuteTransition(false);
       fsmRef.CurrentState.AIControl = a;
       if (fsmRef.CurrentState is FightState)
       {
           (fsmRef.CurrentState as FightState).InitCharacter = i;
       }
       else if (fsmRef.CurrentState is ChatState)
       {
           (fsmRef.CurrentState as ChatState).InitCharacter = i;
       }
       else if (fsmRef.CurrentState is DanceWithState)
       {
           (fsmRef.CurrentState as DanceWithState).InitCharacter = i;
       }
       if ((fsmRef.CurrentState is EvadeState) || (fsmRef.CurrentState is
           ChaseState) || (fsmRef.CurrentState is GoToState))
       {
           if ((t != null) && (t.AvailableTarget))
           {
               fsmRef.CurrentState.Target = t;
               t.SetTarget(Game1.GameStateMan.CharManager.GetToonId(target));
           }
           else
           {
               fsmRef.Token = "STOP";
               fsmRef.ExecuteTransition(false);
           }
        }
        fsmRef.CurrentState.LastState = Identifier;
        fsmRef.CurrentState.LastTarget = self;
        fsmRef.CurrentState.LastInit = init;
        fsmRef.CurrentState.LastAIControl = AIControl;
        return;
    }

    if (self.WasSleeping)
    {
         return;
    }

   //do we like the person we are beating up?
  if (self.GetAttraction(Game1.GameStateMan.CharManager.GetToonId(target)) > -10f)
  {
      if (self.GetActivityAffinity(fsmRef.GetStateIdKey(Identifier)) > -10f)
      {
          //the first rule about Fight Club…
          self.PersonalityType.Joy = Personality.PersonalityType.MedInput;
          //which is kind of scary after the whole blowing-up-buildings thing
          self.PersonalityType.Fear = Personality.PersonalityType.SmallInput;
      }
      else
      {
          //you are making me mad
          self.PersonalityType.Anger = Personality.PersonalityType.MaxInput;
          //and a little disgusted
          self.PersonalityType.Disgust = Personality.PersonalityType.MedInput;
          //not to mention a little sad
          self.PersonalityType.Sadness = Personality.PersonalityType.
              SmallInput;
     }
}
else
{
    if (self.GetActivityAffinity(fsmRef.GetStateIdKey(Identifier)) > -10f)
    {
        //I never liked you!
        self.PersonalityType.Joy = Personality.PersonalityType.MaxInput;
        //but this is a little scary
        self.PersonalityType.Fear = Personality.PersonalityType.MedInput;
        //and you still tick me off
        self.PersonalityType.Anger = Personality.PersonalityType.MedInput;
    }
    else
    {
        //uh, I don't want to do this
        self.PersonalityType.Fear = Personality.PersonalityType.LargeInput;
        //but I will if you don't back off
        self.PersonalityType.Anger = Personality.PersonalityType.LargeInput;
        //you disgusting creep
        self.PersonalityType.Disgust = Personality.PersonalityType.
            LargeInput;
    }
}

Next up, I want to add a bit of surprise and annoyance at bumping into someone (and I want to wake up sleepyheads). This is easily done in the collision-detection method we already have, CollisionHandler.DoCollision():

//oh, I didn't see you there!
worker.PersonalityType.Surprise = Character.Personality.PersonalityType.
    LargeInput;
//you idgit!
worker.PersonalityType.Anger = Character.Personality.PersonalityType.
    MinInput;

//pardon me, was I in your way?
hitter.PersonalityType.Surprise = Character.Personality.PersonalityType.
    LargeInput;
//you clumsy oaf!
hitter.PersonalityType.Anger = Character.Personality.PersonalityType.
    MinInput;

if (worker.IsSleeping && worker.FSM.CurrentState.AIControl)
{
    //Oh you dirty rat you woke me up!
    worker.PersonalityType.Anger = Character.Personality.PersonalityType.
        LargeInput;
    worker.FSM.Token = "STOP";
    worker.FSM.ExecuteTransition(false);
    worker.FSMUpdate = true;
}

if (hitter.IsSleeping && hitter.FSM.CurrentState.AIControl)
{
    //Oh you dirty rat you woke me up!
    hitter.PersonalityType.Anger = Character.Personality.PersonalityType.
        LargeInput;
    hitter.FSM.Token = "STOP";
    hitter.FSM.ExecuteTransition(false);
    hitter.FSMUpdate = true;
}

Finally, we will reward the character for being excited and annoy him if he is bored:

public void Update(GameTime gameTime)
{
    if (boredom > 10f)
{
    pt.Joy = Personality.PersonalityType.SmallInput;
}
else if (boredom < -10f)
{
    pt.Anger = Personality.PersonalityType.SmallInput;
}

At this point, we have made pretty good strides toward appropriate emotional fidelity for this game. We have behavior selection being affected by personality type (although not yet by emotion), emotions being affected by behavior, and mood being affected by emotions. That said, the game in its current state illustrates a very good point: If the player can’t see the personality and emotion, you might as well not put it in the game. We have this wonderful emotion model, but our expressions are dictated by which state we are in. We meticulously keep mood updated, but do nothing to communicate it to the player. This must change, Valued Reader, and in the next section, it will!

Intentionality: Communicating Mental State

Intentionality—the perceived link between a character’s actions and its mental state—is vital to successfully creating an engaging video-game character because intentionality is necessary for the consistent communication of believable emotional context and social cues to the player. It very rarely falls into any game design by sheer luck, however. On the contrary, intentionality must be designed and purposefully implemented.

What forms of communication are available in a game setting? Visual communication, auditory communication (sound effects, music), and linguistic communication (however, the capacity for natural language understanding and generation is obviously lacking), or any combination of the three. In our particular game, there is no sound, and the characters don’t talk, but we still employ linguistic communication in the GUI panels. But the primary means we have for communicating mental state in Suma Sumi is visual.

Traditionally, human-to-human communication is conducted through language, gesture, gaze, expression, and touch. Just like any animation, gestures must have the appropriate fidelity; in fact, gestures that do not contain qualitative aspects of movement do not communicate any aspects of mental state.

In humans, gesture can take any of five forms:

  • Iconics. These are representative of some feature of the subject, such as its shape or size.

  • Metaphorics. These represent some abstract feature of the subject, such as its use.

  • Deictics. These are used to show some point in space.

  • Beats. These are hand movements that accent spoken words and speaker turn-taking.

  • Emblems. These are stereotypical patterns with well-understood meanings, such as waving in greeting.

In video games, visual communication is comprised of quality of action, timing of action, appropriate animations (both behaviors and gestures), expression, and other visual cues appropriate to the game design. Since we are fairly limited by the medium (or more precisely by my skills as an artist), we have to do the best we can to make what is available communicate what we need it to.

All forms of communication can divided into two camps:

  • Lexical communication. This type of communication takes place in a system that relies on a lexicon of syntax-semantics pairs.

  • Creative communication. This type of communication uses a system that relies on a set of encoded rules (i.e., a grammar) to create new syntax for a given meaning or to decipher a non-ubiquitous meaning from a given syntax.

Lexical systems are great for simple pattern-response styles of communication; however, they lack adaptability, because they are limited to known atoms of language and syntax. Creative systems are far more adaptable, and may not be much more complicated that a lexical system. For example, creating two lexical systems—say, one for expression and one for behavior—and then combining them by some sort of grammatical rule set can constitute creative communication. For instance, if I meet you on the street and smile, wave, and enthusiastically say hello, you will likely take that to mean I am happy to see you. If instead, however, when I meet you on the street, I sneer, wave, and say buh-bye, you might take that to mean I don’t much like you. There are no dictionary definitions for these combinations, only social rules that govern their use.

Lexical intentionality is the expression of internal beliefs and desires by a synthetic character through either a lexicon of linguistic atoms or a lexicon of visual atoms. In the game so far, our lexical atoms are limited to a few animations and a few expressions that we paste on our characters according to the behavior they are presently executing. We could very easily extend this to pick the emotion with the highest value and set the expression based on that. All we would need to do this is to add a single accessor in both EmotionModel and PersonalityType.

Here’s what to add to EmotionModel:

public Emotions StrongestEmotion
{
    get { return (Emotions)rankOrder[0]; }
}

Here’s what to add to PersonalityType:

public EmotionModel.Emotions StrongestEmotion
{
    get { return em.StrongestEmotion; }
}

We already have this system of setting expression in all the State.Action() methods. Instead of undoing all that, let’s do this in Character:

public void SetExpression(int m)
{
    return; //uncomment / comment this line to switch off / on the State.
        Action expression setting routine
    switch (m)
    {

Bam! No more expressions set willy-nilly without regard for emotion. Now, in the Update() method of Character, we will do this:

public void Update(GameTime gameTime)
{
    if (!lastState.Equals("NONE"))
    {
        if (boredom > 10f)
        {
            pt.Joy = Personality.PersonalityType.SmallInput;
        }
        else if (boredom < -10f)
        {
           pt.Anger = Personality.PersonalityType.SmallInput;
        }
    }
    pt.Update(gameTime);
    switch (pt.StrongestEmotion)
    {
       case Personality.EmotionModel.Emotions.ANGER:
           mouth = mouthAngry;
           break;
       case Personality.EmotionModel.Emotions.DISGUST:
           mouth = mouthDisgusted;
           break;
       case Personality.EmotionModel.Emotions.FEAR:
           mouth = mouthBored; //yeah, yeah I know :)
           break;
       case Personality.EmotionModel.Emotions.JOY:
           mouth = mouthHappy;
           break;
       case Personality.EmotionModel.Emotions.SADNESS:
           mouth = mouthSad;
           break;
       case Personality.EmotionModel.Emotions.SURPRISE:
           mouth = mouthConfused; //yeah, yeah I know, part II :)
           break;
    }
    mouth.Position = pos;

Figure 6.7 shows the result of these changes, which are pretty cool and definitely a step in the right direction. We can see that lots of the Suma are more happy than not. We can see that Tyeu and Dibu are more disgusted than not, and the Fhet is more angry than anything else. What we don’t see here is how intensely any of these characters are feeling these particular emotions. It may well be the case that Fhet is only 1 percent angry but is also 0.99 percent happy. We also don’t see any of the moods or the level of boredom except in the handy debugging panel.

Emo Suma in the wild.

Figure 6.7. Emo Suma in the wild.

Figure 6.7 is a pretty good example of a lexical system—and about the extent of the capabilities of lexical intentionality. The system sees a pattern (e.g., the top emotion) and presents an atomic response (e.g., the right mouth texture). There has to be a way we can incorporate mood and boredom into the mix.

To address boredom or excitement, we can do something with the eyes (like we did with StopState). I don’t want to change the current eye animations, however; I just want to augment them. In EyeSprite.cs, we need a few extra members, a property, and a reset method. The members will represent the scale for the pupil (i.e., the dilation of the eye). Here are the changes:

private float pscaler; //internal use
private float pscale; //default pupil scale

public float PupilDilation
{
   get { return pscaler; }
   set { pscaler = value; }
}
public void ResetPupilDilation()
{
   pscaler = pscale;
}

To use these changes, we just need to make some basic changes to the Update() method of Character, like this:

public void Update(GameTime gameTime)
{
    if (!lastState.Equals("NONE"))
    {
       if (boredom > 10f)
       {
           pt.Joy = Personality.PersonalityType.SmallInput;
           //I'm excited!
           float dil = 1f + boredom / 100f;
           if (dil > 1.2f)
           {
                  dil = 1.2f;
           }
           leye.PupilDilation = dil;
           reye.PupilDilation = dil;
      }
      else if (boredom < -10f)
      {
           pt.Anger = Personality.PersonalityType.SmallInput;
           //I'm bored!
           float dil = boredom / -100f;
           if (dil < 0.8f)
           {
               dil = 0.8f;
           }
           leye.PupilDilation = dil;
           reye.PupilDilation = dil;
     }
     else
     {
           //I'm normal!! Oh, yes I am!
           leye.ResetPupilDilation();
           reye.ResetPupilDilation();
     }
}

Figure 6.8 shows the results. Take a look at Biix—he’s a little bored—as compared to Laka—he’s very excited. Now, this isn’t a perfect solution, since sometimes a Suma will be bored yet still have the big happy face plastered across its head, but it’s a good start.

Pupils gone wild!

Figure 6.8. Pupils gone wild!

This is an example of creative intentionality. We are taking two lexical systems and combining them to create a novel message. On top of that, it didn’t take much effort. The only downside is that it is pretty limited in effectiveness.

I’m going to take a second here and create three new expressions. I’ve decided to make the bored mouth better (see Figure 6.9). It will need an opposite, or an excited mouth (see Figure 6.10). Finally, I’m adding a fearful expression (see Figure 6.11). We already know how to add these images to the Character class, so I’ll skip the discussion (see Chapter 5 if you’ve forgotten).

The new and improved bored mouth.

Figure 6.9. The new and improved bored mouth.

Awesome, award-winning, excited mouth.

Figure 6.10. Awesome, award-winning, excited mouth.

The vewy-scawy fear mouth.

Figure 6.11. The vewy-scawy fear mouth.

Next, what I’d like to do is to somehow blend between the bored or excited expression and the strongest emotion expression. The very first change we need to make for this to work is in the CharacterSprite class, and it’s trivially minor (if that isn’t saying the same thing over and over redundantly). We just need to add a setter to the Texture property.

The next step is a bit more complicated. Basically, I need to take two expression images (which, if you recall, are PNG files that are all transparent except for the black where the mouth is), and compare the columns of black pixels in the first image to the same column in the second picture, making some kind of blending decision based on a percent value. For example, if the character is 30-percent bored (i.e., boredom = −30), and the character is 50-percent angry (i.e., anger = 35), the percent is calculated as 62.5-percent (50 out of 80) angry and 37.5-percent (30 out of 80) bored.

The implementation of the blending algorithm is really outside the scope of this book, so I’ve commented it up pretty well and will leave its review as an exercise. Beware, though, that doing the image processing for the blend on the CPU is very expensive computationally (e.g., it’s mad inefficient); for this reason, I’ve threaded the process. (That sound like more goblet-guck; just smile, nod, and forget all about this part.) For now, rest assured that there is a new GameState.Utilities.ImageEffects object in the Character class that is named fx. We will call its one and only method, Blend(), from the Update() method in Character to create nifty little custom expressions. Right now, we are interested only in blending the new bored or excited expressions with the strongest emotion. Here’s the code:

float percent = .55f * (Math.Abs(boredom)/100f);

switch (pt.StrongestEmotion)
{
    case Personality.EmotionModel.Emotions.ANGER:
        if (boredom > 10f)
        {
            Color[] dataBlend = new Color[mouthExcited.Texture.Width *
                mouthExcited.Texture.Height];
            mouthExcited.Texture.GetData<Color>(dataBlend);

            Color[] dataOrig = new Color[mouthAngry.Texture.Width *
                mouthAngry.Texture.Height];
            mouthAngry.Texture.GetData<Color>(dataOrig);

            Texture2D mouthTex = mouth.Texture;
            mouth.Texture = GameState.Utilities.ImageEffects.
                ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
       }
       else if (boredom < -10f)
       {
            Color[] dataBlend = new Color[mouthBored.Texture.Width *
                mouthBored.Texture.Height];
            mouthBored.Texture.GetData<Color>(dataBlend);

            Color[] dataOrig = new Color[mouthAngry.Texture.Width *
                mouthAngry.Texture.Height];
            mouthAngry.Texture.GetData<Color>(dataOrig);

            Texture2D mouthTex = mouth.Texture;
            mouth.Texture = GameState.Utilities.ImageEffects.
                ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
      }
      else
      {
         mouth = mouthAngry;
      }
      break;
  case Personality.EmotionModel.Emotions.DISGUST:
      if (boredom > 10f)
      {
          Color[] dataBlend = new Color[mouthExcited.Texture.Width *
              mouthExcited.Texture.Height];
          mouthExcited.Texture.GetData<Color>(dataBlend);

          Color[] dataOrig = new Color[mouthDisgusted.Texture.Width *
              mouthDisgusted.Texture.Height];
          mouthDisgusted.Texture.GetData<Color>(dataOrig);

          Texture2D mouthTex = mouth.Texture;
          mouth.Texture = GameState.Utilities.ImageEffects.
              ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else if (boredom < -10f)
     {
         Color[] dataBlend = new Color[mouthBored.Texture.Width *
             mouthBored.Texture.Height];
         mouthBored.Texture.GetData<Color>(dataBlend);
         Color[] dataOrig = new Color[mouthDisgusted.Texture.Width *
             mouthDisgusted.Texture.Height];
         mouthDisgusted.Texture.GetData<Color>(dataOrig);

         Texture2D mouthTex = mouth.Texture;
         mouth.Texture = GameState.Utilities.ImageEffects.
             ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
      }
      else
      {
         mouth = mouthDisgusted;
      }
      break;
  case Personality.EmotionModel.Emotions.FEAR:
      if (boredom > 10f)
      {
          Color[] dataBlend = new Color[mouthExcited.Texture.Width *
              mouthExcited.Texture.Height];
          mouthExcited.Texture.GetData<Color>(dataBlend);

          Color[] dataOrig = new Color[mouthFear.Texture.Width *
              mouthFear.Texture.Height];
          mouthFear.Texture.GetData<Color>(dataOrig);

          Texture2D mouthTex = mouth.Texture;
          mouth.Texture = GameState.Utilities.ImageEffects.
              ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else if (boredom < -10f)
     {
        Color[] dataBlend = new Color[mouthBored.Texture.Width *
            mouthBored.Texture.Height];
        mouthBored.Texture.GetData<Color>(dataBlend);

        Color[] dataOrig = new Color[mouthFear.Texture.Width *
            mouthFear.Texture.Height];
        mouthFear.Texture.GetData<Color>(dataOrig);

        Texture2D mouthTex = mouth.Texture;
        mouth.Texture = GameState.Utilities.ImageEffects.
            ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else
     {
         mouth = mouthFear;
     }
     break;
 case Personality.EmotionModel.Emotions.JOY:
      if (boredom > 10f)
      {
          Color[] dataBlend = new Color[mouthExcited.Texture.Width *
              mouthExcited.Texture.Height];
          mouthExcited.Texture.GetData<Color>(dataBlend);

          Color[] dataOrig = new Color[mouthHappy.Texture.Width *
              mouthHappy.Texture.Height];
          mouthHappy.Texture.GetData<Color>(dataOrig);

          Texture2D mouthTex = mouth.Texture;
          mouth.Texture = GameState.Utilities.ImageEffects.
              ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else if (boredom < -10f)
     {
          Color[] dataBlend = new Color[mouthBored.Texture.Width *
              mouthBored.Texture.Height];
          mouthBored.Texture.GetData<Color>(dataBlend);

          Color[] dataOrig = new Color[mouthHappy.Texture.Width *
              mouthHappy.Texture.Height];
          mouthHappy.Texture.GetData<Color>(dataOrig);

          Texture2D mouthTex = mouth.Texture;
          mouth.Texture = GameState.Utilities.ImageEffects.
              ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
      }
      else
      {
         mouth = mouthHappy;
      }
      break;
  case Personality.EmotionModel.Emotions.SADNESS:
      if (boredom > 10f)
      {
          Color[] dataBlend = new Color[mouthExcited.Texture.Width *
              mouthExcited.Texture.Height];
          mouthExcited.Texture.GetData<Color>(dataBlend);

          Color[] dataOrig = new Color[mouthSad.Texture.Width *
              mouthSad.Texture.Height];
          mouthSad.Texture.GetData<Color>(dataOrig);

          Texture2D mouthTex = mouth.Texture;
          mouth.Texture = GameState.Utilities.ImageEffects.
              ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else if (boredom < -10f)
     {
        Color[] dataBlend = new Color[mouthBored.Texture.Width *
            mouthBored.Texture.Height];
        mouthBored.Texture.GetData<Color>(dataBlend);

        Color[] dataOrig = new Color[mouthSad.Texture.Width *
            mouthSad.Texture.Height];
        mouthSad.Texture.GetData<Color>(dataOrig);

        Texture2D mouthTex = mouth.Texture;
        mouth.Texture = GameState.Utilities.ImageEffects.
            ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else
     {
        mouth = mouthSad;
     }
     break;
 case Personality.EmotionModel.Emotions.SURPRISE:
     if (boredom > 10f)
     {
         Color[] dataBlend = new Color[mouthExcited.Texture.Width *
             mouthExcited.Texture.Height];
         mouthExcited.Texture.GetData<Color>(dataBlend);

         Color[] dataOrig = new Color[mouthSurprised.Texture.Width *
             mouthSurprised.Texture.Height];
         mouthSurprised.Texture.GetData<Color>(dataOrig);
         Texture2D mouthTex = mouth.Texture;
         mouth.Texture = GameState.Utilities.ImageEffects.
             ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
    }
    else if (boredom < -10f)
    {
         Color[] dataBlend = new Color[mouthBored.Texture.Width *
             mouthBored.Texture.Height];
         mouthBored.Texture.GetData<Color>(dataBlend);

         Color[] dataOrig = new Color[mouthSurprised.Texture.Width *
             mouthSurprised.Texture.Height];
         mouthSurprised.Texture.GetData<Color>(dataOrig);

         Texture2D mouthTex = mouthSurprised.Texture;
         mouth.Texture = GameState.Utilities.ImageEffects.
             ThreadedBlendMouth(percent, dataBlend, dataOrig, mouthTex);
     }
     else
     {
        mouth = mouthSurprised;
     }
     break;
}
mouth.Position = pos;

Most of that is routine business. It is, however, a shame that we’re going to comment all that out in a minute …. At any rate, what we get from this change is shown in Figure 6.12.

Lots of expressive expressions being expressed.

Figure 6.12. Lots of expressive expressions being expressed.

That is pretty interesting in terms of expressiveness, and we have broken into the realm of creative intentionality, but the accuracy of what is being communicated is quite low. If we were using a finite state model of emotion, we would be done here (because there would be only one valid emotion). Using the fuzzy state model, however, means we can be feeling more than one emotion at any given time, and we need a way to express the total emotional state (or at least the part we deem appropriate).

I’m also interested in making it easier for the player to recognize at a glance what personality type the character is. If this simulation was much higher fidelity, such a bold stroke as what I’m about to suggest might not be necessary, but as they say, it is what it is. Each specific type of personality will modify the bodyColor member of its Character class. We will accomplish that by doing this:

/// <summary>
/// modifies the color passed in based on the defined personality type
/// </summary>
/// <param name="start"></param>
/// <returns></returns>
public Color GetColorDelta(Color start)
{
    switch (type)
    {
       case PersonalityTypes.AMD:
            //yeah, standard Suma colored!
            break;
       case PersonalityTypes.EN:
            //ENs are a bit teal around the gils
            start.R = (byte)100;
            start.G = (byte)255;
            start.B = (byte)50;
            break;
       case PersonalityTypes.LW:
            //LWs are a bit creamy and boring
            start.R = (byte)225;
            start.B = (byte)125;
            break;
       case PersonalityTypes.PH:
            //Limon!
            start.G = (byte)255;
            start.B = (byte)125;
            break;
      case PersonalityTypes.SE:
           //make SEs bright poison yellow
           start.R = (byte)255;
           start.G = (byte)255;
           start.B = (byte)25;
           break;
      case PersonalityTypes.TU:
           //darker green than AMDs
           start.G = (byte)175;
           start.R = (byte)55;
           break;
   }
   return start;
}

Figure 6.13 shows the variety of colors we now have available. Potr is a Sumacidal explorer, Mati is a plural hedonist, Laka and Fhet are lone wolves, Biix is a timid underling, Dibu is an egocentric narcissist, and both Raas and Xret are absentminded despots. Yay!

Lots of pretty colors!

Figure 6.13. Lots of pretty colors!

But wait! There’s more! I’d like to be able to express the mood of the character. Given the constraints based on the relatively low modeling and animation fidelity, there’s not a whole lot I can do in terms of facial expressions. In addition, the plan is to blend expressions for up to three of the highest-intensity emotions, determined by the rank order of emotions, and having more than 25-percent intensity coupled with an expression to communicate boredom levels. There is a solution, Valued Reader, and it’s a good example of communicating some part of the mental state by any means necessary. The other benefit is that it is blisteringly simple.

What we will do is scale the size of the character based on his percent positive or negative mood; if his mood is neutral, we won’t apply any scale. I’ve added a couple of accessors to both PersonalityType and EmotionModel to access both the current value of the mood and the official names (BAD, GOOD, and MEH). We must also add a new float in Character, called moodBodyScaler. Once that is done, jump to the Update() method and do this:

public void Update(GameTime gameTime)
{
    if (!lastState.Equals("NONE"))
    {
        if (!isSleeping)
        {
            if (boredom > 10f)
            {
                pt.Joy = Personality.PersonalityType.SmallInput;
                //I'm excited!
                float dil = 1f + boredom / 100f;
                if (dil > 1.3f)
                {
                    dil = 1.3f;
                }
                leye.PupilDilation = dil;
                reye.PupilDilation = dil;
           }
           else if (boredom < -10f)
           {
              pt.Anger = Personality.PersonalityType.SmallInput;
              //I'm bored!
              float dil = boredom / -100f;
              if (dil < 0.7f)
              {
                  dil = 0.7f;
              }
              leye.PupilDilation = dil;
              reye.PupilDilation = dil;
          }
          else
          {
              //I'm normal!! Oh, yes I am!
              leye.ResetPupilDilation();
              reye.ResetPupilDilation();
          }
     }

     float work = pt.Mood;
     String m = pt.MoodName;
     //now express mood
     if (m.Equals("BAD"))
     {
         //turn the mood value into a percent
         work /= -100f;
         moodBodyScaler = 1.0f - (0.15f * work);
     }
     else if (m.Equals("GOOD"))
     {
         //turn the mood value into a percent
         work /= 100f;
         moodBodyScaler = 1.0f + (0.15f * work);
     }
     else
     {
         moodBodyScaler = 1.0f;
     }
}

Yay! Nothing happens! To use this little creature creatively, we need to cram it down the collective throats of the art assets contained in the Character class. In most cases, it’s a simple change that we can do during the Draw() method:

public void Draw(SpriteBatch sb)
{
    if (isWalking)
    {
        if (drawMask)
        {
            walkSheetMask.Draw(sb, dir, moodBodyScaler);
        }
        walkSheet.Draw(sb, dir, moodBodyScaler);
   }
   else if (isDancing)
   {
        if (drawMask)
       {
           danceSheetMask.Draw(sb, dir, moodBodyScaler);
       }
       danceSheet.Draw(sb, dir, moodBodyScaler);
   }
   else if (isFighting)
   {
        if (drawMask)
        {
            fightSheetMask.Draw(sb, dir, moodBodyScaler);
        }
        fightSheet.Draw(sb, dir, moodBodyScaler);
   }
   else
   {
      if (drawMask)
      {
          idleBodyFixedMask.Draw(sb,moodBodyScaler);
      }
      idleBodyFixed.Draw(sb, moodBodyScaler);
   }
   mouth.Draw(sb, moodBodyScaler);
   leye.Draw(sb);
   reye.Draw(sb);
   nameSprite.Draw(sb);
}

Yes, that does mean changes are incoming in the sprite classes we wrote! CharacterSprite needs this minor change:

public void Draw(SpriteBatch sb, float Sc)
{
   sb.Draw(spriteTexture, position, srcRectangle, tintColor, rotation,
       Vector2.Zero, Sc, SpriteEffects.None, depth);
}

CharacterSpriteSheet needs this change:

public void Draw(SpriteBatch sb, SpriteEffects se, float Sc)
{
    destRectangle.Height = (int)(spriteHeight * Sc);
    destRectangle.Width = (int)(spriteWidth * Sc);
    sb.Draw(sheet, destRectangle, srcRectangle, myColor, 0f, Vector2.Zero, se, 0f); }

Bam! That was sure easy. That’s okay, EyeSprite makes up for the ease of these two classes. You might have noticed that we didn’t change the way we called Draw() for the eyes. That’s because EyeSprite does a lot of mind-altering (or at least eye-altering, yuk yuk yuk) calculations in its Update() method. For the eyes to scale correctly, we need to do this:

public Vector2 Position
{
    get { return position; }
    set
    {
       position = value;
       pupilPos = value;
    }
}

and this:

public void Update(GameTime gameTime, float Sc)
{
    if (lookatPos == Vector2.Zero)
    {
        deltaX = 0;
        deltaY = 0;
        return;
    }

    float xLow = 0.05f;
    float xHi = 5f;
    float yLow = 0.05f;
    float yHi = 2.5f;
    if (Sc > 1.0f)
    {
        xHi = 3f;
        yHi = 1.25f;
    }
    else if (Sc < 1.0f)
    {
        xHi = 5f;
        yHi = 2.75f;
    }
    if (lookatPos.X < pupilPos.X && pupilPos.X >= position.X + xLow)
    {
        deltaX++;
        pupilPos.X -= deltaX;
    }
    else if (lookatPos.X > pupilPos.X && pupilPos.X <= position.X + xHi)
    {
        deltaX++;
        pupilPos.X += deltaX;
    }
    else
    {
       deltaX = 0;
    }

    if (lookatPos.Y < pupilPos.Y && pupilPos.Y >= position.Y + yLow)
    {
       deltaY++;
       pupilPos.Y -= deltaY;
    }
    else if (lookatPos.Y > pupilPos.Y && pupilPos.Y <= position.Y + yHi)
    {
       deltaY++;
       pupilPos.Y += deltaY;
    }
    else
    {
       deltaY = 0;
    }

    if (pupilPos.X < position.X + xLow * pscaler)
    {
        pupilPos.X = position.X + xLow * pscaler;
    }
    else if (pupilPos.X > position.X + xHi * pscaler)
    {
        pupilPos.X = position.X + xHi * pscaler;
    }

    if (pupilPos.Y < position.Y + yLow * pscaler)
    {
        pupilPos.Y = position.Y + yLow * pscaler;
    }
    else if (pupilPos.Y > position.Y + yHi * pscaler)
    {
        pupilPos.Y = position.Y + yHi * pscaler;
    }

    if (hscale < scale)
    {
        ballDestRectangle.Height = (int)(eyeTexture.Height * hscale * Sc * pscaler);
        ballDestRectangle.Y = (int)(position.Y + ballOffset.Y * Sc +
            ballOffset.Y * hscale / 6.5f * (Sc));
        pupDestRectangle.Height = (int)(pupilTexture.Height * hscale *
            pscaler * Sc * pscaler);
        pupDestRectangle.Y = (int)(pupilPos.Y + pupilOffset.Y * Sc +
            pupilOffset.Y * hscale / 6.5f * (Sc));
    }
    else
    {
        ballDestRectangle.Height = (int)(eyeTexture.Height * Sc * pscaler);
        ballDestRectangle.Y = (int)(position.Y + ballOffset.Y * Sc);
        pupDestRectangle.Height = (int)(pupilTexture.Height * Sc * pscaler);
        pupDestRectangle.Y = (int)(pupilPos.Y + pupilOffset.Y * Sc);
    }

    ballDestRectangle.X = (int)(position.X + ballOffset.X * Sc + .24f);
    ballDestRectangle.Width = (int)(eyeTexture.Width *Sc);

    pupDestRectangle.X = (int)(pupilPos.X + pupilOffset.X * Sc);
    pupDestRectangle.Width = (int)(pupilTexture.Width * pscaler * Sc);

    if (pupDestRectangle.X < ballDestRectangle.Left)
    {
        pupDestRectangle.X = ballDestRectangle.Left;
    }
    else if (pupDestRectangle.X > ballDestRectangle.
        Right - pupDestRectangle.Width)
    {
        pupDestRectangle.X = ballDestRectangle.
            Right - pupDestRectangle.Width;
    }
    if (pupDestRectangle.Y < ballDestRectangle.Top)
    {
        pupDestRectangle.Y = ballDestRectangle.Top;
    }
    else if (pupDestRectangle.Y > ballDestRectangle.Bottom - pupDestRectangle. Height)
    {
        pupDestRectangle.Y = ballDestRectangle.Bottom - pupDestRectangle.Height;
    }
}

Them’re some pretty big changes. True, but it’s worth it. Before I can show you the beautiful picture, we need to make a few small changes in Character. First, find every leye.Update() or reye.Update() call and amend it so that moodBodyScaler is passed in. Then, in the open- and close-eye routines, we need to do this:

/// <summary>
/// Cause the left eye to open (by scaling the height of the image to 1)
/// </summary>
/// <returns>True when the HScale property is 1</returns>
public Boolean OpenLeftEye()
{
    float guard = (boredom / 100f);
    if (guard >= 0)
    {
        guard = MathHelper.Lerp(0.88f, 1.0f, guard);
    }
    else
    {
        guard = MathHelper.Lerp(0.7f, 0.879f, (-1 * guard));
    }
    if (leye.HScale * moodBodyScaler < guard)
    {
        leye.HScale += 0.1f;
        return false;
    }

    leye.HScale = guard;
    return true;
}
/// <summary>
/// Cause the right eye to open (by scaling the height of the image to 1)
/// </summary>
/// <returns>True when the HScale property is 1</returns>
public Boolean OpenRightEye()
{
    float guard = (boredom / 100f);
    if (guard >= 0)
    {
        guard = MathHelper.Lerp(0.88f, 1.0f, guard);
    }
    else
    {
        guard = MathHelper.Lerp(0.7f, 0.879f, (-1 * guard));
    }
    if (reye.HScale * moodBodyScaler < guard)
    {
        reye.HScale += 0.1f;
        return false;
    }

    reye.HScale = guard;
    return true;
}

All we are doing here is opening the eyes, but not always all the way. We’re reserving that wide-eyed look for really being excited. Not so excited? Our characters are now so squinty-eyed that Clint Eastwood would be proud!

Figure 6.14 shows our characters running around being all moody and stuff. You can see that Potr is in a really good mood and that he is pretty excited about what he’s doing. Biix, on the other hand, is in a bad mood and is bored. Raas has

Moody little buggers!

Figure 6.14. Moody little buggers!

misplaced his eyeballs. (You know how taking a group photo during the holidays is really hard? You ain’t seen nothing yet ….)

The last big change to visual language is the blending of a boredom expression and the top few emotions the character is feeling with an intensity of over 25 percent. Now, fair warning: There is more image-processing-fu in this step, and again I’ll leave review of that code as an exercise (or you can ignore it!). I took pains to do this processing on the CPU instead of how it should be done (on the graphics card in a two-pass shader) simply because code is generally more accessible to your average AI-atron than shader-fu is. It leads to some general slowdowns, but it’s okay for an example (because of the threaded-fu in the blenderizer)—although it will be very susceptible to major slowdowns on machine with low available memory or slow processor-clock speed. Alas.

Remember that big chunk of code we just wrote in Character.Update()? We’re about to cut it out … and replace it with face-ripping blender code that looks like this:

//figure out if it's time to pick a new boredom expression and scale it
//appropriately
scaleTime += gameTime.TotalGameTime.Milliseconds;
if (scaleTime > SCALE_INTERVAL)
{
    scaleTime = 0f;
    if (boredom > 10f)
    {
        Texture2D worker = fx.TextureClone(mouthExcited.Texture);
        scaledBETex = fx.ScaleTexture((Math.Abs(boredom) / 100f), worker, 25);
        pt.BoredomExpression = fx.TextureClone(scaledBETex);
    }
    else if (boredom < -10f)
    {
       Texture2D worker = fx.TextureClone(mouthBored.Texture);
       scaledBETex = fx.ScaleTexture((Math.Abs(boredom) / 100f), worker, 25);
       pt.BoredomExpression = fx.TextureClone(scaledBETex);
    }
    else
    {
       pt.BoredomExpression = fx.TextureClone(mouthNeutral.Texture);
    }
}
if (isSleeping)
{
    mouth.Texture = fx.TextureClone(mouthNeutral.Texture);
}
else
{
    mouth.Texture = fx.TextureClone(pt.SumExpressionTexture);
}

No, I didn’t do some kind of magical shrinking-fu. I moved the bulk of the processing to the EmotionModel.Update() method, as it is way easier to do it all there than here. Here’s how that method now looks:

CalculateRank();
float d = MAX_INTENSITY;

if ((rankOrder[0] != lastBest&&(((moodUpdate> (moodTime/15f))) || ud)) || firstRun)
{
   if (firstRun)
   {
       firstRun = false;
   }
   lastBest = rankOrder[0];


   //start by getting new copies of all the expressions, and scaling them by
   //the intensity of the emotion
   if (fuzzyStates[(int)Emotions.ANGER] / d > 0.25f || rankOrder[0] ==
       (int)Emotions.ANGER)
   {
       anger = me.EfxProcessor.ScaleTexture(fuzzyStates[(int)Emotions.ANGER] /
           d, me.EfxProcessor.TextureClone(me.AngryTexture), 25);
   }
   if (fuzzyStates[(int)Emotions.JOY] /d>0.25f || rankOrder[0] ==
       (int)Emotions.JOY)
   {
       joy = me.EfxProcessor.ScaleTexture(fuzzyStates[(int)Emotions.JOY] /
           d, me.EfxProcessor.TextureClone(me.JoyTexture), 25);
   }
   if (fuzzyStates[(int)Emotions.SADNESS] / d > 0.25f || rankOrder[0] ==
       (int)Emotions.SADNESS)
   {
       sad = me.EfxProcessor.ScaleTexture(fuzzyStates[(int)Emotions.SADNESS] /
            d, me.EfxProcessor.TextureClone(me.SadTexture), 25);
   }
   if (fuzzyStates[(int)Emotions.FEAR] / d > 0.25f || rankOrder[0] ==
       (int)Emotions.FEAR)
   {
       fear = me.EfxProcessor.ScaleTexture(fuzzyStates[(int)Emotions.FEAR]
            / d, me.EfxProcessor.TextureClone(me.FearTexture), 25);
   }
   if (fuzzyStates[(int)Emotions.DISGUST] / d > 0.25f || rankOrder[0] ==
       (int)Emotions.DISGUST)
   {
       disgust = me.EfxProcessor.ScaleTexture(fuzzyStates[(int)Emotions.
           DISGUST] / d, me.EfxProcessor.TextureClone(me.DisgustTexture), 25);
   }
   if (fuzzyStates[(int)Emotions.SURPRISE] / d > 0.25f || rankOrder[0] ==
       (int)Emotions.SURPRISE)
   {
       surprise = me.EfxProcessor.ScaleTexture(fuzzyStates[(int)Emotions.
           SURPRISE] / d, me.EfxProcessor.TextureClone(me.SurpriseTexture),
           25);
   }

   //next, get ready to blend a few of the textures together
   Color[] dataA = new Color[10000];
   Color[] dataB = new Color[10000];

   float percent = .0f;

   Emotions e = (Emotions)rankOrder[0];
   switch (e)
   {
       case Emotions.ANGER:
          if (sumTexture != null)
          {
              //start by blending this texture with the previous sumTexture
              sumTexture.GetData<Color>(dataA);
              anger.GetData<Color>(dataB);
              sumTexture = GameState.Utilities.ImageEffects.
                  ThreadedBlendMouth(.75f, dataB, dataA, sumTexture);
              sumTexture.GetData<Color>(dataA);
          }
          else
          {
             anger.GetData<Color>(dataA);
             sumTexture = anger;
          }
          percent = fuzzyStates[(int)Emotions.ANGER] / d;
          break;
      case Emotions.JOY:
          if (sumTexture != null)
          {
               //start by blending this texture with the previous sumTexture
               sumTexture.GetData<Color>(dataA);
               joy.GetData<Color>(dataB);
               sumTexture = GameState.Utilities.ImageEffects.
                   ThreadedBlendMouth(.75f, dataB, dataA, sumTexture);
               sumTexture.GetData<Color>(dataA);
          }
          else
          {
             joy.GetData<Color>(dataA);
             sumTexture = joy;
          }
          percent = fuzzyStates[(int)Emotions.JOY] / d;
          break;
      case Emotions.SADNESS:
          if (sumTexture != null)
          {
              //start by blending this texture with the previous sumTexture
              sumTexture.GetData<Color>(dataA);
              sad.GetData<Color>(dataB);
              sumTexture = GameState.Utilities.ImageEffects.
                  ThreadedBlendMouth(.75f, dataB, dataA, sumTexture);
              sumTexture.GetData<Color>(dataA);
          }
          else
          {
             sad.GetData<Color>(dataA);
             sumTexture = sad;
          }
          percent = fuzzyStates[(int)Emotions.SADNESS] / d;
          break;
      case Emotions.FEAR:
          if (sumTexture != null)
          {
             //start by blending this texture with the previous sumTexture
             sumTexture.GetData<Color>(dataA);
             fear.GetData<Color>(dataB);
             sumTexture = GameState.Utilities.ImageEffects.
                 ThreadedBlendMouth(.75f, dataB, dataA, sumTexture);
             sumTexture.GetData<Color>(dataA);
         }
         else
         {
            fear.GetData<Color>(dataA);
            sumTexture = fear;
         }
         percent = fuzzyStates[(int)Emotions.FEAR] / d;
         break;
     case Emotions.DISGUST:
         if (sumTexture != null)
         {
             //start by blending this texture with the previous sumTexture
             sumTexture.GetData<Color>(dataA);
             disgust.GetData<Color>(dataB);
             sumTexture = GameState.Utilities.ImageEffects.
                 ThreadedBlendMouth(.75f, dataB, dataA, sumTexture);
             sumTexture.GetData<Color>(dataA);
         }
         else
         {
            disgust.GetData<Color>(dataA);
            sumTexture = disgust;
         }
         percent = fuzzyStates[(int)Emotions.DISGUST] / d;
         break;
     case Emotions.SURPRISE:
         if (sumTexture != null)
         {
             //start by blending this texture with the previous sumTexture
             sumTexture.GetData<Color>(dataA);
             surprise.GetData<Color>(dataB);
             sumTexture = GameState.Utilities.ImageEffects.
                 ThreadedBlendMouth(.75f, dataB, dataA, sumTexture);
             sumTexture.GetData<Color>(dataA);
        }
        else
        {
            surprise.GetData<Color>(dataA);
            sumTexture = surprise;
        }
        percent = fuzzyStates[(int)Emotions.SURPRISE] / d;
        break;
 }


//blend the top few emotions that are greater than 25 percent
int cnt = 1;
for (int i = 1; i < 3; i++)
{
    if (fuzzyStates[rankOrder[i]] / d > 0.25f)
    {
        cnt++;
        Emotions f = (Emotions)rankOrder[i];
        switch (f)
        {
            case Emotions.ANGER:
                anger.GetData<Color>(dataB);
                sumTexture = GameState.Utilities.ImageEffects.
                    ThreadedBlendMouth(percent, dataB, dataA,
                    sumTexture);
                sumTexture.GetData<Color>(dataA);
                percent = fuzzyStates[(int)Emotions.ANGER] / d;
                break;
            case Emotions.JOY:
                joy.GetData<Color>(dataB);
                sumTexture = GameState.Utilities.ImageEffects.
                    ThreadedBlendMouth(percent, dataB, dataA,
                    sumTexture);
                sumTexture.GetData<Color>(dataA);
                percent = fuzzyStates[(int)Emotions.JOY] / d;
                break;
            case Emotions.SADNESS:
                sad.GetData<Color>(dataB);
                sumTexture = GameState.Utilities.ImageEffects.
                    ThreadedBlendMouth(percent, dataB, dataA,
                    sumTexture);
                sumTexture.GetData<Color>(dataA);
                percent = fuzzyStates[(int)Emotions.SADNESS] / d;
                break;
            case Emotions.FEAR:
                fear.GetData<Color>(dataB);
                sumTexture = GameState.Utilities.ImageEffects.
                    ThreadedBlendMouth(percent, dataB, dataA,
                    sumTexture);
                sumTexture.GetData<Color>(dataA);
                percent = fuzzyStates[(int)Emotions.FEAR] / d;
                break;
            case Emotions.DISGUST:
                disgust.GetData<Color>(dataB);
                sumTexture = GameState.Utilities.ImageEffects.
                    ThreadedBlendMouth(percent, dataB, dataA,
                    sumTexture);
                sumTexture.GetData<Color>(dataA);
                percent = fuzzyStates[(int)Emotions.DISGUST] / d;
                break;
            case Emotions.SURPRISE:
                surprise.GetData<Color>(dataB);
                sumTexture = GameState.Utilities.ImageEffects.
                    ThreadedBlendMouth(percent, dataB, dataA,
                    sumTexture);
                sumTexture.GetData<Color>(dataA);
                percent = fuzzyStates[(int)Emotions.SURPRISE] / d;
                break;
       }
       dataB = new Color[10000];
    }
}
dataB = new Color[10000];
bored.GetData<Color>(dataB);
percent = (Math.Min(Math.Abs(me.Boredom), 75) / d) / cnt;
sumTexture = GameState.Utilities.ImageEffects.
    ThreadedBlendMouth(percent, dataB, dataA, sumTexture);
}

Figure 6.15 shows the resulting blendified expressions. With that, Valued Reader, I am satisfied (for now!!) with the intentionality of our creation—that is, at least insofar as expressing personality dynamics. It’s now time to move on to the last section of the book—an open and frank discussion of creating iconic behaviors using synthetic personality.

Emo little buggers!

Figure 6.15. Emo little buggers!

The Principle of Amplification Through Simplification Applied to Video-Game Characters

The hard part is done, Valued Reader. Well, that would be true if we were working from a complete game design, instead of me making this up as I go along. (Note to self: Remove this last bit about making stuff up before this goes to press.) We now have little dudes that run around and emote pretty well. But if you watch them for any length of time (or, as I have, until your family thinks you are loopy, and you’ve fallen asleep in your chair), they all still seem to act mostly the same. We are starting to get some emergence and individualization, but it’s not enough yet. It’s time to close the loop and add emotions to the behavior controls. To make this easy, we will create a few new methods in the PersonalityType class and work within our established architecture. That means the behavior state machine will remain largely untouched in this section; mostly, we will just add processing calls to the PersonalityType methods we are about to add.

At this stage of the game, it is very important to have a clear idea of who your characters are and what roles they will fill. In our previous discussion, we gave pretty good descriptions of how these personalities will act in our game, so let’s build from there:

  • Absent-minded despots live for conquest; are aggressive, territorial, and ascetic; and love to order Suma around.

  • Timid underlings are timid, shy, indecisive, followers. They want to avoid confrontation and to be led. They are the classic “bottler”—slow to boil, but when the top goes ….

  • Lone wolves are solitary, agoraphobic, undependable, and aloof. These Suma just want to be left alone, and don’t want anysuma near them.

  • Sumacidal explorers are blood thirsty, driven to seek out new life and new civilizations, and violent. These Suma have no fuse. It’s on, and it’s on all the time—unless, of course, they are distracted….

  • Egocentric narcissists are completely self involved. They don’t believe pleasure comes from anysuma else, and couldn’t care less about anysuma but themelves.

  • Plural hedonists are driven to seek pleasure in the company of another. They almost don’t care what they are doing as long as it is pleasurable and with someone.

Right away, I’m starting to think about the ability to issue orders versus making requests. For example, if an ABM wants to dance with someone, he wouldn’t ask, he would order them to comply (like the AI does now). In response, however, I can imagine TUs and PHs jumping to and heading right over, while an SE would just fly into a rage. A LW might say he’s coming right over, but then never arrive, while an EN might ignore the request completely.

To begin our rabid quest for total domination of the Suma, we need to create this method in PersonalityType:

/// <summary>
/// Processes actions to be taken by individual types in response to behavior
/// states
/// </summary>
/// <param name="gameTime"></param>
public void PersonalityAction(GameTime gameTime)
{
    if (!me.FSM.CurrentState.AIControl)
    {
        return;
    }
    switch (type)
    {
        case PersonalityTypes.AMD:
             AMDAction(gameTime);
             break;
        case PersonalityTypes.EN:
             ENAction(gameTime);
             break;
        case PersonalityTypes.LW:
             LWAction(gameTime);
             break;
        case PersonalityTypes.PH:
             PHAction(gameTime);
             break;
        case PersonalityTypes.SE:
             SEAction(gameTime);
             break;
        case PersonalityTypes.TU:
             TUAction(gameTime);
             break;
    }
}

Then we need to create each of the specific methods in turn, using this starting point:

private void TUAction(GameTime gameTime)
{
   timeInAction += gameTime.TotalGameTime.Milliseconds;
   Character t = me.FSM.CurrentState.Target;
   switch (behavior)
   {
       case "FIGHT":
           break;
       case "CHAT":
           break;
       case "DANCE_WITH":
           break;
       case "CHASE":
           break;
       case "EVADE":
           break;
       case "GOTO":
           break;
       case "DANCE":
           break;
       case "MOVE":
           break;
       case "FREE_ROAM":
           break;
       case "STOP":
           break;
       case "SLEEP":
           break;
    }
}

The goal for each of these methods is to create individualized behavior for each type of class. We can use any information we need to accomplish this, but remember that these behaviors will be better off if they are iconic to the type rather than complicated. For instance, here is my SEAction() method (the actions for the remaining types are on the CD):

private void SEAction(GameTime gameTime)
{
    timeInAction += gameTime.TotalGameTime.Milliseconds;
    Character t = me.FSM.CurrentState.Target;
    Character et = Game1.GameStateMan.CharManager.GetTargetInRange(me, 9000f);
    switch (behavior)
    {
        case "FIGHT":
            //I'm a psycho, remember??
            if (t != null && t.PersonalityType.IsOrder() && timeInAction < 1000f)
            {
                em.Anger = MaxInput;
                me.ChangeAttraction(Game1.GameStateMan.CharManager.
                    GetToonId(t), -em.Anger);
            }
            else if (et != null && rand.Next(100) > 50)
            {
                //WHAT ARE YOU LOOKING AT?!??!
                em.Anger = em.MaxIntensity;
                me.FSM.Token = "FIGHT";
                me.FSM.ExecuteTransition(false);
                me.FSM.CurrentState.Target = et;
                if (me.FSM.CurrentState is Logic.CharacterFSMStates.FightState)
                {
                    (me.FSM.CurrentState as Logic.CharacterFSMStates.
                        FightState).InitCharacter = me;
                }
                me.FSMUpdate = true;
           }
           else if (t != null && !t.FSM.CurrentState.Identifier.Equals("FIGHT"))
           {
                em.Joy = LargeInput;
                em.Anger = -1 * SmallInput;
                me.FSM.Token = "CHASE";
                me.FSM.ExecuteTransition(false);
                me.FSM.CurrentState.Target = t;
           }
           else
           {
                //I will fight you forever
                em.Anger = MaxInput;
                em.Joy = SmallInput;
           }
           break;
      case "CHAT":
      case "DANCE_WITH":
          //I don't know whether to fight you or just get really mad at you
          if (t != null && me.GetAttraction(Game1.GameStateMan.CharManager.
              GetToonId(t)) >= 0 && et == null)
          {
              em.Joy = MinInput;
              em.Anger = -1 * MinInput;
              em.Sadness = -1 * MaxInput;
              em.Disgust = -1 * MinInput;
          }
          else if ((et != null) && em.Anger/em.MaxIntensity > 0.15f)
          {
              //WHAT ARE YOU LOOKING AT?!??!
              em.Anger = em.MaxIntensity;
              me.FSM.Token = "FIGHT";
              me.FSM.ExecuteTransition(false);
              me.FSM.CurrentState.Target = et;
              if (me.FSM.CurrentState is Logic.CharacterFSMStates. FightState)
              {
                  (me.FSM.CurrentState as Logic.CharacterFSMStates.
                      FightState).InitCharacter = me;
              }
              me.FSMUpdate = true;
         }
         else if (timeInAction > 32000f)
         {
              me.ForcedStateChange = true;
         }
         else
         {
              em.Anger = -1 * MinInput;
              em.Sadness = -1 * MaxInput;
              em.Disgust = -1 * MinInput;
         }
         break;
     case "CHASE":
         if (t != null && em.Anger / em.MaxIntensity > 0.25f)

         {
                  //make me CHASE you?? You are mine!
                  em.Anger = em.MaxIntensity;
                  me.FSM.Token = "FIGHT";
                  me.FSM.ExecuteTransition(false);
                  me.FSM.CurrentState.Target = t;
                  if (me.FSM.CurrentState is Logic.CharacterFSMStates.
                         FightState)
                  {
                      (me.FSM.CurrentState as Logic.CharacterFSMStates.
                          FightState).InitCharacter = me;
                  }
                  me.FSMUpdate = true;
             }
             else if (timeInAction > 20000f)
             {
                me.ForcedStateChange = true;
             }
             else
             {
                em.Surprise = MinInput;
                em.Anger = MedInput;
                em.Disgust = LargeInput;
             }
             break;
         case "EVADE":
             if (t != null && em.Joy < em.Anger)
             {
                //oh you want to chase me? You got me bucko!
                em.Anger = em.MaxIntensity;
                me.FSM.Token = "FIGHT";
                me.FSM.ExecuteTransition(false);
                me.FSM.CurrentState.Target = t;
                if (me.FSM.CurrentState is Logic.CharacterFSMStates.
                    FightState)
             {
                   (me.FSM.CurrentState as Logic.CharacterFSMStates.
                       FightState).InitCharacter = me;
             }
             me.FSMUpdate = true;
        }
        else if (timeInAction > 15000f)

        {
           me.ForcedStateChange = true;
        }
        else
        {
           //meh..
           em.Joy = -1 * (timeInAction / 12500f);
           em.Anger = -1 * MaxInput;
           em.Surprise = -1 * MedInput;
        }
        break;
    case "GOTO":
        if (et != null && t != null && me.GetAttraction(Game1.
             GameStateMan.CharManager.GetToonId(t)) <= 0 && em.Anger / em.
             MaxIntensity > 0.1f)
        {
             //WHAT ARE YOU LOOKING AT?!??!
             em.Anger = em.MaxIntensity;
             me.FSM.Token = "FIGHT";
             me.FSM.ExecuteTransition(false);
             me.FSM.CurrentState.Target = et;
             if (me.FSM.CurrentState is Logic.CharacterFSMStates.FightState)
             {
                 (me.FSM.CurrentState as Logic.CharacterFSMStates.
                     FightState).InitCharacter = me;
             }
             me.FSMUpdate = true;
        }
        else if (timeInAction > 10000f && em.Anger/em.MaxIntensity > .4f)
        {
             me.ForcedStateChange = true;
        }
        else
        {
             em.Anger = MinInput;
        }
        break;
    case "DANCE":
    case "MOVE":
    case "FREE_ROAM":
        if (et != null && rand.Next(100) > 90 && em.Anger/em.MaxIntensity > 0.2f)
        {
            //WHAT ARE YOU LOOKING AT?!??!
            em.Anger = em.MaxIntensity;
            me.FSM.Token = "FIGHT";
            me.FSM.ExecuteTransition(false);
            me.FSM.CurrentState.Target = et;
            if (me.FSM.CurrentState is Logic.CharacterFSMStates.FightState)
            {
                (me.FSM.CurrentState as Logic.CharacterFSMStates.
                    FightState).InitCharacter = me;
            }
            me.FSMUpdate = true;
       }
       else if (timeInAction > 60000f && em.Joy/em.MaxIntensity > .75f)
       {
            me.ForcedStateChange = true;
       }
       else
       {
            em.Anger = -1 * SmallInput;
            em.Surprise = -1 * SmallInput;
            em.Disgust = -1 * SmallInput;
            em.Joy = MedInput;
            em.Fear = -1 * SmallInput;
       }
       break;
   case "STOP":
   case "SLEEP":
       if (et != null && rand.Next(100) > 50 && em.Anger/em.MaxIntensity
           > 0.2f)
       {
           //WHAT ARE YOU LOOKING AT?!??!
           em.Anger = em.MaxIntensity;
           me.FSM.Token = "FIGHT";
           me.FSM.ExecuteTransition(false);
           me.FSM.CurrentState.Target = et;
           if (me.FSM.CurrentState is Logic.CharacterFSMStates.
                FightState)
           {
               (me.FSM.CurrentState as Logic.CharacterFSMStates.
                   FightState).InitCharacter = me;
           }
           me.FSMUpdate = true;
       }
       else
       {
          em.Anger = -1 * MinInput;
          em.Surprise = -1 * MinInput;
          em.Disgust = -1 * MinInput;
          em.Joy = MinInput;
          em.Fear = -1 * MinInput;
       }
       break;
   }
}

As you can see, the SE is pretty focused on laying out the beat down (or finding someone to lay out the beat down on), as any good maniac should be. In contrast, my EN type tries to avoid contact with others if it is unhappy, and my LW type actively runs away. In other words, these characters have the ability to act differently from one another, depending on the personality types—and even if they are the same personality type, their unique mental state will drive them towards independent actions.

A Few Parting Words

How did we get here? We explored what it takes to make AI for video games in Chapter 1, “Introduction to Video Game Artificial Intelligence (VGAI),” where we concluded that clever engineering (as opposed to strong academic AI) that leverages the lessons learned from academic AI can sometimes be convincing enough for video games. In Chapter 2, “The Interaction Hierarchy,” we explored what it takes to create interaction, engagement (in all four of its forms), and immersion. We also discussed the scaffolding architecture that links all three concepts. We undertook that discussion in an effort to create more believable characters and games that are more fun to play. After agreeing that the Tic-TacToe example had exhausted its usefulness in exploring more complex character AI, we created a strong foundation for further work by developing a 2D sprite-based game engine, complete with a GUI that supported indirect control of the characters, in Chapter 3, “Building 2D Sprite-Based Games with Microsoft XNA 3.0,” and augmenting that engine with finite state machine controls for behavior and a basic VGAI routine for selecting behaviors in Chapter 4, “Indirect Control in a 2D Sprite-Based Game.” Chapter 5, “Gidgets, Gadgets, and Gizbots,” led us to back to the discussion of believability and introduced the fidelity hierarchy. We corrected imbalances in modeling, animation, and cognitive fidelities, as well.

All of this groundwork led us to the point where we could discuss the pinnacle of the fidelity hierarchy—emotional fidelity—and enabled us to focus on developing a computational model of personality. Our current example boasts characters that can communicate personality via expression, eye movements, visual indicators, as well as through behavior—a solid example of creative intentionality. We have, in our current code base, individual characters, motivated by different mental states and different drives. They behave differently, events in the game have different emotional impact on these individuals, and their reactions are suitably different from one another.

Adding more and more complex, emotionally driven behavior is as simple as writing the code to make these little guys do what we want, as we have the underlying architecture completely written. It is true that at present the goals the characters set for themselves are extremely short term (but hey, alien froggies who travel across the galaxy to destroy our planet but forget to bring entertainment probably aren’t the longest-term planners around). As a future step, we could augment what we’ve written with longer-term planning and variable motivation based off of emotional state. We could also strengthen the interactivity between characters by creating more base behaviors and establish behavior “grammars” linked to personality type or dramatic role that more firmly situate characters in the game world as well as providing for stronger individualization.

The final version of the code is included on the CD. I’ve taken the liberty of doing some tuning and some cleaning up of various methods; I hope you don’t mind me doing that without explaining it. Most of these changes amount to just tweaking values to make something look better (in my opinion) or adding guards on those pesky methods that require the parameter to be valid (like accessing something stored in an array). None of it was important to the main themes here.

I’ve tried to comment as much of the code as I could remember to do, and I hope you find it useful. If you come across bugs, mistakes, things that could be done better, or outright silliness on my part, please don’t hesitate to drop me a line. I would love to hear from you.

Throughout the course of this book, I’ve subtly emphasized the importance of moving away from this mad rush toward hyper-realism in favor of more iconic forms. This argument extends perfectly from a discussion of models, textures, and animations to behaviors and actions. Hopefully, the iconic behaviors we developed in the last few chapters will lead you, Valued Reader, to the same conclusion that I’ve reached over the years: that believability hinges on appropriate levels of realism rather than high levels of realism.

Characters are what makes fictional worlds go ’round. Video-game developers must find ways of creating real characters for people to play with. It is my solemn hope, Valued Reader, that this game example and this book in general have left you thinking “What if?” and have sparked your creativity for building emotionally salient, engaging, and immersive video-game characters. I’m looking forward to seeing some emotionally valid characters in your next game.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.84.157