Theatrix: A C++ Game Class Library

Theatrix is a C++ class library that encapsulates the operations of a typical game arcade.


June 01, 1995
URL:http://www.drdobbs.com/cpp/theatrix-a-c-game-class-library/184409695

Figure 1


Copyright © 1995, Dr. Dobb's Journal

Figure 1


Copyright © 1995, Dr. Dobb's Journal

SP95: Theatrix: A C++ Game Class Library

Theatrix: A C++ Game Class Library

Encapsulating arcade-game operations

Al Stevens

Al is a DDJ contributing editor and can be contacted on CompuServe at 71101,1262.


PC game programming is currently very popular among programmers largely because of the overwhelming success of games such as DOOM and Myst. Games themselves have always been part of the personal-computer phenomena. Among the first PC games to be widely used were Microsoft's Flight Simulator, a graphical simulation where you fly a Cessna 182, and Adventure, a text-mode tour through a cave of dragons, dwarfs, mazes, chasms, and other imaginative obstacles. Those games ran very well on the small PCs of their time--4.77-MHz 8088 machines with 640 Kbytes of RAM, 360-Kbyte diskette drives, and little or no hard-disk space. By comparison, today's desktop machines are supercomputers, and the best contemporary games take full advantage of the processing power and high-resolution graphics of mainstream configurations.

Theatrix is a C++ class library that encapsulates the operations of typical arcade games. The name comes from the metaphor that the library implements--games are viewed as theatrical productions, with directors, players, and scenery. You build a game by designing these components with graphics tools and by deriving from the Theatrix classes, modifying the behavior of the classes to provide the actions in the game. An event-driven programming model sends controller and timer-event messages to the game's directors and players.

The Theatrix library is the subject of a book I am writing in association with the library's author, Stan Trujillo. Stan brought the library to me for my opinion. Its level of abstraction was impressive: In a couple of days and with only about 500 lines of C++ code, I built an arcade-style game with background scenery and seven sprites that move around the screen in the fashion of an animated cartoon. Most of the work was the artistic part--designing the scenery and the sprites with a paint package.

The book project came about when we realized that a programmer can build a comprehensive set of game-creation tools from widely available freeware and shareware programs. There are paint programs, 3-D modelers, ray-tracers, image-format converters, graphics libraries, sound editors, and so on. All that was missing was a class library to encapsulate the organization of the graphical elements into a game scenario. Stan's Theatrix library filled that need, and we agreed to publish it in a book that includes the source code for the library, several demonstration games, and the shareware and freeware programs on a companion CD-ROM. The book is to be entitled C++ Games Programming and will be published in mid-1995 by M&T Books.

Abstraction

Theatrix provides several levels of interface. Each lower level in the class hierarchy encapsulates and hides more of the details of the game implementation, raising the programmer's level of abstraction in his or her view of the problem. At the highest level of abstraction, you create scenes that include players under the control of directors. You are unaware of (and do not care) how the library manages the low-level details of page flipping, z-ordering, sound generation, and the like. Those details are hidden in the Theatrix class implementations.

In this article, I'll describe Theatrix at its highest level of abstraction and provide example code that uses the library at that level. When the book is available, you will be able to use these examples to build the simple demo game discussed here, as well as others, by using the library and tools on the companion CD-ROM. When the library is complete, the entire source code will be available electronically; see "Availability," page 3.

A Graphics Library

The lowest level of game control is handled by a graphics video package. To support the objectives of Theatrix, the package must be able to display full-screen, static graphics scenes and superimpose smaller frames of graphic sprites at refresh rates fast enough to suggest movement--to achieve animation. We chose FastGraph, a graphics library from Ted Gruber Software (Las Vegas, NV) known for its efficiency and performance. To experiment with Theatrix classes, you will need at least the shareware version of FastGraph, which is available for download from the graphics forums on CompuServe and other online services. For serious Theatrix-based game development, you should get the commercial version of FastGraph. As with all shareware programs, the downloaded version includes ordering information for the registered version.

A future version of Theatrix will work with the graphics APIs of Win32, allowing portable games that compile and run on both DOS and Windows with few or no source-code changes. The same executables will run with Windows NT, Windows 95, and Windows 3.1 with WinG and Win32s. That work is underway, but not yet ready for widespread consumption.

Game Action

A game usually consists of several ancillary displays--menus, help screens, and options screens--but they all eventually get down to the action. That's the part that I'll discuss here. The game's action is organized into scenes, with each scene under the control of a director. The scene director is a game-dependent class derived from the Theatrix SceneDirector class. Only one scene is playing out at any given time. Each scene has a background display and one or more players--sprites--derived from the Theatrix Player class. Players move around the screen and do things in response to external events such as keystrokes and ticks of the clock. You build action into a game by deriving player objects that remember their current, game-dependent mode and change their modes, image, and position at regular intervals based on those modes. Players can communicate with one another by using messages or simply calling member functions. Players and directors communicate with one another in the same ways.

There are several distinctions between the graphical representation of a scene's background and that of the players: The background is stationary, occupies the entire screen, and represents the lowest (most-distant) z-order; the players are smaller, superimposed over the background, and positioned anywhere on the screen. Players move around, maintaining distinct z-order relationships with one another and are always in front of the background. The z-order determines which player passes in front when two players intersect on the screen. Players can enter and leave the scene through portals (doors, for example) defined as clipping parameters for the display of the player. There are usually several graphic renderings for each player and one image for the scene's background. The player tells Theatrix which player image to display at any given interval, and the combination of images generates animated sequences.

Game Timers and Events

Theatrix uses the 18.2-ticks-per-second clock frequency to manage animation. The scene director includes an on_timer function called at that interval. The players each have an update_position function that is called at intervals specified in numbers of ticks. The system automatically calls update_position, a virtual member function of the Player base class. Players and directors may also register to be called when external events occur. For example, a player may use a specified keystroke to initiate an action, such as firing a weapon or changing the direction of movement. The registration specifies the event and the member function in the player's class to be called when the event occurs. The derived Player class declares the registrations with macros.

Game Architecture

A Theatrix-based game metaphor usually follows this scenario: The game program instantiates a derived SceneDirector object, which constructs itself and instantiates one or more derived Player objects. The director and the players register for events with macros at compile time. Each player has a z-order based on its order of instantiation within the director, and the player's constructor specifies how frequently the player is to be called to update its position and image.

Once every clock tick, Theatrix calls the director's on_timer function. The director monitors the action in the game and sends messages to players to tell them what to do next. At regular intervals, Theatrix calls each player so that the player can update its image and position.

The director and players respond to events for which they have registered. These events cause the director and players to change either their own mode parameters or those of other elements in the game. The players' timer-driven functions respond to these modes and call Theatrix functions to change their position and image.

Inside Theatrix

Theatrix maintains three screen buffers. One always contains a rendering of the background scenery with no players in view. This buffer is constant and never changes. Its purpose is to provide screen segments of the untouched background to effectively erase a player's current image. The second buffer is a working buffer in which Theatrix builds the next screen to be displayed. The third buffer is the one that the user is currently viewing. Most Theatrix games use the VGA's Mode X, which has 320x240-pixel resolution, 256 colors, and three video buffers. Theatrix takes advantage of the fact that memory-to-memory writes are faster when both buffers are video buffers.

During construction, a scene-director object tells Theatrix the name of a PCX file that contains its graphical rendering. Theatrix uses this file to build the first versions of the three buffers. When player objects construct, they tell Theatrix the name of a file of images that contain the animation still frames for the player. Theatrix reads these image clips and stores them in extended memory, if the computer has it, or in conventional memory otherwise. Each player may also use an optional file of sound clips in Sound Blaster VOC format; Theatrix likewise stores these clips in extended or conventional memory. These files of image and sound clips are in a Theatrix-specific format. You build the files with utility programs that organize the clips into the format of the database.

Instantiated players are either on or off stage. At each tick of the clock, Theatrix iterates through the current scene director's list of on-stage players. If the player's refresh interval has expired, Theatrix calls the player's update_position function to allow the player to modify its position and image. If the circumstances of the game tell the player to make any changes, it does so by calling Theatrix functions to change its image number, screen coordinates, and possibly its clipping coordinates. These functions only post the changed values; they do not take any immediate action on the image itself. When the player returns, Theatrix uses the player's updated image number and screen position (or existing ones, if no update occurred) to superimpose the image onto the background in the working screen buffer (not the visible one). After iterating through all the players, Theatrix swaps the working screen buffer with the visible one, which displays the updated frame with scenery and all the players in their new configurations. Then, while the user views the updated screen for an instant, Theatrix iterates through the players again, using their current image coordinates and size to erase the player's image by copying a rectangle from the constant scenery buffer to the working buffer. This process prepares the working buffer for the next refresh frame of the entire scene.

A scene director can change the z-order of its players. It does so by monitoring the progress of the action and deciding that a player needs a different position in the z-coordinate system with respect to the other players. Players are maintained in a linked list among the director's data members. To change the z-order, the director changes the position of a player in the list. Theatrix provides interface functions to support this operation.

Graphical Elements

A game's graphical elements consist of PCX files. A scene is a PCX file that fills the 320x240-pixel screen. Players are represented by smaller PCX files organized into player-oriented graphics databases. Each image of a player includes the image's 256-color representation on a solid black background, which the low-level graphics function library uses for a transparent color. Elements of the background scene display through the transparent parts of a player. This permits you to build a player of any shape in a rectangular PCX file.

Building those pictures is the biggest part of game construction, (although designing a good game scenario is no trivial task). Your choice of tools depends on your artistic abilities and the look you want for the game. We built some games by using a paint program to construct all the graphical elements, which gave the game an arcade look and feel. Years ago, I was a newspaper cartoonist, and those skills, rusty though they are, came in handy during this project.

We built other game graphics by using 3-D modeling and ray tracing to achieve photo-realistic images. Games with surrealistic scenery, spacecraft, robots, and accurate perspective are best built this way, although designing and rendering such objects can involve a substantial time investment.

Your choice of a tool for building pictures has no effect on the game's performance, however. To Theatrix, the scenes and players are all PCX files that display on a 256-color, 320x240-pixel Mode-X screen.

An Example

Listing One is skater.cpp, the C++ code that implements a demo of a game. Although the game doesn't do much, the code contains all the elements of a more-complex game built under Theatrix. As Figure 1 shows, the game's background is a skating pond. There are three players in the game: Two of them stand still while the third skates around them in a figure eight. If you press the Enter key, the skating player breaks through the ice, making a splashing sound.

Listing One begins by deriving the Skater class from Theatrix's Player class. Two data-member integers maintain a step count (actually a skid count, since the skater is skating) and the current action mode. There are eight modes during the normal course of the game, representing the eight segments of the figure eight, which is actually a squared eight to simplify the example. The DECLARE_CUELIST macro declares the presence of a list of cue registrations that assign event messages to the class. The CUELIST macro series that follows the class declaration declares the cues that the player receives. In this case there is only one cue, which occurs when the user presses the Enter key. The KEYSTROKE macro specifies a key value and a function to execute when the key is pressed. This method registers all objects of a class for a common set of events. Individual objects of a class can register other events independently of one another by calling functions in Theatrix. Besides keystroke events, there are timer and logical-message events.

The Skater constructor specifies the name of the graphics and sound-effects files that a skater uses to animate itself and make sounds. The Skater::update_position function is called from Theatrix once each timer tick to modify the skater's position and image, if appropriate. The function tests the mode data member to see what to do next. Each segment of the figure eight involves an image and a number of steps through the x- or y-coordinate, depending on whether the segment is horizontal or vertical. I built the images in three sizes to suggest perspective as the skater gets further away from or nearer to the user in the figure-eight pattern.

If the mode member is greater than 8, the user has pressed the Enter key, and the skater crashes through the ice. The program controls that sequence by incrementing modes until it gets to Mode 13. The program manages all animation sequences by calling setxy and set_imageno with appropriate parameters for each execution of update_position.

A Stander class is derived from Player to represent the two stationary figures. This class shares the graphics file with the Skater class, and it has no sound-effects file.

The Pond class is derived from Theatrix's SceneDirector class. It declares instances of Stander classes and sets their position and image numbers. The Pond class also instantiates the Skater object, which manages its own representation and movement.

The Pond::on_timer function overrides SceneDirector's virtual function. The overriding function gets called once for each timer tick, or approximately 18 times per second. Its purpose is to watch the progress of the game and give direction as needed. In this case, the on_timer function monitors the skater's progress. When the skater enters the forward, center, or rear lateral segments of the skating pattern, the director must change the skater's z-order so that the sprites display appropriately. Only the director can change the z-order. If the player tried to do it from within its update_position function, the change could be a problem. That function is executing from within an iteration through the list of players. Changing the z-order changes the sequence of players in the list, which could have an undesirable side effect on the iteration. A player can tell the director to change the player's z-order, but only from a different function, perhaps an event-driven one.

Theatrix is a good example of the powers of abstraction. Listing One contains less than 200 lines of C++ code. However, when combined with a sound clip and some PCX files, the code implements most of an arcade game's contents. You can download an executable version of the skater game and run it to see what you can do with Theatrix and some shareware utilities. The graphics are primitive, roughed out in about an hour with a laptop computer and the wonderful NeoPaint shareware program from Neosoft (Bend, OR). You could use a 3-D modeler and ray tracing to build other PCX files and give the game a completely different, photorealistic look, yet the code would be virtually unchanged.

Figure 1 Sample Theatrix game.

Listing One


// ------- skater.cpp
#include <theatrix.h>
const int sidesteps = 25;  // number of steps in lateral movement
const int fwdsteps = 12;   // number of steps in front/rear movement
const int sstepincr = 5;   // lateral x coordinate increments
const int fstepincr = 3;   // front/rear y coordinate increments
// ------- a moving sprite
class Skater : public Player    {
    short int steps;    // number of steps taken current segment
    short int mode;     // 1-8 = skating pattern #; 11-13 = splash
    friend class Pond;
    void OnEnter(int);
protected:
    DECLARE_CUELIST
public:
    Skater();
    virtual ~Skater() { }
    void update_position();
};
// ---- event message map for Skater class
CUELIST(Skater)
    KEYSTROKE('\r', OnEnter)
ENDCUELIST
// ---- construct a moving sprite
Skater::Skater() : Player("skater.gfx", "skater.sfx")
{
    setxy(90,145);  // initial position on pond
    set_imageno(1); // first skater frame
    appear();
    steps = 0;
    mode = 1;
}
// --- skater frame animation entry point (once every tick)
void Skater::update_position()
{
    switch (mode)    {
        case 1:
        case 3:
        case 5:
        case 7:
            // --- side to side movement
            if (++steps == sidesteps)    {
                steps = 0;
                set_imageno(++mode);
                break;
            }
            if (mode & 2) // modes 3 and 7: to the left
                setx(getx() - sstepincr);
            else          // modes 1 and 5: to the right
                setx(getx() + sstepincr);
            break;
        case 2:
        case 4:
        case 6:
        case 8:
            // --- front or back movement
            if (++steps == fwdsteps)    {
                steps = 0;
                if (mode == 8)
                    mode = 0;
                set_imageno(++mode);
                break;
            }
            if (mode < 6)  // modes 2 and 4: away from the screen
                sety(gety() - fstepincr);
            else           // modes 6 and 8: toward the screen
                sety(gety() + fstepincr);
            break;
        case 9:
            setinterval(3);  // slow down refresh rate
            set_imageno(13); // 1st frame of ice-breaking splash
            mode++;
            play_sound_clip(1);
            break;
        case 10:
            set_imageno(12); // 2nd frame of ice-breaking splash
            mode++;
            break;
        case 11:
            set_imageno(13); // 3rd frame of ice-breaking splash
            mode++;
            break;
        case 12:
            set_imageno(11); // hole in ice
            mode++;
            steps = 0;
            break;
        default:
            if (steps++ == 30)
                stop_director();
            break;
    }
}
// ---- pressed Enter, break the ice
void Skater::OnEnter(int)
{
    if (mode < 9)    {
        int yp = 35;
        if (mode == 3 || mode == 7)
            yp = 25;
        else if (mode > 3 && mode < 7)
            yp = 15;
        setxy(getx()-10, gety() + yp);
        mode = 9;
    }
}
// ---- stationary sprites
class Stander : public Player    {
public:
    Stander() : Player("skater.gfx") { }
};
// ---- the scene: a skating pond
class Pond : public SceneDirector    {
    Stander *stander1;
    Stander *stander2;
    Skater *skater;
    void on_timer();
public:
    Pond();
    ~Pond();
};
// ---- construct the scene
Pond::Pond() : SceneDirector("pond.pcx")
{
    // --- most distant stationary sprite
    stander1 = new Stander;
    stander1->set_imageno(10);
    stander1->setxy(180,100);
    stander1->appear();
    // --- closest stationary sprite
    stander2 = new Stander;
    stander2->set_imageno(9);
    stander2->setxy(140,130);
    stander2->appear();
    // --- moving sprite
    skater = new Skater;
}
Pond::~Pond()
{
    delete skater;
    delete stander2;
    delete stander1;
}
// ---- called after each timer tick
void Pond::on_timer()
{
    SceneDirector::on_timer();
    if (skater->steps == 0)    {
        switch (skater->mode)    {
            case 1:
                // -- front lateral segment
                //    set moving sprite in front of others
                MoveZToFront(skater);
                break;
            case 3:
            case 7:
                // -- center lateral segment
                //    set moving sprite between other two
                ChangeZOrder(skater,stander2);
                break;
            case 5:
                // -- rear lateral segment
                //    set moving sprite behind others
                ChangeZOrder(skater,stander1);
                break;
            default:
                break;
        }
    }
}
int main(int argc,char *argv[])
{
    process_cmdline(argc,argv);
    Pond *pond = new Pond;  // scene director
    begin("Pond");          // launch scene
    delete pond;
    return 0;
}



Copyright © 1995, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.