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."
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:
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.
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.
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.
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.
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
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;
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:
Putting that stuff in 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());
Game.update() is useful for the scripts.
I'll want to write something like
So as long as I make sure everything happens in that function, all will be well.while(!script_finished()) {
do_script_line();
Game.update();
script_next_line();
}
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.
In terms of hooks, there's just one. When the player hits the CTRL key,
this loop runs
// 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
So, if the player is 'near' object obj, call
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();
}
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.
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!
| Command | Action | Example |
|---|---|---|
| ' | Prints something | 'Guard:Who goes there? |
| M | Moves the player | M+10 |
| P | Pauses some cycles | P20 |
| # | 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
|
void quest(void) { |
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
To load a script, I just read it in one line at a time, and store it in my
class Script {
public:
Script();
void load(char* filename);
void run();
private:
vector<string> lines;
};
vector of strings. To run it, the code looks like
That
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...
}
}
}
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
That's pretty easy. For the objects, I gave them a
Script startup;
startup.load("start.sc");
startup.run();
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.
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!
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.
| Command | Example | Description | How-to |
|---|---|---|---|
| A | Agandalf | Activate - select object for future M commands | Have a GameObj* current_obj;
set it using Game.objects["obj-name"] |
| Command | Example | Description | How-to |
|---|---|---|---|
| ! | !%1 | set variable to current value | Have a member Script::vars[100] to keep them in |
| @ | @99 | set current value to variable or number | If it starts '%' it's a variable, or a constant number otherwise |
| + | +1 | add variable or number to current value. | Do the same for -, *, / |
| = | =0 | set current value to 1 if current value equals value or variable, 0 otherwise | Do the same for < > |
| J | J-2 | Jump forward or backward | Use the property of iterators,
e.g. i+=param where i is a vector<string>::iterator |
| B | B10 | Branch forward or backward only if current value is non-zero | As J command, but test current_value first |
# 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...
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
obj.prop is equivalent to obj["prop"]
obj.prop(...) is a function call (obj["prop"])(...)
obj:prop(...) is a method call (obj["prop"])(obj, ...)
| Lua | C++ |
|---|---|
|
|
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
there is now
// 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
This is because Lua can only call functions with this prototype:
// API functions
static int say(lua_State*);
static int pause(lua_State*);
static int move(lua_State*);
// end of API functions
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.
step3 loads scripts player.lua, gandalf.lua, and clock.lua. These scripts run
when
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;
}
doActivate is called
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
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());
}
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")
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
If you type
clock={
name="Clock",
say=say_method,
move=move_method,
x=get_x_obj,
activate=function(me) me:say(asctime()) end
}
return clock
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:
clock:say "xx"
say_method(clock, "xx"), a C++ functionclock.id and store in obj_name
Game.objects[obj_name]
and store in obj
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.
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.
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
Another advantage is that you can put
messages like this into a queue, if needed.
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);
}
}
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;
| C | Lua |
|---|---|
|
|
| Scheme | Forth |
|
|
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.