Advanced AI ScriptingAt this point, you should be quite the AI expert and things should be starting to gel. I've waited to talk about advanced AI scripting until now so you can see what I'm getting at with more of a foundation. You already saw that a scripted instruction language can be used for AI when you learned about using a simple [OPCODE OPERAND] language and a virtual interpreter earlier in the chapter. This is a form of scripting, of course. Then I showed you yet another way to create decision trees with a scripted language based on logical productions and a set of inputs, operators, and action functions. This technology can be taken to any limits you want. QUAKE C is a good example, as well as UNREAL Script. Both of these actually allow you to program game code with a high-level English-like language that is processed by the engine. Designing the Scripting LanguageThe design of the scripting language is based on the functionality that you want to give it. Here are some questions that you might ask yourself:
These are the kinds of questions that you should think about before you start designing the language. Once you've answered these and any other questions, it's time to implement the language and really design the entire game. This is a very important phase. If your game is going to be completely controlled by a scripting language, you'd better make it really open-ended, robust, extensible, and powerful. For example, the language should be able to model an airplane that flies by every now and then, as well as a monster that attacks you! In any case, remember that the idea of a scripting language is to create a high-level interface to the engine so that low-level C/C++ code doesn't need to be programmed to control the objects in the game. Instead, an interpreted or compiled pseudo-English language can be used to describe actions in the game. This is shown in Figure 12.28. Figure 12.28. The relationship between the engine and scripting language.For example, here's a script for an imaginary scripting language to control a street light: OBJECT: "Street Light", SL VARIABLES: TIMER green_timer; // used to track time // called when object of this type is created ONCREATE() BEGIN // set animation to green SL.ANIM = "GREEN" END // this is the main script BEGINMAIN // is anything near the streetlight IF EVENT("near","Street Light") AND TIMER_STATE(green_timer) EQUAL OFF THEN BEGIN SL.ANIM = "RED" START_TIMER(green_timer,10) ENDIF // has the timer expired yet IF EVENT(green_timer,10) THEN BEGIN SL.ANIM = "GREEN" ENDIF ENDMAIN I just threw this together right now, so it may have some holes in it, but the point is that it's very high-level. There are a million little details about setting animations, checking proximity, and so forth, but with this language anyone can make a traffic light program. The code starts up and sets the light to green with ONCREATE(), and then it tests if anything is close by with the EVENT() test and turns the light red. After the light has been red for awhile, it turns back. The language looks a little like C, BASIC, and Pascal all mixed together—yup! This is the kind of language you need to design and implement to control a game—something that is neutral and knows how to operate on any object. For example, when you say BLOWUP("whatever"), the language processor better know how to make that work for any object in the game. Even though the call to blow up a blue monster might be TermBMs3(), and the call to blow up a wall might be PolyFractWallOneSide(), you just want to say BLOWUP("BLUE") and BLOWUP("wall"). Get the point? You're probably wondering how to implement one of these scripting languages. It's not easy. You have to decide whether you want an interpreted or compiled language—is the language going to compile into straight code, be interpreted by an interpreter in the game engine, or something in between? Then you have to write the language, a parser, a code generator, and a P-code interpreter, or make the code generator create straight PentiumX machine code or maybe translate to C/C++. These are all compiler design issues and you're on your own here, but you've already written a couple of baby interpreters. Some tools to help you are LEX and YACC, which stand for Lexical Analyzer and Yet Another Compiler Compiler. These are language parsing and definition tools to help you implement the recursive decent parser and complex state machines needed for a compiler or interpreter. I have a trick that you can use to get started without needing to write a full-blown language compiler/interpreter. Hold onto your hat! Using the C/C++ CompilerThe nice thing about using an interpreted language is that the engine can read it and the game doesn't need to be recompiled. If you don't mind your game designers compiling (they should know how to anyway), you can use an old trick to make a crude game-scripting language: Use the C/C++ preprocessor to translate your scripting language for you. It takes nothing more than header files and the C/C++ source, which have nothing to do with compiler design. The C/C++ preprocessor is really an amazing tool. It enables you to perform symbolic referencing, substitutions, comparisons, math, and a lot more. If you don't mind using C/C++ as the root language and compiling your scripts, you might be able to write your scripting language by means of a clever design, a lot of text substitutions, a lot of canned functions, identifiers to refer to objects, and a good object-oriented design. Of course, under it is going to be real C/C++, but you don't have to tell your game designers that (if you have any). Or you can force them to use only the pseudo-language and not use all the real C/C++ functionality. The best way to show you this is with a very simple example (that's all I have time for). First, the scripting language will be compiled and each script will be run whenever the object it refers to is created. The script will be terminated when the object it refers to dies. The scripting language you're going to write is based on C, so I'm not going to go over everything there. But I am going to use text substitutions for a lot of new keywords and data types. A script consists of these parts:
As for variable assignment and comparison, only the following operators will be valid:
Comparisons—Greater than, less than, greater than or equal to, and less than or equal to all use the same C standard, which follows: (expression > expression) (expression < expression) (expression >= expression) (expression <= expression) Conditionals—The form of conditional statements is the same as C, except that the code that executes when the statement is TRUE must be contained within a BEGIN ENDIF block. Look at the following example: if (a EQUALS b) BEGIN // code ENDIF else BEGIN // code ENDELSE Similarly, the else block must be contained within a BEGIN ENDELSE block as well as the elseif. There are no switch statements in this language, and there's only one kind of loop in the language, called a WHILE loop: WHILE(condition) BEGIN // code ENDWHILE Next, there's a GOTO keyword that jumps from one point in the code to another. The jump must be labeled with a name of the form: LBL_NAME: where NAME can be any character string up to 16 characters in length. See the following example: LBL_DEAD: if (a EQUALS b) BEGIN GOTO LBL_DEAD; ENDIF You probably get the point by now. Of course, you'd want to add dozens or even hundreds of high-level helper functions that could perform tests on objects. For example, for objects that have a health or life state, you could have a function called HEALTH(): if (HEALTH("alien1") > 50) BEGIN // code ENDIF Moreover, you might create events that could be tested with a text string parameter: if (EVENT("player dead") BEGIN // code here ENDIF All this magic is accomplished by using clever global state variables and making sure to expose enough generic events to the scripts, along with system state variables (via functions) and a lot of text substitution via the preprocessor. Leaving out some of those details to keep things simple, let's take a look at what you need for text substitutions thus far. Referring to Figure 12.29, each script that is compiled will be processed first through the C/C++ preprocessor. Figure 12.29. Using the C/C++ preprocessor for a script language interpreter.This is where you're going to make all those text substitutions and convert your little script language back to C/C++. To make it work, you tell the scripter to save all the files with the extension script .SCR or something, and when the file is imported into your main C/C++ file for compilation, you make sure to first include the script translation header. Here's the script translator for what you've so far: SCRIPTTRANS.H // variable translations #define REAL static float #define INTEGER static int // comparisons #define EQUALS == #define NOTEQUAL != // block starts and ends #define BEGIN { #define END } #define BEGINMAIN { #define ENDIF } #define ENDWHILE } #define ENDELSE } #define ENDMAIN } // looping #define GOTO goto Then you include the following in your game code: #include "SCRIPTTRANS.H" Then you include the actual script file somewhere in your game engine at the proper moment. You can do it at the beginning, or even in a function: Main_Game_Loop() { #include "script1.scr" // more code } // end main game loop This part is up to you. The point is that the code the scripter writes must be compiled somehow, and it must be able to access the globals, see events, and make calls to the function set you expose. For example, here's a crude script that fires an event (which I didn't define at the count of 10): // the variables INTEGER index; index = 0; // the main section BEGINMAIN LBL_START: if (index EQUALS 10) BEGIN BLOWUP("self"); ENDIF if (index < 10) BEGIN index = index + 1; GOTO LBL_START; ENDIF ENDMAIN Obviously, you would have to define BLOWUP(), but you get the picture. This code would be translated by the preprocessor into the following: { static int index; index = 0; LBL_START: if (index == 10) { BLOWUP("self"); } if (index < 10) { index = index + 1; goto LBL_START; } } Cool, huh? Of course, I'm leaving out a lot of details, like problems with variable names colliding, accessing globals, debugging, making sure the script doesn't sit in an endless loop, and so forth. However, I think that you have an idea about using the compiler as a building block of a scripting language. TIP You can tell the Visual C++ compiler to output the preprocessed C/C++ file with the compiler directive /P. |