Writing modifiable games

0. Introduction

If you look at the types of program that exist, you could roughly divide them into two categories : there's code that just runs totally ignorant of the world outside, computing something and returning the result, and there's code that takes a certain input, transforms it somehow, and outputs the end result. Now, ofcourse, just about every program takes some input, but we're making a distinction here between a program that's fed a huge file to be warped (second category), and a simple program which lets the user select between 4 languages and prints an appropriately translated standard text (say a tourist info box) which is first category.

You'll probably agree here that the first category is quite rare, because these programs are very limited in what they can do. Even a simple tourist info box these days could get it's flashy texts and pictures from a central server, so that they can easily be updated for the entire city (say). A program becomes much more powerful, and hence widespread, when the computation it performs is somehow generalized, and the specifics of one particular computation required are coded into some controlling data file. Then, by simply plugging in a new data file, a variant of the original computation, or even a completely new computation, can be executed, resulting in vastly different output for the same original input.

Now, if externally controlled programs are so effective, how come most of the games are written as if they're a First Category project ? Indeed, a lot of games are written for one particular playstyle, with fixed goals, and a fixed path to victory (with or without multiple ways to achieve those goals). This article presents an overview of the techniques a game programmer can use to make the final game easy to modify and extend, with possible tweaks ranging from the basic "make a weapon stronger" to advanced "change a first person shooter in a race game"; in effect, this would move the game to the second category, where the input is an abstract description of how the game should play, and the output is that game, with a new world, new rules, and new goals.

(c) 1999 by Bert Peers / Last Update : 04/06/1999 / bpeers@acm.org - Mail me your comments and ideas !

1. Data driven

Fortunately, these days the situation is not as dramatic anymore as described in the introduction. There are still games that are indeed strict-gameplay only (Myst, some adventure games), because the multimedia is tightly connected with the story. The majority of games however have at the very least become a data driven game. In a strategy game such as Starcraft or Civilization, it's not a good idea to bury the details of a unit into the code implementing the game. Parameters such as movement speed, defensive strength, offensive power and so on are better written down in a data file, and read when the level loads. This allows the designers to fix badly balanced gameplay without the need to change the code -- which would be cumbersome, slow, requires recompiling, and could in the worst case introduce new bugs (for instance, a parameter occurs multiple times but when changing it one or more occurences are accidentally skipped).

A big advantage of this technique is that's easy both to implement and to use. A simple ASCII text file is easy to read by PCs and humans alike, and if you go through the trouble of making some elaborate syntax instead of terse tables full of figures, the data should be easy to modify.

The ease of change however has a price, and that is limited power. You can take a combat unit and change it into an extremely powerful weapon, which moves incredibly slow, and suddenly you have a whole new way to play. But, it's still a strategy game, and not a football or chess game. For this, a data file in it's basic form is too limited.

2. Logic driven / Goal Based

An extension to simple data files would be to somehow squeeze the condition for "winning" the game into the data file. In a game that's to be played with two players, you could say : "The game is won by a player when the other player is killed". This could be written in an ASCII text file as

if player2.IsDead then player1.Win ()
if player1.IsDead then player2.Win ()

These are called rules or goals : the "Win" goal is marked as "achieved" for a player, when the "IsDead" goal/condition has been achieved/reached on the other player. In general, a rule makes the flag at the far right true when all the flags at the left have become true. Or, if you read it as goal oriented, the goal at the far right is marked as accomplished, when all the goals at the left are accomplished.

What the code should do is define a certain number of basic goals or conditions that will be marked by the engine. This varies from project to project, but a simple example is the "IsDead" flag : when a player effectively dies, the "IsDead" "goal" has been achieved. The game goes in an infinite loop, marking these basic conditions that are met as set, and then for every rule checks if all the goals on the left are achieved; if so, the engine sets the goal on the right as "achieved" as well, and the loop continues. So, if the game checks for a certain number of high-level goals to be achieved, such as a "Win" goal for a player, or a general "GameOver" goal, the rules can control how and when somebody "Win"s, or when the "Game" is "Over", by writing down how the lowest level achievements combine into victory or defeat.

Here's a simple example :

if player1.Win then GameOver
if player2.Win then GameOver
if player1.IsDead and player2.IsDead then GameOver
if player1.IsDead and not player2.IsDead then player2.Win
if player2.IsDead and not player1.IsDead then player1.Win

This is a simple script for a simple deathmatch. If one player manages to kill the other one, he wins. If a player wins, it's gameover (this would be the top most flag checked by the infinite loop as described previously). If they happen to kill each other in a mutual kill, then it's gameover too, although nobody wins.

Basically, the entire game is driven by one gigantic If-Then control statement. Because it's written in a run-time parsed text file, rather than simply written in C, it can be changed. For instance, without changing the C code, your 1:1 deadly shooter could become a socially acceptable game where the game can only be won when people play cooperatively, instead of kill each other :

if player1.IsDead then GameOver
if player2.IsDead then GameOver
if player1.Win then GameOver
if player2.Win then GameOver
if player1.AchievesGoal and player2.AchievesGoal then player1.Win and player2.Win

If somebody dies, it's simply GameOver, and nobody wins. Only by staying alive and both achieving some goal (the AchievesGoal flag is set when a player does something, such as jump on a difficult platform in a 3D game, and would ideally be controlled by more rules) can a player win.

This is a powerful technique. A deathmatch game can be changed in a cooperative game, an RPG or an Adventure could get a completely new story, and so on. Especially RPGs and Adventures are naturally expressed with "goals"; a usual story goes along the lines of "To win, the player should kill the Evil Wizard. To kill the Wizard, he must be nearby the Wizard, and use the Rune Spell. To use the Rune Spell, the user must posses the Book Of Knowledge and have the Ring Of Power", etc.

Basically, what's described here is a small scale variant of a well known "rule based" language, called Prolog. It's been proven that Prolog is functionally equivalent to procedural languages, which in English means that anything you could write in C, could also be written in Prolog. The disadvantage here is that it may not be obvious how. Even a simple "you win the game if you do this or that exactly 10 times" is easy to write in C, but looks weird in Prolog. Programming advanced game modifications in this logic based syntax requires a new style of thinking, which immediately brings us to the number one disadvantage of this technique : to use it to it's fullest power, your advanced players who're into mod making will have to be very advanced. This is demonstrated by the low modification rate of Abuse, a Linux 2D shootemup; it's an excellent game with a full blown LISP engine behind it, but the number of modifications for it (such as a conversion into a Pong/Arkanoid type of game) is very low, and they're mostly generated by the original developers.

3. Scripting

Prolog/LISP based if/then constructs are totally unfamiliar to a lot of programmers when used for more than just writing down a couple of goals. Procedural languages, such as C, are much more widespread. So it seems to make sense to cook up some sort of procedural language by yourself, write a parser and interpreter for it, and have this script control your game whatever way it wants, reading values, copying stuff around, and calling whatever functions necessary.

In other words, instead of writing the game logic in C, you write it in some language which you invent yourself (typically a heavily simplified C or Pascal variant) and make sure it can be executed by your game by building the scripting environment for it. This is much like you'd decide to write your own csh or bash : the operating system calls become calls into your game world (accessing players, the scoreboard, the joystick).

The big advantage here is total power for the user. When implemented to the fullest, these scripting environments can give the end user as much power as if he had access to the original C source code. The entire game can be turned upside down : a ground unit can start flying and vice versa; a weapon may be changed, removed, or a new one may appear; and so on.. Any particular function you have written which may be of interest for anybody willing to modify the gameplay, may be given access to from within the script by fixing the interpreter code. The same goes for any variables, such as score, health, power, etc.

The downside here ofcourse is that this is a lot of work. The effort may be reduced by using off the shelf tools as much as possible. The parser for your language can be generated using tools such as yacc; there also exist ready made interpreters for C and Pascal which you can try to incorporate into your game code. This may however not always be possible because these general tools are usually not written with the world of high speed gaming in mind. If an interpreter rules the universe, but reduces your game to 10 frames per second, it may not be worth it. Even if you write your own custom cut-for-speed interpreter, you still have to watch out, as interpreted languages are notoriously slow no matter what. Another disadvantage is that you'll rarely end up with pure C or Pascal. As a result, people who know how to program will still need to learn your language first.

An example of this approach is Quake 1's QuakeC language, a C variant used to control how the game reacts (powerups, weapons, etc).

4. Virtual Machine

If you really want the full power of C or C++ without being slowed down by an interpreter, one hightech possibility is using a virtual machine. This means that the game's C code is not compiled and linked when the game is built, but rather is shipped with the game in it's original, uncompiled C form, and then the game finds that C file, compiles it into some bytecode, and runs it through it's own virtual machine.

Obviously, we're talking about a serious project here. A compiler, a bytecode and a virtual machine is needed. The compiler should preferably be able to handle full C or C++, and the virtual machine should be lightning fast, not too heavy on memory, and allow full access to the relevant game variables and functions. This task is near impossible in a reasonable amount of time without using some off-the-shelf tools. An example game using this technology is Unreal (which was under development for 3 years), who's AI and game world is entirely controlled from a VM-run enhanced-C++ language. The same approach is used in Quake3:Arena, where an lcc-spinoff will interpret standard C, presumably using a VM for efficiency; this C code will control everything from weapon and game settings to visual "splash" effects. In practice, even a VM is not powerful enough to handle really crucial, heavy tasks (such as pathfinding for AI), meaning that these are delegated to native code after all, but the actual call, and the actual arguments, are set up using the VM (example : Unreal).

Note that it is actually possible to reuse the Java Virtual Machine for your application. A Java program can be "extended" (using RMI) with "new" functions, which are really just callbacks to functions implemented by your gaming engine (the non-game logic part of it, such as sound, rendering etc). Furthermore, a virtual machine can be loaded as part of your game (this is called Embedded Java). This means that you could write the game logic in Java, compile it with a standard Java compiler (so you don't have to write one), and run the bytecode through a standard Java VM (again reusing existing tools). An interesting example of this is the Q2Java project, which is implementing exactly this scheme for Quake2, proving that the idea is much more than just a theoretical option.

5. DLLs

The nice thing about a virtual machine is that your game is the only "tool" needed to change the gameplay. If somebody wants to make a modification he'll need to know C or C++, but can go hack directly into the game files. However, the price to pay for this ease is very high : you're effectively reimplementing what already exists; there are plenty of C/C++ compilers, and there is a "virtual" machine to run the compiled code on, namely the bare metal CPU. So, a cheaper alternative is to compile the code without any change (using a standard compiler, compiling directly to the CPU instruction set), but putting the gamelogic code in a separate module or library (a .dll under Windows, a lib under Linux). By giving out the code needed to recompile this library, anybody who has a compiler and the technical knowledge to recompile, can do so. These requirements are not trivial to meet, but this problem is less severe when most of the gamers are already coders anyway (typical with the current Linux situation, not so with Windows).

With the technical hurdles overcome, a lot of power is available : every single notch and bolt can be modified; the modification runs at full CPU speed; and the actual implementation of the modification is hidden for the end user (useful for commercial modifications such as "mission packs"). But, again, there are two problems. One is that the modification becomes platform dependent. Especially for games that are ported to several systems (such as Quake2, which uses this DLL technique) it may be annoying for players when a particular popular modification is not available for their system. A second disadvantage is the power the modification has to the CPU. All the previous approaches to scripting made sure any modification could only occur in a safe playground; here however the library runs directly under the OS, and any malicious attacks or accidental bugs in the code may crash your game, the OS or the entire machine.

6. Conclusion

If you have some programming experience, you probably already have the intuition to use at least a dat file anyway; it's so easy to implement and the benefits are so great that most people have already discovered this technique by themselves. However, in this article I tried showing increasingly powerful, but also increasingly complex techniques, which may or may not have occured to you yet. For your next gaming project, take a careful look at the possibilities, and choose the Right Thing for your game, balancing between the time you have and the tweakability you'd like. You never know what totally unexpected variations of your game your players might create one day !