Chapter 8. Scripted AI and Scripting Engines

This chapter discusses some of the techniques you can use to apply a scripting system to the problem of game AI, and the benefits you can reap from doing this. At its most basic level, you can think of scripting as a very simple programming language tailored to a specific task related to the game in question. Scripting can be an integral part of the game development process, as it enables the game designers rather than the game programmers to write and refine much of the game mechanics. Players also can use scripting to create or modify their own game worlds or levels. Taken a step further, you can use a scripting system in a massively multiplayer online role-playing game (MMORG) to alter the game behavior while the game is actually being played.

You can take several approaches when implementing a scripting system. A sophisticated scripting system might interface an already existing scripting language, such as Lua or Python, for example, with the actual game engine. Some games create a proprietary scripting language designed for the needs of the individual game. Although it’s sometimes beneficial to use those methods, it’s easier to have the game parse standard text files containing the scripting commands. Employing this approach, you can create scripts using any standard text editor. In a real game, the scripts can be read in and parsed when the game first starts, or at some other specified time. For example, scripts that control creatures or events in a dungeon can be read in and parsed when the player actually enters the dungeon area.

In the scope of game AI, you can use scripting to alter opponent attributes, behavior, responses, and game events. This chapter looks at all these uses.

Scripting Techniques

The actual scripting language used in a game is ultimately up to the game designers and programmers. It can resemble preexisting languages such as C or C++, or it can take a totally unique approach; perhaps even a graphical rather than a text-based approach. Deciding how the scripting system looks and works depends primarily on who will be using the scripting system. If your target is the end player, a more natural language or graphical approach might be beneficial. If the system is primarily for the designers and programmers, it might not be beneficial to spend your development time on a complex and time-consuming natural language parsing system. A quick and dirty approach might be better.

You also should consider other factors when developing a scripting system. Perhaps you want the script to be easy to read and write for the game designers, but not necessarily for the game players. In this case, you might want to use a form of encryption. You also could develop a script compiler so that the end result is less readable to humans.

In this chapter we create simple scripting commands and save them in standard text files. We want to avoid the need for a complex language parser, but at the same time we have been careful to choose a vocabulary that makes it relatively easy for humans to read and write the scripts. In other words, we use words that accurately reflect the aspect of the game that the script is altering.

Scripting Opponent Attributes

It’s common and beneficial to specify all the basic attributes of each AI opponent by using some type of scripting. This makes it easy to tweak the AI opponents throughout the development and testing process. If all the vital data were hardcoded into the program, you would have to recompile for even the most basic change.

In general, you can script opponent attributes such as intelligence, speed, strength, courage, and magical ability. In reality, there is no limit to the possible number or types of attributes you can script. It really comes down to the type of game you’re developing. Of course, the game engine ultimately will use these attributes whenever a computer-controlled friend or foe interacts with the player. For example, an opponent that has a higher intelligence attribute would be expected to behave differently from one of lower intelligence. Perhaps a more intelligent opponent would use a more sophisticated pathfinding algorithm to track down a player, while a less intelligent opponent might become easily confused when trying to reach the player.

Example 8-1 shows a basic script you can use to set game attributes.

Example 8-1. Basic script to set attributes
CREATURE=1;
INTELLIGENCE=20;
STRENGTH=75;
SPEED=50;
END

In this example, our script parser has to interpret five commands. The first, CREATURE, indicates which AI opponent is being set. The next three, INTELLIGENCE, STRENGTH, and SPEED, are the actual attributes being set. The final command, END, tells the script parser that we are finished with that creature. Anything that follows comprises a new and separate block of commands.

It would be just as easy to include the numbers 1,20,75,50 in a file and thus avoid any need for parsing the script text. That approach works and developers use it frequently, but it does have some disadvantages. First, you lose quite a bit of readability. Second, and most important, your scripting system can increase in complexity to the point where specifying attributes by just including their numerical values in a file becomes impractical. Example 8-2 shows how a script can become more complicated by using a conditional statement.

Example 8-2. Conditional script to set attributes
CREATURE=1;
If (LEVEL<5)
   BEGIN
      INTELLIGENCE=20;
      STRENGTH=75;
      SPEED=50;
   END
ELSE
   BEGIN
      INTELLIGENCE=40;
      STRENGTH=150;
      SPEED=100;
   END
END

As shown in Example 8-2, we now have conditional statements that initialize the creature attributes to different values depending on the current game level.

Basic Script Parsing

Now that we’ve shown what a basic attribute script looks like, we’re going to explore how a game reads and parses a script. As an example, we will use a basic script to set some of the attributes for a troll. We will create a text file called Troll Settings.txt. Example 8-3 shows the contents of the troll settings file.

Example 8-3. Basic script to set attributes
INTELLIGENCE=20;
STRENGTH=75;
SPEED=50;

Example 8-3 is a simple example that sets only three creature attributes. However, we will set up our code so that we can easily add more attributes with very little change to our script parser. Basically, we are going to set up our parser so that it will search a given file for a specified keyword and then return the value associated with the keyword. Example 8-4 shows how this might look in an actual game.

Example 8-4. Basic script to set attributes
intelligence[kTroll]=fi_GetData("Troll Settings.txt,
                                "INTELLIGENCE");
strength[kTroll]= fi_GetData("Troll Settings.txt,
                             "STRENGTH");
speed[kTroll]= fi_GetData("Troll Settings.txt,
                          "SPEED");

Example 8-4 shows three hypothetical arrays that can store creature attributes. Rather than hardcoding these values into the game, they are loaded from an external script file called Troll Settings.txt. The function fi_GetData traverses the external file until it finds the specified keyword. It then returns the value associated with that keyword. The game designers are free to tweak the creature setting without the need to recompile the program code after each change.

Now that we have seen how you can use the fi_GetData function to set the attributes for a troll, let’s go a step further. Example 8-5 shows how the function accomplishes its task.

Example 8-5. Reading data from a script
int fi_GetData(char filename[kStringLength], char searchFor[kStringLength])
{	
FILE   *dataStream;
char   inStr[kStringLength];
char   rinStr[kStringLength];
char   value[kStringLength];
long   ivalue;
int    i;
int    j;
dataStream = fopen(filename, "r" );
if (dataStream != NULL)
   {
       while (!feof(dataStream))
           {
              if (!fgets(rinStr,kStringLength,dataStream))
                 {
                    fclose( dataStream );
                    return (0);
                 }
            j=0;
            strcpy(inStr,"");
            for (i=0;i<strlen(rinStr);i++)
               if (rinStr[i]!=' ')
                  {
                     inStr[j]=rinStr[i];
                     inStr[j+1]='';
                     j++;
                  }
            if (strncmp(searchFor, inStr,
                        strlen(searchFor)) == 0)
               {
                  j=0;
                  for(i=strlen(searchFor);
                      i<kStringLength;
                      i++)
                     {
                        if (inStr[i]==';')
                           break;
                        value[j]=inStr[i];
                        value[j+1]='';
                        j++;
                     }
                  StringToNumber(value, &ivalue);
                  fclose( dataStream );
                  return ((int)ivalue);
               }
            }
         fclose( dataStream );
         return (0);
      }
   return (0);
}

The function in Example 8-5 begins by accepting two string parameters. The first specifies the name of the script file to be searched and the second is the search term. The function then opens the text file using the specified file name. Once the file is opened, the function begins traversing the script file one text line at a time. Each line is read in as a string.

Notice that each line is read into the variable rinStr, and then it’s copied immediately to inStr, but without the spaces. The spaces are eliminated to make the parsing a bit more foolproof. This prevents our script parser from getting tripped up if the script writer adds one or more spaces before or after the search term or attributes. Once we have a script line stored in a string, sans spaces, we can search for the search term.

As you recall, we passed our search term to the fi_GetData function by using the string variable searchFor. At this point in the function, we use the C function strncmp to search inStr for the search term.

If the search term is not found, the function simply proceeds to read the next text line in the script file. However, if it is found, we enter a new loop that copies into a new string named value the part of inStr that contains the attribute value. The string value is converted to an integer value by calling the outside function StringToNumber. The fi_GetData function then returns the value in ivalue.

This function is written in a very generic way. No search terms are hardcoded into the function. It simply searches the given file for a search term and then returns an integer value associated with it. This makes it easy to add new attributes to our program code.

Also, note that this is one area of game development where it is important to check for errors. This is true particularly if you want players as well as game designers to use the scripting system. You should never assume any of the scripts being parsed are valid. For example, you shouldn’t rely on the script writers to keep all the numeric values within legal bounds.

Scripting Opponent Behavior

Directly affecting an opponent’s behavior is one of the most common uses of scripting in game AI. Some of the previous examples showed how scripting attributes can have an indirect effect on behavior. This included such examples as modifying a creature’s intelligence attribute, which presumably would alter its behavior in the game.

Scripting behavior enables us to directly manipulate the actions of an AI opponent. For this to be useful, however, we need some way for our script to see into the game world and check for conditions that might alter our AI behavior. To accomplish this we can add predefined global variables to our scripting system. The actual game engine, not our scripting language, will assign the values in these variables. They are used simply as a way for the script to evaluate a particular condition in the game world. We will use these global variables in conditional scripting statements. For example, in our scripting system we might have a global boolean variable called PlayerArmed which will direct a cowardly troll to ambush only unarmed opponents. Example 8-6 shows how such a script might look.

Example 8-6. Basic behavior script
If (PlayerArmed==TRUE)
   BEGIN
      DoFlee();
   END
ELSE
   BEGIN
      DoAttack();
   END

In Example 8-6, the script does not assign the value PlayerArmed. It represents a value within the game engine. The game engine will evaluate the script and link this behavior to the cowardly troll.

In this example, the value PlayerArmed is a simple boolean value that represents nothing more than another boolean value within the game engine. There certainly is nothing wrong with this, but scripting is more useful when you use simple global variables which represent a more complex series of evaluations. For example, in this sample script we checked whether the player was armed. Although that might be useful for an opponent to know, it doesn’t necessarily represent how challenging the opponent will be in a fight.

Many factors could contribute to how challenging a potential opponent will be. We can make our scripting system even more powerful if we evaluate these conditions in the game engine and then make the result available to the script as a single global variable. For example, we could use a Bayesian network to evaluate how tough an opponent the player is and then make the result available in a variable such as PlayerChallenge. The script shown in Example 8-7 is just as simple as the one in Example 8-6, but it can have a much more sophisticated effect on the gameplay.

Example 8-7. Behavior script
If (PlayerChallenge ==DIFFICULT)
   BEGIN
      DoFlee();
   END
ELSE
   BEGIN
      DoAttack();
   END

In the case of Example 8-7, PlayerChallenge could represent a series of complex evaluations that rank the player. Some of the factors could include whether the player is armed, the type of armor being worn, the current player’s health, whether any other players in the area might come to his defense, and so on.

Another aspect of behavior that you can script is AI character movement. We can take a concept, such as pattern movement from Chapter 3, and implement it in a scripting system. For example, it might be useful for the game designer to establish patrol patterns for AI characters. Chapter 3 showed some examples of hardcoded pattern movement. Of course, hardcoding behavior has many disadvantages. It’s much more difficult to tweak a game’s design if a recompile is needed after every minor change. Figure 8-1 shows an example of a movement pattern that a game designer can implement using a scripting system.

Scripted pattern movement
Figure 8-1. Scripted pattern movement

Example 8-8 shows how we can construct a script to achieve the desired behavior.

Example 8-8. Pattern movement script
If (creature.state==kPatrol)
   begin
      move(0,1);
      move(0,1);
      move(0,1);
      move(0,1);
      move(0,1);
      move(-1,0);
      move(-1,0);
      move(0,-1);
      move(0,-1);
      move(0,-1);
      move(0,-1);
      move(0,-1);
      move(0,1);
      move(0,1);
   end

pattern. Each move is shown as a single unit change from the previous position. See Chapter 3 for a detailed explanation of pattern movement techniques.

Scripting Verbal Interaction

The benefits of scripting go beyond just making an AI opponent more sophisticated and challenging. Many types of games incorporate intelligent behavior in ways that aren’t meant to be a direct challenge to the player. A role-playing game, for example, might provide the player with a series of subtle hints meant to move the story along. Scripting is an excellent way to enable the game designer to create a compelling story without the need to alter the actual game program.

Intelligent behavior can make a game more challenging, but verbal responses that are intelligent and appropriate to the situation can go even farther when creating an immersive environment for the player. Verbal interaction can range from helpful hints from a friendly nonplayer character to taunts from an adversary. Verbal interaction seems most intelligent and immersive when it relates to the current game situation. This means the game AI needs to check a given set of game parameters and then respond to them accordingly.

For example, how a player is armed might be one parameter that can be checked. We can then have an adversarial AI character comment on how ineffective that weapon will be once combat starts. This seems more intelligent and immersive because it’s not just a random taunt. It applies to the current game situation. It makes it seem as though the computer-controlled characters are aware of what’s happening in the game. A quick example of how this script might look is shown in Example 8-9.

Example 8-9. Verbal taunt script
If (PlayerArmed ==Dagger)
   Say("What a cute little knife.");
If (PlayerArmed ==Bow)
   Say("Drop the bow now and I'll let you live.");
If (PlayerArmed ==Sword)
   Say("That sword will fit nicely in my collection.");
If (PlayerArmed ==BattleAxe)
   Say("You're too weak to wield that battle axe.");

As Example 8-9 shows, knowing a bit about the current game situation can add an immersive effect to gameplay. This is much more effective than simply adding random general taunts.

So, an important aspect of a scripting system is to enable the script writer to see what’s happening inside the game engine. The more game elements the script can see, the better. Figure 8-2 shows a hypothetical game scenario in which an evil giant is chasing the player. In this case, the game AI is able to use unique elements of the game state to supply a taunt appropriate to the situation. In this case, we know that the adversary is a giant, the player is a human, and the player is armed with a staff.

Giant taunt
Figure 8-2. Giant taunt

Example 8-10 shows how the game AI might can be an appropriate taunt during a battle between a computer-controlled giant and a player-controlled human. In a real game you probably would want to add multiple responses for each given situation and then randomly select among them. This would help prevent the responses from becoming repetitive and predictable.

Example 8-10. Giant taunt script
If (Creature==Giant) and (player==Human)
   begin
      if (playerArmed==Staff)
         Say("You will need more than a staff, puny human!");
      if (playerArmed==Sword)
         Say("Drop your sword and I might not crush you!");
      if (playerArmed==Dagger)
         Say("Your tiny dagger is no match for my club!");
      end

Of course, this type of scripting isn’t limited to adversarial characters that are out to kill the players. Benevolent computer-controlled characters can use the same techniques. This can help the script writer create an engaging and immersive plot. Example 8-11 shows how a script helps construct a plot and guide the player actions toward the game goals.

Example 8-11. Benevolent AI script
If (Creature==FriendlyWizard)
   begin
      if (playerHas==RedAmulet)
         Say("I see you found the Red Amulet.
              Bring it to the stone temple
              and you will be rewarded.");
   end

As Example 8-11 shows, a vital piece of information concerning where the amulet should be placed won’t be revealed to the player until the amulet is found and the player confronts the friendly wizard.

The previous script examples show how game AI can respond in a given situation, but it’s also sometimes necessary for game characters to have some type of verbal interaction with the player. This could be benevolent characters meant to provide the player with helpful information, or perhaps a less-than-honest character meant to intentionally mislead the player.

In this type of scenario, the player needs some mechanism to input text into the game. The game engine then makes the text strings available to the script system, which analyzes the text and provides an appropriate response. Figure 8-3 shows how this might appear in an actual game.

Merlin
Figure 8-3. Merlin

In the case of Figure 8-3, the player would type in the text “What is your name?” and the scripting system would return the text “I am Merlin.” Example 8-12 shows a basic script that you could use to accomplish this.

Example 8-12. Basic “What is your name?” script
                    If Ask("What is your name?")
                       begin
                          Say("I am Merlin.");
                       end

Of course, Example 8-12 does have one serious flaw. It works only when the player types in the exact text of the question as it appears in the script. In reality, you can form a question in many ways. For example, what happens if the player enters one of the lines of text shown in Example 8-13?

Example 8-13. Example player input
What's your name?
Whats your name?
What is your name.
What is thy name?
What is your name, Wizard?
Hello, what is your name?

As you can see, the script in Example 8-12 would fail for all the questions shown in Example 8-13, even though it’s quite obvious what’s being asked. Not only can you ask a question in many ways, but we also have to consider the possibility that the player might not form the question in a correct manner. In fact, you can see that one of the example questions ends in a period rather than a question mark. We could have strict requirements for the player-entered text, but it would have the effect of removing the player from the immersive effect of the game whenever he made the inevitable minor error.

One alternative to checking each literal text string is to create a language parser to decipher each sentence to determine exactly what is being asked. For some games a sophisticated language parser might be appropriate; however, for most games there is a simpler approach. As you saw in Example 8-13, you can form the same question in many ways, but if you’ll notice, they all have something in common. They all contain the words “what” and “name.” So, instead of checking for each literal text string, we can simply search for and respond to particular keywords. In this case, the scripting engine simply checks for the presence of given keywords within a text string.

As Example 8-14 shows, the script is checking for the presence of two keywords in the player-entered text. Using this approach, the script responds correctly to every question in Example 8-13.

Example 8-14. Keyword scripting
If (Ask("what") and Ask("name") )
   begin
      Say("I am Merlin.");
   end

Now that we’ve shown you how to write a typical script to check player input for a given set of keywords, let’s look at how the actual game engine checks player input for a given keyword. Example 8-15 shows how to do this.

Example 8-15. Searching for keywords
Boolean FoundKeyword(char inputText[kStringLength], char
searchFor[kStringLength])
{	
   char   inStr[kStringLength];
   char   searchStr[kStringLength];
   int    i;
   for (i=0;i<=strlen(inputText);i++)
      {
         inStr[i]=inputText[i];
         if (((int)inStr[i]>=65) && ((int)inStr[i]<=90))
            inStr[i]=(char)((int)inStr[i]+32);
      }
   for (i=0;i<=strlen(searchFor);i++)
      {
         searchStr[i]=searchFor[i];
         if (((int)searchStr[i]>=65) &&
             ((int)searchStr[i]<=90))
            searchStr[i]=(char)((int)searchStr[i]+32);
      }
   if (strstr(inStr,searchStr)!=NULL)
      return (true);
	
   return (false);
}

Example 8-15 shows the actual code in the game engine that is invoked whenever the “Ask” function is called from the game designer’s script. This function takes two parameters: inputText, which is the line of text the player entered, and searchFor, which is the keyword we want to search for. The first thing we do in this function is to convert both strings to all lowercase. Like many programming languages, C and C++ are case-sensitive. A string containing the text “Name” is not equal to a string containing the text “name.” We can’t rely on the player always using capitalization consistently or properly. The simplest solution is to convert all strings to lowercase. That way, it doesn’t matter if the player enters all uppercase, all lowercase, or some combination.

Once we have two lowercase strings, we call the C function strstr to compare the text strings. The strstr function searches inStr for the first occurrence of searchStr. If searchStr is not found in inStr, a null pointer is returned.

Scripting Events

Now let’s examine some of the other ways scripting can make gameplay more immersive. The previous sections showed how scripts alter the behavior of AI characters. Scripting behavior goes a long way toward making games and AI characters seem more real. However, you can use scripting to make games more entertaining and realistic in other ways as well. In this section we examine how scripts trigger in-game events that might not be related directly to AI characters. For example, perhaps stepping on a particular location will trigger a trap. Example 8-16 shows how this might look in a text-based scripting language.

Example 8-16. Trap event script
If (PlayerLocation(120,76))
   Trigger(kExposionTrap);
If (PlayerLocation(56,16))
   Trigger(kPoisonTrap);

As Example 8-16 shows, the scripting system can compare the player position to some predetermined value and then trigger a trap if they are equal. Of course, you can make this much more sophisticated by making the triggering mechanism more complex. Perhaps the trap is triggered only if the player is holding a certain item or wearing a particular piece of armor.

Scripting also can be an effective way to add a sense of ambience to gameplay. For example, you can link certain situations or objects to particular sound effects. If the player walks on a dock, a seagull sound effect might be triggered. You could use an entire scripting file solely for linking sound effects to different situations.

Figure 8-4 shows the player standing in a doorway. This would be an excellent situation to link to a creaking-door sound effect.

Door Sound script
Figure 8-4. Door Sound script

Example 8-17 shows how the player location or game situation, such as the game time, can trigger relevant sound effects.

Example 8-17. Triggered sound script
If (PlayerLocation(kDoorway))
  PlaySound(kCreakingDoorSnd);
If (PlayerLocation(kDock))
  PlaySound (kSeagullSnd);
If (PlayerLocation(kBoat))
  PlaySound (kWavesSnd);
If (GameTime==kNight)
  PlaySound (kCricketsSnd);
If (GameTime==kDay)
  PlaySound (kBirdsSnd);

Although this chapter differentiated between the types of AI scripting, in a real game it can be beneficial to use them together. For example, instead of a player action triggering an effect such as a sound effect, perhaps it triggers a specific creature AI patrolling pattern. We also showed examples of how AI creatures can respond to text entered by the player; however, this also can be a very useful way to trigger in-game events. For example, the player could recite a spell that triggers some event.

Further Information

In this chapter we showed you how to implement basic scripting that enables you to alter game AI outside the main game program. Such scripting can be very effective. Indeed, we successfully implemented these techniques in an MMORG to enable game masters to change the game AI and other game parameters in real time. Implementing a full-fledged scripting engine can be very challenging, and it involves additional concepts that we have not yet covered. These concepts include finite state machines and rule-based systems, which we’ll get to in Chapters 9 and 11 of this book.

If you decide to pursue scripting even further than we do in this book, you might find the following resources to be particularly helpful:

  • AI Application Programming by M.Tim Jones (Charles River Media)

  • AI Game Programming Wisdom by Steve Rabin, ed. (Charles River Media)

In the first reference, author Tim Jones shows how to implement a scripted rule-based system from scratch. His approach combines concepts we covered in this chapter and those we will cover in Chapter 11. The second reference includes seven articles written by game programming veterans focusing specifically on issues related to implementing scripting engines for games.

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

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