Scripting and Allegro

By Peter Hull ([email protected])

21st March 2002

Abstract

This is a discussion of scripting, using C++ and Allegro. I am going to describe a basic game which I want to add scripting to, then describe two methods. First, a simple home-made script, and second, a general-purpose scripting language. Finally there will be a discussion of some issues, and how I would go about tackling them. You should understand enough C++ and Allegro to be able to follow the base game. I use some STL, so go to Pixelate and read the STL tutorials. For the final part, you need to download and install a working copy of the Lua language.
Thanks to various members of Allegro.cc for help on this; in particular, Chris Barry (23yrold3yrold).

#include <std_disclaimer.h>
"I do not accept responsibility for any effects, adverse or otherwise, that this code may have on you, your computer, your sanity, your dog, and anything else that you can think of. Use it at your own risk."

Contents

Introduction

This article discusses some issues relating to scripting for Allegro games. Scripting is quite a complex topic, so I am assuming that you have a good grasp of Allegro and C++, and you are already able to write a game without scripting. It's impossible to give step-by-step instructions that will be exactly what you need in all cases, so I am just going to cover as many of the issues as I can.

You might want a scripting language because:

The down-side of scripting is that you have to learn (and/or invent) another language, which may be slower than 'native' code. Existing languages are well-supported by tools, such as your IDE, which will accuarately detect language errors and do syntax colouring. These will not work with your scripts, so you might have to write your own tools. Finally, if your language grows in complexity to support variables, loops, and so on, it will become quite hard to maintain. I don't want to put you off, but, like many things, it's a balance. I don't think you ever need scripting, but, if done right, it can make your program simpler and more elegant.

I will start with some background and terminology. These are the definitions I will use in this article. A scripting language is a specialised language which runs inside, and controls another program. This is the host program; it's your game, in this case. Now you have two programs, the host and the script, and they must communicate. When you write the host program, you will build in places where scripts are called. These are called hooks. For example, when the player approaches an NPC and presses the 'action' key, you could start a script which represents a conversation. When the script wants to do something, it calls back to the host. This is the host API. For example, the host could expose a function that draws a message box. This is summarised in the diagram below.

Architecture

What is a programming language?

Comp.Sci students are, by now, jumping up and down. I don't care so much what the proper definition is. If you think of it as being something like C, then writing a scripting language seems scary. But here's what I say: If you imagine writing a program to display this page, it might look like


int main(void) {
 clear_page();
 begin_style(STYLE_H1);
 draw_text("Scripting and Allegro");
 end_style(STYLE_H1);
 new_line();
....

and so on. This would be horrible to write; C just isn't good at that sort of thing. The way the page actually is written is in HTML (remember, the L stands for Language), like this
<html>
<body>
<H1>Scripting and Allegro</H1>
...
So you can think of <H1> as a command that selects a style. Now think (think hard!) of S as a command to draw a letter S, and so on. In other words, almost anything can be a scripting language. Easy!

In summary, a scripting language is a specialised language for a particular task, and we need to write hooks and APIs so that control can pass from one to the other. Next, I will describe the host program that I'm going to add scripting to.

The host program: step1.cc

For the host program, I'm going to keep it simple, as shown in the diagram below. There's going to be a player (the ugly guy on the right), a Wizard and a clock (not shown). Play will take place in an office and there's a status area at the bottom for messages. The player can go left and right, and activate things by pressing the left control key. To finish, press ESC, and to take a snapshot of the screen, press f1. In later stages, you can open up a console by pressing TAB and enter script commands directly. Finish entering commands by pressing RETURN on a blank line.

Screen shot

I don't want to spend time going through the code at length, but basically the classes Player, Wizard and Clock derive from a common base GameObj. To handle input there's an Input class, which could be overridden for joystick and user-defined key input, and a Flipper class, which gives double buffering, and could be overridden to use page flipping. I don't do that here, so the code is slightly more complex than needs be. Finally, there's a class Texter which has a very basic stream interface (i.e. you can write txt<<"a string";), to display messages to the user. The code that doesn't change from example to example is in the file 'common.cc' with a header 'common.h'. All the parts of the game that are 'global' are in the class _state, which has one instance, Game.

struct _state {
  Texter* ptxt;
  Input* pinput;
  Flipper* pflipper;
  Map* pmap;
  map<string, GameObj*> objects;
  vector<GameObj*> active;
  bool sorted;
  void add(const char* name, GameObj* o) {
    active.push_back(o);
    objects[name]=o;
    sorted=false;
  }
  void update();
} Game;
Note that I maintain two lists of objects in the game. One, objects has all the objects and is indexed by name. This is so the scripts can grab a named object. The second, active, just holds the ones that I want to draw. This is sorted by GameObj::priority(), so that the player is drawn over the top of the npcs and background objects. In fact, in this case the two lists are the same, because all (all three!) objects are active.

All the redrawing and timing stuff is held in the function Game::update(). Therefore, the main loop just looks like:

  do {
    kbd.clear();
// update logic etc.
    Game.update();
// handle keyboard input
    if (kbd.left()) {
...
    }
// do activation bit
    if (kbd.fire()) {
...
    }
// run until ESC is pressed
  } while(!kbd.esc());
Putting that stuff in Game.update() is useful for the scripts. I'll want to write something like
while(!script_finished()) {
 do_script_line();
 Game.update();
 script_next_line();
}
So as long as I make sure everything happens in that function, all will be well.

To make it absolutely clear, I have put all the API functions as functions of the Game object. This doesn't serve any real purpose in C++ terms, but it just makes it clear which functions I am exposing.

  // API functions
  void say(const char* msg); // display a message
  void move(int steps); // move the player
  void pause(int cycles); // wait a while
  // end of API functions
In terms of hooks, there's just one. When the player hits the CTRL key, this loop runs
 bool didHit=false;
 for (vector<GameObj*>::iterator i=Game.active.begin(); 
      i<Game.active.end(); 
      ++i) {
   GameObj* o=(*i);
   if (o!=me && o->isNear(*me)) {
     didHit=true;
     o->doActivate();
   }
 }
 if (!didHit) {
   me->doActivate();
 }
So, if the player is 'near' object obj, call obj.doActivate(), otherwise call player.doActivate(). This in turn runs the one and only script associated with that object. In this step, there is no scripting, so the program calls a block of C++ code instead. This just prints a helpful message at the moment.

Running the programs

You should have unpacked the archive and found a DAT-file, some .cc files and some .h files. The supplied makefile should autodetect MinGW, DJGPP and Linux. For MSVC people, you might need to study the makefile and build your own project. In step one, you need to compile and link step1.cc and common.cc. The target is step1.exe or step1 for Linux. Compile and run the program. What fun!

A simple script: step2.cc

Based on what I said above about the definition of a language, I will introduce a simple scripting language. The rule is that the first character of every line is a command, and the rest of the line is the parameter. There are just four commands
CommandActionExample
'Prints something'Guard:Who goes there?
MMoves the playerM+10
PPauses some cyclesP20
#A comment# Script for meeting the wizard

So, an example script, and C++ equivalent might look like this. Admittedly, the C++ could be simplified by defining functions closer to the M and P script commands.
# Quest script
'Wizard:How can I help?
'Peter:What is my quest?
'Wizard:Go over there and come back
M-50
P10
M+1
P10
M-1
'Peter:Well, that was easy
M+50
'Peter: I did it!
void quest(void) {
Texter& txt=GetText(); txt<<"Wizard:How can I help?"; txt.draw();
txt<<"Peter:What is my quest?"; txt.draw();
txt<<"Wizard:Go over there and come back"; txt.draw();
for (int i=0; i<50; ++i) {
me.move(-1);
update(); }
for (int i=0; i<10; ++i) {
update();
}
me.move(1);
update(); for (int i=0; i<10; ++i) {
update();
}
me.move(-1);
update(); txt<<"Peter:Well, that was easy"; txt.draw();
for (int i=0; i<50; ++i) {
me.move(1);
update(); }
txt<<"Peter: I did it!"; txt.update();
}

The code for this section is in step2.cc. Most of it is carried over from the previous step; the main changes were to insert a call to the scripts, and a little bit of support code to load a script from a file. For each script, there is a Script object. The definition of it looks like this

class Script {
public:
  Script();
  void load(char* filename);
  void run();
private:
  vector<string> lines;
};
To load a script, I just read it in one line at a time, and store it in my vector of strings. To run it, the code looks like
void Script::run() {
  for (vector<string>::iterator i=lines.begin(); i!=lines.end(); ++i) {
    string& s=*i;
    switch(s[0]) {
      case 'P': 
 	int cycles=atoi(s.substr(1).c_str());
	while(cycles>0) {
	  Game.update();
	  --cycles;
        }
        break;
// and so on...
    }
  }
}
That atoi(s.substr(1).cstr()) just gets the number following the P command. Notice how the loop calls Game::update(). This means all the timing and background events (if there were any) occur as normal. If you hear people boasting about scripting engines and virtual machines, this is one, right here. A simple one, but that's enough to get started!

Now I need to load and trigger the scripts. There's one that runs right at the start, which goes like this

  Script startup;
  startup.load("start.sc");
  startup.run();
That's pretty easy. For the objects, I gave them a Script member function, and load it up in the constructor
class Clock: public GameObj {
public:
  Clock(Map&m);
  void draw(BITMAP*);
  void doActivate(GameObj&);
private:
  Script tick;
  BITMAP* img;
};
...
Clock::Clock(Map& m) : GameObj(m) {
  img=(BITMAP*) dat[DAT_CLOCK_BMP].dat;
  x=128;
  tick.load("clock.sc");
}   

Because the loader uses the Allegro packfile functions, in a real game, you could put the scripts into a datafile (using individual compression) and load them using the "file.dat#name" syntax. This avoids having all your scripts lying around as plain text files.

Running the programs

In step two, MSVC users need to compile and link step2.cc and common.cc. The target is step2.exe. For Linux, just make step2. Compile and run the program. What fun!

Next steps

This scripting language is OK for the kind of cut-scene scripts that are triggered by an event, but it has severe limitations. You can only move the player, not the wizard, and the script always does exactly the same thing each time. To progress, you can add extra commands. I am not going to implement these, but I will suggest how to.
CommandExampleDescriptionHow-to
AAgandalfActivate - select object for future M commands Have a GameObj* current_obj; set it using Game.objects["obj-name"]
To introduce some variety, implement script variables, which are named %1, %2, .. %99. You also need a 'current value'.
CommandExampleDescriptionHow-to
!!%1set variable to current value Have a member Script::vars[100] to keep them in
@@99set current value to variable or number If it starts '%' it's a variable, or a constant number otherwise
++1add variable or number to current value. Do the same for -, *, /
==0set current value to 1 if current value equals value or variable, 0 otherwise Do the same for < >
JJ-2Jump forward or backwardUse the property of iterators, e.g. i+=param where i is a vector<string>::iterator
BB10Branch forward or backward only if current value is non-zeroAs J command, but test current_value first
Once you've done all that you can write a script like

# is variable %7 equal to 1? jump if so
@%7
=1
B5
# otherwise, show message and set variable 7
'Voice: Cut the red wire...
@1
!%7
J1
'Voice: I've told you once!
#end of script.
roughly equivalent to C++ code
void script() {
 static int v7=0;
 if (v7!=1) {
  puts("Voice: cut the red wire");
  v7=1;
 }
 else
  puts("I've told you once!");
}
That's a simple way to implement, but the resulting script language is going to be rather hard to read and debug. It's like a machine code for scripts. And not many people write machine code these days! Maybe you should consider a more general language, which is where we're going next...

General scripting language: step3.cc

For this section, I am not going to write my own language. I will use Lua instead. Lua is a language somewhat like C, BASIC or Pascal, but specialised for scripting host programs. It's mature and has been used in several games already (see the Lua web site, which is also a good source of docs and tutorials). It's not object oriented, but it can simulate objects. If you've used objects in Visual Basic, the idea is similar. Lua has tables which are a bit like the STL map<..,..> class, so you can write address["city"]="London" for example. A Lua value has a type, number, string, function or table, but a variable itself has no type, so you can write x="hello" followed by x=99 without problem. Lua variables can hold numbers, strings, references to tables, references to user data, and functions. User data will not be covered here; it just means a void* pointer that the host program knows about but Lua does not process in any way. Using functions and tables together, you can make Lua 'objects.' There is a clever bit of syntactic sugar which says

which means that these are equivalent
LuaC++
mypoint {x=0; y=0; 
   draw=function(pt, bm) 
    putpixel(bm, pt.x, pt.y, 255) 
   end 
}
...
mypoint:draw(screen)
mypoint.x=99;
class pointclass {
 public:
  pointclass() {
   x=0.0; y=0.0;
  }
  void draw(BITMAP* bm) {
   putpixel(bm, int(x), int(y), 255); 
  }
  double x, y;
} mypoint;
...
mypoint.draw(screen);
mypoint.x=99.0;
Note that all functions are effectively virtual in Lua, and that all numbers are doubles. You can change the number type when you compile Lua, but double precision is the default.

The code for this section is in step3.lua. The script loader code has changed a little bit, for Lua, but the main difference is the change to the API declarations. Where I had

  // API functions - these are the ones we want to make
  // available to the scripts
  void say(const char* msg);
  void move(int steps);
  void pause(int cycles);
  // end of API functions
there is now
  
// API functions
  static int say(lua_State*);
  static int pause(lua_State*);
  static int move(lua_State*);
// end of API functions
This is because Lua can only call functions with this prototype: int (*)(lua_State*). The arguments and return values are passed using the stack. See the Lua docs for more information on this. Here is the code for the say function, which takes a variable number of string arguments, and returns nothing.
int _state::say(lua_State* L) {
  // print out all the arguments, separated by spaces
  // e.g. say("You got",17,"out of",20)
  // prints "You got 17 out of 20"
  int n = lua_gettop(L);  // number of arguments 
  int i;
  Texter& txt=*Game.ptxt;

  for (i=1; i<=n; i++) {
    // if not the first arg, print a space
    if (i>1) txt<<" ";
    // Get the arg in position i and 
    // try and convert to a string
    const char* s=lua_tostring(L, i);
    if (s)
      // valid conversion: print it
      txt<<s;
    else
      // invalid: show the type name in angle brackets
      txt<<"<"<<lua_typename(L, lua_type(L, i))<<">";
  }
  // finish off with a new line
  txt<<"\n";
  txt.draw();
  // return no values
  return 0;
}
step3 loads scripts player.lua, gandalf.lua, and clock.lua. These scripts run when doActivate is called
void LuaScript::run() {
  // st holds the script text
  TRACE("Running '%s'\n", chunkname.c_str());
  // just call lua_dobuffer
  lua_dobuffer(Game.lua, script, length, chunkname.c_str());
}
There is also start.lua, which replaces start.sc in step two. You can hit the TAB key to put an input line in the text area. Press return on a blank line to finish. Try typing move(20) to move the player, or say("Hello") to print something. In Lua, you can miss off the brackets from a function call if the argument is a single string. For example, say "Hi" and say("Hi") are both valid, but say "Hi","there" is not; use say("Hi", "there")

More complex scripting: step4.cc

In step3, we were limited to moving the player only. Now we take a different, more object-based approach. The scripts player2.lua, gandalf2.lua, etc. are run when those objects are constructed, and they define the functions they need, put them in a table, and return that table to C++. They can also store 'private' variables in there. As an example, here is the code from clock2.lua

clock={
	name="Clock",
	say=say_method,
	move=move_method,
	x=get_x_obj,
	activate=function(me) me:say(asctime()) end
}
return clock
If you type player:say("Hi") it calls say_method(player, "Hi"), which calls say(player.name..":".."Hi") You see Peter: Hi and if you type wizard:say("Hi") you get Gandalf: Hi. Now the hook calls the table's activate() function. For the clock, it calls another function, asctime, defined in the C++. It just uses the C library function ctime to get the local time and date.

To do this, we need a way to jump from the Lua code to a C++ object. There are several ways to do this in Lua; I will present one. When the script returns the newly-created table (clock in the case above) , the C++ code adds another property to it, id. This is a string which identifies the object. Step by step:

  1. [C++]create a new object of class Clock
  2. [C++]register it in Game.objects with name 'clock'
  3. [C++]run the script 'clock2.lua'
  4. [Lua]create a table with all the methods and variables we need
  5. [Lua]return it to C++
  6. [C++]add another property 'id' with the same name we registered in step 2
  7. [C++]bind the table to the C++ object
So a call looks like this
  1. [Lua]call clock:say "xx"
  2. [Lua]call say_method(clock, "xx"), a C++ function
  3. [C++]get clock.id and store in obj_name
  4. [C++]get Game.objects[obj_name] and store in obj
  5. [C++]call obj.say("xx")

Another thing we're missing are map scripts or triggers. These scripts run when the player enters a particular area of the map. There are three ways they could execute.

The triggers here are run like the third case above. In a real game, they would be associated with the map data that's loaded from disk. Here, there's just one, which is hardcoded to the leftmost edge of the map. If you call Map::getScript(int x) it returns a Lua command (not a function name) for position x, or NULL. In this case, it returns the string "map_script_0()" when the player is near the left hand side. The gandalf2.lua and start2.lua scripts interact on this; when the player talks to the wizard, for the first time, the variable mission is made equal to "set". Subsequently, if mission equals "done", a congratulatory message is printed. When the player triggers map_script_0(), if mission equals "set" it is changed to "done", otherwise nothing happens.

Some more issues

From here on in, there aren't really any answers, just questions. These are some of the problems that I have run up against, and some thoughts I have had. Please let me know if you've found a really good solution.

Our program assumed only one script at a time would operate. If you want something to run in the background, there are a couple of approaches. As an example, we want a guard to wander endlessly round in a square. I'll write it in C++ so it looks familiar, but this would be a script, of course. We could put


while(true) {
 for (int i=0; <10; ++i)
  move_right();
 for (int i=0; i<10; ++i)
  move_down();
 for (int i=0; i<10; ++i)
  move_left();
 for (int i=0; i<10; ++i)
  move_up();
 } 

But you can see the problem; this doesn't allow anything else to run. You could rewrite the script engine to allow it to pause (yield) and restart where it left off, like this
while(true) {
 for (int i=0; i<10; ++i) {
  move_right();
  yield();
  } 
 for (int i=0; i<10; ++i) {
  move_down();
  yield();
  } 
 for (int i=0; i<10; ++i) {
  move_left();
  yield();
  } 
 for (int i=0; i<10; ++i) {
  move_up();
  yield();
  } 
 } 
It isn't very easy to get Lua to do this. Another option is to code the routine with a state, so each call moves the guard on one step. This has the problem that flow of the code is lost - it's not clear what this does any more.

switch(direction) {
 case UP:
  if (steps==0) {
   steps=10;
   direction=RIGHT;
  } 
  else {
   move_up();
  }
  break;
 case LEFT:
  if (steps==0) {
   steps=10;
   direction=UP;
  } 
else {
move_left();
}
break;
 case RIGHT:
  if (steps==0) {
   steps=10;
   direction=DOWN;
  } 
else {
move_right();
}
break;
 case DOWN:
  if (steps==0) {
   steps=10;
   direction=LEFT;
  } 
else {
move_left();
}
break;
}
--steps;
All in all it's a tricky problem and there's no easy solution.

When I specified the API for an object, I used virtual functions. They are fast and easy to use in C++. However, in this implementation, you need to derive every object from the same base class, which means that class must implement every single virtual function used by any of it's derived classes. This adds bulk and means you constantly have to modify the base. A solution is to use a multiplex function. These are used in Windows, and also turn up in the Allegro GUI code. Basically it's a general-purpose function like int GameObj::handler(int msg, void* param1, void* param2);. In the body of the code is a switch statement for all the cases you're interested in, such as

int Player::handler(int msg, void* param1, void* param2) {
 switch(msg) {
  case MSG_GET: { // just asked to pick up an object
   GameObj& o=*(GameObj*) param1;
   ....
   }
   return OK;
   ....
   default:
    return GameObj::handler(msg, param1, param2);
 }
}
Another advantage is that you can put messages like this into a queue, if needed.
class Message {
 public:
  Message(int m, void* p1, void* p2) : msg(m), param1(p1), param2(p2) {
  }
  int apply(GameObj& o) {
   return o.handler(msg, param1, param2);
  }
 private:
  int msg;
  void* param1, * param2;
};
queue<Message> message_queue;

Other scripting languages

As an alternative to Lua, you might consider Guile or ficl. Guile is the GNU implementation of Scheme, which is in the Lisp family. Scheme is used for scripting in the Gimp, GnuCash and others. Alternatively, ficl is based on Forth. These languages are powerful but very different from C++.
CLua
int a=0;
int square(x) {
 return x*x;
  } 
a=square(6);
printf("%d", a);

a=0
function square(x) return x*x end
a=square(6)
print(a)

SchemeForth
(define a 0)
(define (square x) (* x x))
(set! a (square 6))
(display a)
0 variable a
: square dup * ;
6 square a !
a @ .

If you want to make your own language, the conventional way is to use a lexical analyser and a parser. For example, the C program would be split up by the lexical analyser as "typename: int" "identifier: a" "=" "integer: 0" ";" and the parser has lists of rules like "a valid line is a type name followed by an identifier followed by an optional assignment followed by a semicolon" and "an assignment is an equals sign followed by an expression", etc. It's all very complicated. Tools that can help you are Flex, Bison or ANTLR. For more information, consult Thomas Harte's article in Pixelate #5.

Conclusion

Definition List

hooks
Places where the host program calls a script
host API
Functions in the host program that the script can call
host program
Main program, written in C or C++
lexical analyser
A program to split an input file into tokens
parser
A program to assemble tokens into a program structure
scripting language
A specialised language that controls the host program
syntactic sugar
A language construction which makes syntax more convenient
tables
A lua table can be indexed by number or name
triggers
A command that runs when the user enters an area of the map

© Peter Hull 2002