Action Arcade Adventure Set
Diana Gruber

Chapter 4
The Art of Creating Levels


Find out how you can transform an artist's rendition of a scene to re-usable tiles for creating great visual effects.

Now that you've seen the Fastgraph game editor and have learned about the different components needed to create a side-scrolling arcade game, you're probably eager to jump in and start writing your own game. But before you begin the coding process, you'll need a way to create your level art.In this chapter, we'll have a look at a valuable utility--the tile ripper. This powerful tool can help you automate the process of creating your game levels by breaking up your art into distinct tiles. This tile ripper is the same one that is included in the game editor introduced in the previous chapter. For simplicity, it is presented here as a standalone program.

Creating Level Art for Your Games

One of the first steps in writing a game is to acquire the level art for the game. I have always found this step to be fraught with problems. I can only think of two ways to get art--you either draw it yourself or you get somebody else to draw it for you. Either way, acquiring good level art will be a challenge. Remember that the level art that you create for your games needs to be designed so that it can be represented as distinct tiles. As an example, Figure 4.1 shows how one of the levels in Tommy's Adventures is divided into tiles. The tiles in the level have been selected from the tile library. Recall that each tile is 16x16 pixels in size. Designing art like this poses quite a challenge, especially if you don't have any experience creating art in this fashion. Should you just draw each tile by hand and then piece them together to create your levels? Or, should you hire an artist to draw your levels as a complete scene and then divide the art into distinct tiles?

There's no perfect solution.

Figure 4.1 Representing your level art as distinct tiles.

Making One Tile at a Time

Because programmers are typically pragmatic, technical, and tend to see in things in patterns, they tend to make inferior tile artists. If you take the "programmer approach" to drawing tiles, your levels would probably look like a checkerboard. Figure 4.2 shows an example of a game level that was created by drawing tiles one at a time. The tiles look distinctly rectangular and symmetric, and the background does not look very natural. Rectangular tiles are appropriate for games like Scrabble, Mahjongg, and Dominoes. In side scrollers, we need to create our levels so that they don't look like they are made of tiles. Ideally, we should be able to use our tiles to create asymmetrical objects like trees, gardens, clouds, and space ships. Unless you have natural artistic ability (a rare talent in a programmer), you will get better results if you hire an artist to draw your backgrounds.

Figure 4.2 Tiles drawn individually--like these to create Jason Storm in Space Chase (Safari Software)--tend to look choppy.

Working with an Artist

An experienced side-scrolling game artist will create levels that can be reduced to a set of unique tiles. Unfortunately, most artists seem to have trouble with the concept of reducing artwork to tiles. They tend to give you screens of art that look wonderful, but from which you can not extract any duplicate tiles. This poses a problem because tile duplication is essential to building large levels. You want to be able to build levels containing patterns of tiles that offer variety as well as repetition. Describing this to an artist is very difficult. Suppose, for example, you tell your artist you want some trees and bushes. He understands that you must use the same branches over and over, so he draws you a background with many copies of the same greenery in it. The problem is that the duplicates do not appear at evenly spaced 16-pixel intervals. A perfectly beautiful branch may be repeated 15 or 17 pixels away. When the art is reduced to tiles, both branches show up in unique tiles, rather than the desired multiple copies of the same tile as shown in Figure 4.3.

Figure 4.3 Creating art that can be turned into distinct tiles.

Creating Ideal Art

We want a special kind of background artwork for our games. The artwork must be repetitive without looking blocky, and it must lend itself to building the kind of world we want our characters to inhabit. In particular, we need something for our sprites to stand on--a path or a floor. To compliment that, we will need background scenery, either indoors or outdoors. Some of this scenery will be strictly decorative, but we will also expect our sprites to respond to and interact with parts of the background. That means we will need things like doors, windows, ledges, and tunnels. We need to stretch a small amount of art to cover a very large level. The tiling technique gives us the technology we need to make a small amount of artwork look like a lot of artwork. To make the tiling technique work, the artwork must be designed to lend itself to creating re-usable tiles.

Because we can only work with 240 tiles, we need to get as much mileage as possible out of each tile. The tiles need to be carefully constructed so that we can get repeating patterns that don't look identical. For example, one set of tiles could be used to create both a tall tree and a short tree by simply re-using a few tiles to extend the trunk. Similarly, you can create a very long stretch of floor by repeatedly displaying three or four tiles. Of course, the tiles must also be drawn to fit together perfectly. The left side of one floor tile must match up with the right side of the next floor tile, and that tile must match the next tile, which happens to be identical to the previous tile--because it is the same tile. This sounds complicated, but it is actually quite simple. Figure 4.4 shows an example of how floor tiles should match.

Figure 4.4 A long section of floor may be made from only four tiles.

Building an Actual Level

To understand how you go about building an actual level, take a look at the following steps, which summarize the process:

  1. Create art that can be arranged into tiles.
  2. Divide the art into distinct 16x16 pixel tiles.
  3. Construct the level from the set of tiles.

To draw the level correctly, you must keep track of where each tile goes. As Figure 4.5 shows, a two-dimensional array holds byte values that represent tiles. The entire level is built by examining the array, and placing the appropriate tile at each location. Our job is to break down the artwork into tiles, and generate the array so that we can reconstruct the level.

The 16x16 tile size gives us a good balance between speed and versatility. Smaller tiles, such as 8x8, would give us more variety, but would also create more data to work with, slowing us down. Larger tiles, say 32x16 or 32x32, would allow us to redraw the screen faster, but we would be constrained to working with fewer tiles. The 16x16 tile size is standard for side scrollers, and it is the tile size we will use throughout this book.

Figure 4.5 Building a level with tiles.

Paying for Your Art

If you use an artist, you can pay him or her on an hourly basis, a royalty basis, or a per-tile basis. My best artist charges a fixed rate per tile and gives me great tiles. When he creates background screens that can't easily be reduced to fewer than 240 tiles--the maximum number of tiles that can fit on a screen--I reject them. The bottom line is that when you pay for art, make sure you get your money's worth. If you find an artist who can create dazzling level art and can make duplicate tiles line up on 16 pixel boundaries, reward him or her well. How do you determine how many tiles your artist has drawn? One easy way is to use a tile ripper to reduce the screen to tiles, and then count the tiles. I'll give you the code for this tile ripper soon. By exploring this simple program, you'll learn about the basic graphics programming concepts that we'll use in the later chapters.

The tile ripper is useful for more than counting tiles. It also is the first step in preprocessing background artwork to import into our game. As we will see, preparing artwork is one of the biggest jobs we will face in game development. Having tools to help us with this job will make the development process faster and infinitely more pleasant.

Let 'er Rip!--Introducing the Tile Ripper

Our tile ripper takes raw artwork stored in PCX files and reduces the art to its elemental tiles. The unique tiles that the ripper locates are then stored in a tile library. Later, you can use the tiles in the tile library to add or replace tiles in your level art. The addition and/or replacement of tiles is done by using the level editor presented in the previous chapter. You can also edit the individual tiles in the tile library using the tile editor presented in the previous chapter.

The Tile Library

Background artwork is made up of unique tiles stored in a tile library. Our tile library consists of 240 unique tiles, These are stored on disk in a PCX file and are displayed in offscreen video memory during level editing and game play.

Here's how the tile ripper works: It loads in a PCX file and then displays the art on the screen. Next, the ripper starts at the upper-left corner, picks up a tile as a 16x16 bitmap, and stores it in RAM and in offscreen video memory. Then, the ripper moves down the first column and picks up a second tile bitmap and compares it to the first copy in RAM (see Figure 4.6). If it finds a match, it throws out the second tile, and updates an array called the level_map. If a match isn't found, the new tile is stored on the hidden page, and the tile library is increased by one. Each subsequent tile is compared to the first tile and any other tiles that are found to be unique. Eventually, the tile page contains only unique tiles, and the picture is stored as an array of indices into the tile library.

Figure 4.6 How the tile ripper creates tiles.

As the tile ripper progresses, it "blacks out" the duplicate tiles, leaving only the unique tiles visible. After the ripper has reduced the picture to its elemental tiles, it writes out the level to a binary file and creates a PCX file containing the tile library. Finally, the ripper checks its work by reconstructing the original picture from the tiles and the tile array, and displays it on the visual page. Figure 4.7 shows how the tile ripper processes a sample level art.

Figure 4.7 (a) The original level art; (b) the tile library created by ripping the level art; (c) and the background reduced to unique tiles.

Using the Tile Ripper Program

< Note: Download ripper.zip from the downloads page. Diana. >

You can use the tile ripper program stored on the companion disk by running the program from the DOS command line:

RIPPER infile1 <infile2> ... <infile6>

Notice that you can specify up to six input files. Each of these must be a PCX file. If you don't include at least one PCX file to rip, you'll get the following error message:

Command syntax is: RIPPER infile1 <infile2>...

To simplify running the tile ripper, I have included a batch file, GO.BAT, with some suggested PCX filenames in it.

The tile ripper program will process all of the PCX files you include and will create two output files: TILES.PCX and RIPPER.LEV. The file, TILES.PCX, contains a bitmap image of all the distinct tiles that are created by the ripper. The RIPPER.LEV file contains the information needed to construct levels from the tile library that is created by ripping your art.

On Disk: The tile ripper program is stored on the companion disk as RIPPER.EXE in the directory \fg\util\. If you compile the RIPPER.C source file yourself, make sure that you have either the Fastgraph or Fastgraph/Light library available to link the program.

The Complete Tile Ripper Program

It's time to look at the tile ripper code. Although the code shown here is for a standalone program, when you are working on your game, you will probably want to use the more complete tile ripper that is incorporated into the game editor. The program is called from a menu when the computer is already in graphics mode, and filenames are passed to it according to user input.For our standalone version, we'll need to include the basic code to read in PCX art files and set the proper video modes for processing the art work that is read in. Here is the complete RIPPER.C program.

< Note: code is displayed as "preformatted" text. This works in my Netscape viewer. If your viewer displays gibberish, try downloading the source code. Diana. >

 
/******************************************************************\ 
  ripper.c -- tile ripper for preprocessing background tiles 
              by Diana Gruber 
 
  compile using large memory model 
  requires Fastgraph(tm) or Fastgraph/Light(tm) to link 
 
\******************************************************************/ 
 
#include <fastgraf.h>    /* Fastgraph header file */ 
#include <stdio.h>       /* standard include files */ 
#include <stdlib.h> 
#ifdef __TURBOC__ 
   #include <mem.h>      /* header file for Borland C and Turbo C */ 
#else 
   #include <memory.h>      /* header file for Microsoft C */ 
#endif 
 
int rip(char *filename); /* function declarations */ 
void write_level(void); 
void display_level(int col); 
 
FILE *stream;            /* level output data file */ 
 
#define TILESIZE  256    /* tiles are 16 pixels x 16 pixels */ 
#define TILELIMIT 240    /* only 240 unique tiles */ 
#define MAXROWS   12     /* 12 rows per page */ 
#define MAXCOLS   120    /* up to 6 screens of art = 120 columns */ 
 
/* the ripper_tiles array is used to store the unique tiles as   bitmaps RAM in for easy tile 
comparisons */ 
 
unsigned char far ripper_tiles[TILELIMIT][TILESIZE]; 
 
/* the level_map array stores the information needed to rebuild 
   the level from tiles */ 
 
unsigned char far level_map[MAXCOLS][MAXROWS]; 
 
int tile_index;          /* keep track of current tile */ 
int level_index;         /* keep track of level position */ 
int ncols;               /* total columns in level map */ 
int col;                 /* keep track of current tile position */ 
 
/*****************************************************************/ 
 
void main(int argc, char *argv[]) 
{ 
   register int i; 
 
   /* check that an input file was specified */ 
   if (argc < 2) 
   { 
      printf("Command syntax is: RIPPER infile1 <infile2>...\n"); 
      exit(1); 
   } 
 
   /* initialize the video mode to Mode X: 320x200x256 */ 
   fg_setmode(20); 
 
   /* initialize some globals */ 
   tile_index = 0; 
   level_index = 0; 
   ncols = 0; 
   col = 0; 
 
   /* set all the tiles in the level map to 255 */ 
   memset(level_map,255,MAXCOLS*MAXROWS); 
 
   /* rip all the files specified on the command line */ 
   for (i = 1; i <= argc; i++) 
     rip(argv[i]);   /* write the level data and tiles out to disk */ 
   write_level(); 
 
   /* check your work -- reconstruct the picture from the tiles */ 
   for (i = 0; i < ncols; i+=20) 
      display_level(i); 
 
   /* restore the text video mode and exit */ 
   fg_setmode(3); 
   fg_reset(); 
   exit(0); 
} 
 
/******************************************************************/ 
 
int rip(char *filename) 
{ 
   register int i,n; 
   unsigned char new_tile[TILESIZE]; 
   int x,y,x1,y1; 
   int status; 
   int row; 
 
   /* if you already have a full screen tiles, return an error */ 
   if (tile_index >= TILELIMIT)

return(-1); /* display the PCX file on the visual page */ fg_setpage(0); fg_setvpage(0); fg_move(0,0); status = fg_showpcx(filename,0); /* return an error code if the PCX file is bad or missing */ if (status > 0)

return(status); /* loop on the PCX file, starting at upper-left corner, moving down the columns in sequence */ row = 0; for (n = 0; n < TILELIMIT; n++) { x = (n/12)*16; y = (n%12)*16 + 15; /* get the new tile bitmap */ fg_move(x,y); fg_getimage(new_tile,16,16); /* compare the new tile to all the ripper tiles */ for (i = 0; i < tile_index; i++) { if (memcmp(new_tile,ripper_tiles[i],TILESIZE) == 0) { /* a duplicate tile is found, update the level map */ level_map[col][row] = (unsigned char)i; /* black out the duplicate tile */ fg_setcolor(0); fg_rect(x,x+15,y-15,y); break; } } /* no match was found, therefore the tile must be unique */ if (level_map[col][row] == 255) { /* copy the new tile to the hidden page */ x1 = (tile_index%20)*16; y1 = (tile_index/20)*16 + 23; fg_transfer(x,x+15,y-15,y,x1,y1,0,3); /* build the level map with the tile index */ level_map[col][row] = (unsigned char)tile_index; /* hold the array in RAM for later comparisons */ memcpy(ripper_tiles[tile_index],new_tile,TILESIZE); /* we can't have more than 240 unique tiles */ tile_index++; if (tile_index >= TILELIMIT) break; } /* increment the row and column count */ row++; if (row >= 12) { row = 0; col++; } } /* total number of columns */ ncols = col; } /*****************************************************************/ void write_level(void) { register int i,j; /* make a PCX file out of the tile page */ fg_setpage(3); fg_setvpage(3); fg_makepcx(0,319,8,199,"tiles.pcx"); /* open a binary file for the level array */ stream = fopen("ripper.lev","wb"); /* write out all the columns, 12 tiles per column */ j = 0; for (i = 0; i < ncols; i++) { fwrite(&level_map[i][0],sizeof(char),12,stream); j+=12; } fclose(stream); } /*****************************************************************/ void display_level(int col) { register int i,j; int x,y,x1,y1; int tile; /* set the visual page to page 0 and erase whatever is on it */ fg_setpage(0); fg_setvpage(0); fg_erase(); /* display the tiles starting at the top of the first column */ for (i = 0; i < 20; i++) { for (j = 0; j < 12; j++) { tile = (int)level_map[col+i][j]; x = (tile%20)*16; y = (tile/20)*16 + 23; x1 = i*16; y1 = j*16 + 15; fg_transfer(x,x+15,y-15,y,x1,y1,3,0); } } /* wait a bit so you can see what you did */ fg_waitfor(20); }

Exploring the Tile Ripper Code

Let's go through the program and discuss what each section does. As Table 4.1 shows, the tile ripper program only requires four functions.

Table 4.1 The Functions Used in RIPPER.C

FunctionDescription
main()Opens the PCX art files, sets the video modes, and calls the other functions to create the tiles
rip()Performs the work of ripping tiles and building the tile library
write_level()Creates a PCX file to store the tiles and a binary file to store the level data
display_level()Reconstructs a screen of art from the tiles and level data

Including FASTGRAF.H

Starting at the top, we see the usual include files for an ANSI C program, plus a special one:

#include <fastgraf.h>    /* Fastgraph header file */ 

This is the header file required for Fastgraph. You should include this file in any program that calls any Fastgraph functions. The first few lines of the FASTGRAF.H file look like this:

 
/************************************************************************\ 
*  FASTGRAF.H 
*  This file contains the C and C++ function prototypes for Fastgraph v3.05  * 
*  and Fastgraph/Light v3.05. 
*  Copyright (c) 1991-1993 Ted Gruber Software. All rights reserved. 
\************************************************************************/ 
 
#ifdef __cplusplus 
extern "C" { 
#endif 
 
int    fg_allocate (int); 
int    fg_alloccms (int); 
int    fg_allocems (int); 
int    fg_allocxms (int); 
int    fg_automode (void); 
 
int    fg_bestmode (int, int, int); 
void   fg_box (int, int, int, int); 
void   fg_boxdepth (int, int); 
void   fg_boxw (double, double, double, double); 
void   fg_boxx (int, int, int, int); 
void   fg_boxxw (double, double, double, double); 
int    fg_button (int); 
 
int    fg_capslock (void); 
void   fg_chgattr (int); 
void   fg_chgtext (char *, int); 
void   fg_circle (int); 
void   fg_circlef (int); 
void   fg_circlefw (double); 
void   fg_circlew (double); 
void   fg_clipmask (char *, int, int); 
void   fg_clpimage (char *, int, int); 
void   fg_clprect (int, int, int, int); 
void   fg_clprectw (double, double, double, double); 
void   fg_copypage (int, int); 
void   fg_cursor (int); 
 
void   fg_dash (int, int, int); 
void   fg_dashrel (int, int, int); 
void   fg_dashrw (double, double, int); 
void   fg_dashw (double, double, int); 
void   fg_defcolor (int, int); 
void   fg_defpages (int, int); 
void   fg_dispfile (char *, int, int); 
void   fg_display (char *, int, int); 
void   fg_displayp (char *, int, int); 
void   fg_draw (int, int); 
void   fg_drawmap (char *, int, int); 
void   fg_drawmask (char *, int, int); 
void   fg_drawrel (int, int); 
void   fg_drawrelx (int, int); 
void   fg_drawrw (double, double); 
void   fg_drawrxw (double, double); 
void   fg_draww (double, double); 
void   fg_drawx (int, int); 
void   fg_drawxw (double, double); 
void   fg_drect (int, int, int, int, char *); 
void   fg_drectw (double, double, double, double, char *); 
void   fg_drwimage (char *, int, int); 

As you can see, the header file only contains function declarations for the Fastgraph functions. No variables or structures are declared, no constants or macros are defined, and no additional header files are embedded in the Fastgraph header file. It is really a very benign file, and we don't need to worry about it further.

Looking Up a Function

If you need more information about a Fastgraph function (these functions all have the prefix fg_), use the online manual provided with the companion disk. You can view the manual by reading the text file REF.DOC.

Adding the Definitions

In accordance with commonly accepted C programming practices, we use the C preprocessor directive, #define, to assign logical names to some fixed values. These definitions are included at the top of RIPPER.C:

 
#define TILESIZE  256
#define TILELIMIT 240
#define MAXROWS   12 
#define MAXCOLS   120 

These values will be useful to us in allocating space for our tiles and managing the tile library.

* TILESIZE is the size of a single tile. Since each tile is 16 pixels wide and 16 pixels high, a tile will take up 256 bytes (16x16 = 256). When we allocate space in RAM for a tile, this is how much space we'll need.

* TILELIMIT is the number of tiles we can have. We are limiting ourselves to 240 tiles, which is the number of tiles that can be displayed on a single screen of graphics. As shown in Figure 4.8, our screen is 320x200 pixels, so we can fit 20 tiles horizontally and 12 tiles vertically.

 20 columns of tiles 
x12 rows of tiles 
---- 
240 total tiles on one page 

Figure 4.8 How tiles are arranged on a screen.

Why Limit Your Tiles?

We could conceivably fit more than 240 tiles in video memory, but we choose not to. Our upper limit on tiles is 256 because we want to address them as unsigned bytes. If we have more than 256 tiles, we will have to use integer indices to access the tile array. This is not particularly desirable because our tile arrays are going to be large, and we are going to be facing RAM limitations later on. Byte arrays take up half as much room in RAM as integer arrays. Remember, as game programmers we need to always think in terms of conserving RAM. As our game develops, we'll use every last byte of RAM for things like sprites and sound effects, and the more room we have, the more exciting stuff we can put into our game.

* MAXROWS is the number of rows of tiles. We use 12 rows of tiles per screen:

 
200 lines of pixels in the vertical direction 
/ 16 pixels per tile 
--- 
  12 rows of tiles, plus 8 lines of pixels left over 

Since the screen resolution is 200 lines, we cannot evenly divide it into tiles. We have eight lines of pixels left over. We will ignore those extra eight lines for now, but we'll find a use for that space later. In the tile editor, we will use the extra room for labeling the pages and the tile numbers. In our game, we'll use every bit of video memory we can get our hands on, but in the tile ripper, we don't really need that little bit of extra space, so we'll just ignore it.

* MAXCOLS is the total number of columns. There are 20 columns of tiles per screen, and up to six screens of tiles can be ripped at one time:

 
320 pixels horizontally 
/  16 pixels per tile 
---- 
  20 tiles per screen (horizontally) 
x  6 screens of artwork 
---- 
 120 total columns 

It's unusual to rip more than six screens of tiles at a time. Usually we'll only rip two or three, as in Figure 4.9. But you can rip more screens if you want to, depending on what your art looks like. If you have a lot of screens that contain lots of duplicate tiles, you can increase the number of MAXCOLS. Eventually, though, you will run out of RAM. Also, since there can only be a maximum of 240 unique tiles, you usually reach that number in fewer than six screens of art. If you have more than six screens of art, you probably have unnecessary duplication in your artwork, and you can reduce the number of screens by combining similar pictures into one picture.

Figure 4.9 Three screens of background art for ripping.

Adding the Declarations

The goal of the tile ripper is to reduce several screens of artwork to a single screen of unique tiles, but let's remember we'll also want to reconstruct the original pictures from the tiles. To do that, we'll construct an array to hold the tile data as it relates to the original pictures. Since we'll want to access this array often and from many different functions, it is most convenient to make it a global array, which we declare like this:

 
unsigned char far level_map[MAXCOLS][MAXROWS]; 

Every element of level_map is a number between 0 and 239, representing the 240 unique tiles. We use this array to construct a level out of the tiles. The tile ripper generates this array, and later we'll use a level editor to add rows and columns, rearrange blocks of tiles, and so on. We declare the level_map as a far array to get it out of the default data segment. It will be a fairly large array, and we only have 64K in the default data segment. Even when using the large memory model, we always declare large arrays as far whenever possible to avoid overflowing the near data segment. That's why we also declare the ripper_tiles array to be far:

 
unsigned char far ripper_tiles[TILELIMIT][TILESIZE]; 

The ripper_tiles array keeps copies of the unique tile bitmaps in RAM for fast comparisons.

Next, we'll need to declare some global variables.

 
int tile_index;          /* keep track of current tile */ 
int level_index;         /* keep track of level position */ 
int ncols;               /* total columns in level map */ 
int col;                 /* keep track of current tile position */ 

The tile_index and level_index variables will be useful as we work our way through the rip() function. The ncols variable is the total number of columns we will rip. The final global variable we declare is col. Since we are going to rip the tiles one column at a time, we need to keep track of which column we are currently working on. This value does not need to be global; we could declare it in function main() and pass it to rip(). Some people frown on using global variables, but as a game programmer, I am in the habit of using them. Gamers must think in terms of optimizing everything, including stack space. If you have functions nested several levels deep, and you pass a variable all the way down and all the way back up again, it's not as efficient as having a value immediately available as a global.

On the other hand, you need to be careful with global variables. You don't want to overdo it. Globals take up room in the default data segment (unless declared far), and when working in real mode, gamers are constantly facing a shortage of near memory. Try to use globals appropriately, but sparingly.

Getting to main()

The tile ripper program begins with the main() function. The first thing main() does is examine the command- line parameters and make sure at least one input file is specified. If not, it notifies the user and politely exits. (We could have better error-checking at this point, such as checking the suffix to make sure a PCX file is specified. For a program designed for public consumption, this would be a requirement. But since we are designing this program for in-house use, we can be a little more relaxed about error-checking and just give ourselves enough of an error message to remind ourselves how the program works.)

The mystery function call in the next line of code is:

 
fg_setmode(20); 
This code has far-reaching consequences. It is, perhaps, the most important thing we have done to this point. This single line of code assumes control of the VGA card and sets the video mode to Mode X.

Introducing Mode X

Mode X is a VGA mode that is available on all VGA cards with 256K of memory. Fastgraph calls this mode Mode 20. This particular incarnation of Mode X has a visible resolution of 320x200 and allows you to display 256 simultaneous colors out of a selection of 262,143. Mode 20 is a planar mode, meaning that data is moved in four groups called planes, as opposed to a linear mode where all data is moved sequentially. Don't worry about that right now. Fastgraph will handle the data moves for us. I would prefer not to get sidetracked into a discussion of bit twiddling, because if we do, we may never get our program written. The inner workings of Mode X are documented in many other places. Exactly how Mode X works is not nearly so interesting as what you can do with it. For now, let's just accept that this is the best video mode to use, and move on to some of the wondrous things we can do in this fabulous video mode. The original Mode X, as defined by Michael Abrash, called for a 320x240 resolution. Our video mode has a smaller vertical resolution but retains the other properties of Mode X, including the planar memory organization. Originally, Mode X was known by gamers as the "secret mode."

Initializing Variables

Our next step in the tile ripper program is to initialize the global variables. The indexes and column count all start at 0, and all the elements of the level map are set to 255. Because there are only 240 tiles, 255 is an invalid value for a tile in the level map. We don't want to initialize the level map to all zeros, because zero is a valid tile number. Later, we'll see how this value is used to check an array position to see if a tile has been assigned to it yet. To initialize the level map, we use the C function memset():

 
memset(level_map,255,MAXCOLS*MAXROWS); 

Processing PCX Files

Now that we have declared and initialized everything and set the video mode, we are ready to rip. For each PCX file specified on the command line, we'll call the rip() function to rip each file in sequence.

for (i = 1; i <= argc; i++)
   rip(argv[i]);
The first step in ripping a PCX file is displaying it in video memory. We use Fastgraph's fg_showpcx() function to accomplish this with a minimum of fanfare:

 
status = fg_showpcx(filename,0); 

Supporting Multiple Pages

One of the marvelous things about Mode X is that we have more than one page to work with. That means we can display graphics to any one of four pages, and we can also look at any of those pages. We don't necessarily have to be looking at the same page we are writing to, and it is a common trick to write graphics to offscreen video memory. But in the case of the PCX file, we want to display it on the visual page, which is page 0, and we also want to make sure we are looking at page 0 when we display it. The following two Fastgraph function calls accomplish this:

fg_setpage(0); 
fg_setvpage(0); 

The fg_setpage() function and the fg_setvpage() function set the active and visual page, respectively. That means the PCX file will be displayed on page 0, and page 0 will be visible when the PCX file is displayed.

Into the Heart of the rip() Function

Now comes the heart of the rip() function. We loop continuously, stopping either when we have ripped the whole picture, or when we have reached our limit of 240 unique tiles. We start by grabbing tiles and comparing them to each other, beginning with the tile in the upper-left corner of the picture and moving down the columns. The x and y screen coordinates for the lower-left corner of the tile are calculated based on the loop index, n, and then the fg_move() function is used to move the graphics cursor to the desired location. Then, we use Fastgraph's fg_getimage() function to grab the bitmap from video memory and put it in RAM:

x = (n/12)*16; 
y = (n%12)*16 + 15; 
fg_move(x,y); 
fg_getimage(new_tile,16,16); 

Introducing Fastgraph's Graphics Cursor

The graphics cursor is Fastgraph's internal method of keeping track of an x and y position on the screen. Many Fastgraph functions that "happen" at the graphics cursor including fg_getimage(), fg_drwimage(), fg_display(), and fg_draw(). The graphics cursor is not a real cursor, just a set of (x,y) coordinates referencing a position in video memory.

The bitmap array, new_tile, is declared to hold 256 elements. Our tile is 16x16, so it fits nicely in this array. Since this is our first tile, we know it is going to be unique and will obviously fail the comparison test.

Processing Unique Tiles

When a tile fails the comparison loop, the following things happen. First, the tile is copied to a hidden page (page 3 in this case). This is where the tile library is stored.

 
x1 = (tile_index%20)*16; 
y1 = (tile_index/20)*16 + 23; 
fg_transfer(x,x+15,y-15,y,x1,y1,0,3);

The x and y coordinates of the tile library destination are calculated in terms of the tile index. If it is the first tile, it will be copied to x = 0, y = 23. The second tile will be placed to the right of the first tile, and so on. Tiles are lined up in rows of 20 on the hidden page, as shown in Figure 4.10.

Figure 4.10 Tiles are lined up on the hidden page.

A direct video-to-video blit is used to copy the tile from page 0, the visual page, to page 3, the tile library page. Fastgraph's fg_transfer() function handles the video-to-video blit for us.

Introducing Blits

Ablit, also known as a blt or bitblt, is a block image transfer. It is a shorthand term, commonly used by gamers to describe applying an image to video memory, usually a rectangular shape, and originating either in RAM or in another part of video memory. It's usually used as a verb ("to blit a tile"), and is also used as slang among gamers ("I'm getting an awesome blit rate in Mode X").

The first y coordinate is 23 instead of 15 because of those extra eight lines of video memory we mentioned earlier. We will leave those eight lines empty at the top of the screen as shown in Figure 4.11. Later, we'll use that area for labels and status messages in the tile editor.

Figure 4.11 Accessing the first y coordinate.

The next step is for the tile ripper to add the tile to the level map:

 
level_map[col][row] = (unsigned char)tile_index; 

As we move down the column of tiles in the PCX file, we'll add to this array. This array contains the indexes into the tile library and will be used later to reconstruct the original picture. We also want to keep a copy of the tile in RAM for comparisons to other tiles. We copy it to the ripper_tiles array using C's memcpy() function, like this:

 
memcpy(ripper_tiles[tile_index],new_tile,TILESIZE); 

Repeated String Instructions

Whenever possible, we use C's string functions like memcmp(), memcpy(), and memset() to manipulate arrays. These functions are much faster than writing our own code to compare, copy, and initialize arrays because they take advantage of the microprocessor's repeated string instructions. The code to copy the tile array could be written like this:

for (i = 0; i < tilesize;i++)
   ripper_tiles[tile_index][i] = new_tile[i]; 

But it would be slower than using the memcpy() function.

Then, we increment the tile index, checking to make sure we haven't exceeded the limit of 240 tiles:

 
tile_index++; 
if (tile_index >= TILELIMIT) 
   break; 
Finally, we increment the row index, and if we are at the end of a row, we increment the column index as well:

 
row++; 
if (row >= 12) 
{ 
   row = 0; 
   col++; 
} 

Processing Duplicate Tiles

The code in the previous section handles the case of the unique tile, but what about the non-unique tiles? Suppose the second tile is exactly the same as the first tile, a not-so-unusual situation if you are dealing with a wall or a blue sky, for example. To find duplicate tiles, you need to compare the current tile to all the other tiles in a loop, using the C memcmp() function:

if (memcmp(new_tile,ripper_tiles[i],TILESIZE) == 0)

If a match is found, we don't need to copy the tile to the hidden page or update the ripper_tile array. All we need to do is add the tile to level_map:

 
level_map[col][row] = (unsigned char)i; 

For good measure, we draw a black rectangle over the tile, indicating it is a duplicate tile, as shown here:

 
/* black out the duplicate tiles */ 
fg_setcolor(0); 
fg_rect(x, x+5,y-15,y); 

It is not really necessary to black out the duplicate tiles, but it is helpful when watching the program to get a feel for how many tiles are unique and where they are. It is also helpful in debugging the program. You can tell if the tile ripper is doing something or just sitting there thinking.

If no match is found, the level_map array remains at its original value of 255. After we exit the comparison loop, we look at the level map and see if it has been assigned a value:

if (level_map[col][row] == 255) If the level map is 255, then we know no match was found and the current tile is unique.

Saving the Level Information

The tile ripper progresses through the screen, grabbing tiles, comparing them to previous unique tiles, throwing out the duplicates, and adding unique tiles to the tile library, until it runs out of artwork or hits the 240-tile limit. It rips all the PCX files specified on the command line, and in this way the tile ripper reduces several screens of art to one screen of tiles. As it rips, it generates an array containing the information necessary to rebuild the artwork from the tiles. After all the tiles are ripped, the function write_level() is called and the level information is written to disk. A PCX file is created to store the tiles, and a binary file is created to hold the level data so that the level art can be reconstructed later. The PCX file, TILES.PCX, is created using these three Fastgraph calls:

 
/* make a PCX file out of the tile page */ 
fg_setpage(3); 
fg_setvpage(3); 
fg_makepcx(0,319,8,199," tiles.pcx"); 

fg_makepcx()

The fg_makepcx() function creates a PCX file from the specified rectangular region of the active video page. The region's extremes are expressed in screen space units.

int fg_makepcx(int minx, int maxx, int miny, int maxy, char *filename); 

* minx is the x coordinate of the region's left edge.

* maxx is the x coordinate of the region's right edge. It must be greater than or equal to the value of minx.

* miny is the y coordinate of the region's top edge.

* maxy is the y coordinate of the region's bottom edge. It must be greater than or equal to the value of miny.

* filename is the name of the PCX file to create.

The actual level data is stored in RIPPER.LEV. Once this file is created with the fopen() function, a simple loop is used to send the level data stored in the level_map array to the file:

 
for (i = 0; i < ncols; i++) 
   { 
      fwrite(&level_map[i][0],sizeof(char),12,stream); 
      j+=12; 
   } 

Finishing Up

Once the level information has been saved, a final function, display_level(), is called to double-check our work. If display_level() is able to reconstruct the screens of art from the tiles and the level array, we can assume the rip was successful. Each reconstructed screen of art is left on the screen for just over a second, long enough to see the results, but so long that it becomes boring. We use Fastgraph's fg_waitfor() function to control the delay:

fg_waitfor(20); 

Returning to DOS

Like all well-behaved programs, our tile ripper will put everything back the way it was before it exits. Most importantly, we will set the video mode back to mode 3, which is the default text mode that computers operate in when they are not doing something funny, like running Windows. We will also call fg_reset() to clear the screen and reset the screen attributes, if any.

   fg_setmode(3); 
   fg_reset(); 

The fg_reset() function may require a little more explanation. It is not immediately obvious why it is called. If you are used to looking at white text on a black screen, or if you do not have the ANSI.SYS device driver loaded, then resetting the screen attributes will not affect you. Some of us prefer a more colorful screen at the DOS prompt. For example, I like to look at white letters on a blue screen. I use the Norton Control Center program (from Norton Utilities) to set the default state of my computer to the desired colors. When I exit some programs, I see a partially black screen with a blue and white DOS prompt up in the corner. I think it is more attractive to see a program exit to a fully blue screen with white letters on it. That's what fg_reset() does, it clears the screen to whatever screen attributes were set with ANSI.SYS.

More about Fastgraph

In this chapter we have introduced many of the Fastgraph functions that we will be using in the later chapters. For a complete listing of Fastgraph functions, you can refer to the Fastgraph Reference Manual which is included on the disk. You may print out the entire manual, if you want, but be warned -- it's pretty big! If you print out both the Fastgraph User's Guide and the Fastgraph Reference Manual, it will total about 700 pages. That will keep your printer busy for a while. You can also read the manuals online (they are in a straight ASCII format), or you can get a hard copy version. Information about ordering Fastgraph is in the back of this book. Meanwhile, you can always refer back to this chapter for information about the Fastgraph functions we have discussed so far.

< Note: This book shipped with version 3 of Fastgraph for DOS. The current version is 5, the current shareware version is 4. Fastgraph for Windows is also available. Check out http://www.fastgraph.com. Diana. >

Next Chapter

_______________________________________________

Cover | Contents | Downloads
Awards | Acknowledgements | Introduction
Chapter 1 | Chapter 2 | Chapter 3 | Chapter 4 | Chapter 5 | Chapter 6
Chapter 7 | Chapter 8 | Chapter 9 | Chapter 10 | Chapter 11 | Chapter 12
Chapter 13 | Chapter 14 | Chapter 15 | Chapter 16 | Chapter 17 | Chapter 18
Appendix | License Agreement | Glossary | Installation Notes | Home Page

Fastgraph Home Page | books | Magazine Reprints
So you want to be a Computer Game Developer

Copyright © 1998 Ted Gruber Software Inc. All Rights Reserved.