Action Arcade Adventure Set
Diana Gruber

Chapter 6
Inside the Level Editor

Learn how to fine-tune your levels to elevate the status of your games from good to professional quality.

Years ago I did contract work for several major commercial game publishing and development houses. These are the big boys in the industry who sell games on the shelves in attractive boxes for $50 or more. I wrote some of those games, but not from scratch. My job was coding up somebody else's specs so I can't claim any responsibility for their content. I merely followed instructions and stayed away from the game design issues. I remember on one project my producer gave me some of the worst advice I ever received. He told me, "Don't write games you like to play, write games that sell!"

Then he and I wrote a boxing game. I have never been a fan of contact sports, or sports simulations, so I had no knowledge or "feel" for a boxing game. The game itself seemed pointless to me--two men punched each other in the face until they were both bruised and bloody. The player (or computer player) who inflicted the most damage on his opponent was declared the winner.

My producer assured me this would sell. It didn't. Fortunately, I was paid for my time and not how well the game sold. Why am I telling you this now? I want to warn you not to fall into the same trap. Don't write games that don't inspire you, even if you are offered money to do it. Write games that you personally get a thrill out of playing. That way your games will be much better.

If you share my enthusiasm for side-scrolling arcade games, you've probably played many levels of many games and you have some idea of how a level should be designed. They must be fun, challenging, and they must feel just right. You'll have a much better chance of designing good levels if you play many of these types of games.

Which brings us to the question, how are levels designed? You can design them in your head or you can design them on paper, but eventually you will need to commit your design to data files. For this, you will need another tool and I have just the one for the job--a level editor.

Introducing the Level Editor

The level editor is the tool that helps you refine your levels. You can think of this as the second stage of processing your level art. After the tile ripper has done its job, you will have from one to several screens of art and a library of tiles. These need to be further processed to build levels. The general idea is to start adding rows and columns to the screens you already have. Then you can use block moves to copy tiles from one area to another. In this manner, you can build walls, doors, platforms, tunnels, scenery, and whatever else appeals to your imagination.

Editing the level consists of using the keyboard and the mouse to insert or delete rows and columns, selecting tiles from the level itself or from the tile library, and copying tiles onto the level. These functions are the bare minimum needed to build game levels. Additional features will make the level editor easier and faster to use. If you recall from Chapter 3, the level editor can scroll your art. The scrolling technique we discussed in Chapter 5 will be put to practical use in this chapter. You can use the arrow keys on your keyboard to scroll the level up, down, left, or right, so that any part of it can be viewed and edited. The technique used to scroll the editor is similar to the technique to scroll the game, but it is a little bit simpler. The primary difference is the number of page flips. In the game, we will flip pages constantly. In the level editor, we only flip pages when we need to redraw the screen during a scroll. Also, scrolling in the level editor is done in 16- pixel increments in only four directions. The game can scroll any number of pixels in any direction. So game scrolling is more complicated than level editor scrolling, but as we will see, the underlying technique of resizing video memory and rebuilding screens by moving columns and rows of tiles is the same.

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

Running the Level Editor Program

You can use the level editor stored on the companion disk by running this program from the DOS command line:

LEVEL

You'll then see the program shown in Figure 6.1.

Figure 6.1 The standalone level editor program.

Make sure that you have a mouse installed before trying to run the program. Otherwise, you'll get the following error message

Mouse not found!

and the program will exit. Notice that you don't need to specify any input files or command-`line arguments. The level editor uses the two files created by the tile ripper program presented in Chapter 4, TILES.PCX and RIPPER.LEV. Thus, before you use the level editor you should follow these steps:

  1. Create your level art as PCX files.
  2. Run the tile ripper program (RIPPER.EXE) and convert your art into tiles. When you quit the level editor, it updates the file RIPPER.LEV. Recall that this file stores your level data in a binary format (so, don't try to read it with your word processor!).On Disk: The level editor program is stored on the companion disk as LEVEL.EXE in the directory \fg\util\. If you compile the LEVEL.C source file yourself, make sure that you have either the Fastgraph or Fastgraph/Light library available to link the program.

The Complete Level Editor Program

The source code for the level editor shown next can be compiled into a standalone program. It has been simplified for our discussion. A more complete level editor, along with the source code, is included as part of the game editor on the companion disk. When reading this code, you may want to take note of how the code is structured and the programming style used. For example, notice how the functions are listed in roughly alphabetical order after the main() function--one of my personal style preferences. Other style preferences include placing declarations and definitions at the top of the file, using capital letters for defined constants, not using capital letters in function names or variable names, placing curly brackets on a line by themselves, and using the /* comment */ style. You may have your own preferences; that's fine, but I urge you to be consistent in your coding style so that you can debug and maintain your code. Also, gamers often share code among themselves. Clean, commented, and consistently styled code will make you popular among your peers, who will trade valuable information with you.

Some game programmers prefer to code in C++ these days, although the majority of gamers still code in straight C. C is still my language of preference as well. Again, in the interest of sharing code, it is a good idea to use the languages other game programmers are coding in. Both C and C++ have advantages and disadvantages, and the debate about which is better is likely to continue. Since most game code is "event-driven" and "object- oriented" by nature, you shouldn't have too much trouble turning C code into C++. Here is the complete program for the level editor:

 
/******************************************************************\ 
 level.c -- level editor code for side-scrolling games by 
             Diana Gruber 
 
 compile using large model 
 requires Fastgraph(tm) or Fastgraph/Light(tm) to link 
 
\******************************************************************/ 
 
#include <fastgraf.h>     /* header for the Fastgraph lib */ 
#include <stdio.h> 
#include <stdlib.h> 
 
/* standard defines */ 
#define OFF   0 
#define ON    1 
#define OK    1 
#define FALSE 0 
#define TRUE  1 
 
/* define keys */ 
#define ESC         27 
#define SPACE       32 
#define UP_ARROW    72 
#define LEFT_ARROW  75 
#define RIGHT_ARROW 77 
#define DOWN_ARROW  80 
#define INSERT      82 
#define DELETE      83 
 
/* define colors */ 
#define WHITE       255 
#define BLACK         0 
 
/* mouse variable declarations */ 
int xmouse,ymouse;        /* mouse position  */ 
int buttons;              /* state of mouse buttons */ 
int tile_xmouse;          /* mouse position on the tile page */ 
int tile_ymouse; 
 
/* tile variable declarations */ 
int tile_orgx;            /* tile coords of upper-left corner */ 
int tile_orgy; 
int screen_orgx;          /* screen coords of upper-left corner */ 
int screen_orgy; 
 
/* screen coordinates for scrolling */ 
int vpo;                  /* visual page offset (0 or 240) */ 
int hpo;                  /* hidden page offset (240 or 0) */ 
int tpo;                  /* tile page offset (always 480) */ 
 
/* level map declarations 
#define MAXROWS 200       /* maximum number of tile rows in level */ 
#define MAXCOLS 240       /* maximum number of tile cols in level */ 
 
/* large array containing all the tile information for the level */ 
unsigned char far level_map[MAXCOLS][MAXROWS]; 
 
int nrows;                /* actual number of rows in the level */ 
int ncols;                /* actual number of columns in level */ 
 
FILE *stream;             /* file handle for level data */ 
 
/**** function declarations */ 
void  main(void); 
void  edit_level(void);               /* main editor loop */ 
int   get_tile(void);                 /* get tile from tile page */ 
void  put_tile(int x,int y);          /* put a tile */ 
void  delete_tiles(void);             /* delete row or column */ 
void  insert_tiles(void);             /* insert row or column */ 
void  load_level(void);               /* read level from file */ 
void  redraw_screen(void);            /* put all tiles on screen */ 
void  save_level(void);               /* save level to file */ 
int   scroll_left(int npixels);       /* scrolling functions */ 
int   scroll_right(int npixels); 
int   scroll_down(int npixels); 
int   scroll_up(int npixels); 
void  swap(void);                     /* flip pages */ 
 
/*****************************************************************/ 
 
void main() 
{ 
   /* set the video mode to Mode X 320x200x256 */ 
   fg_setmode(20);   /* resize video memory */ 
   fg_resize(352,727); 
 
   /* initialize the Mode X mouse handler */ 
   if (fg_mouseini() <= 0) 
   { 
      fg_setmode(3); 
      fg_reset(); 
      printf("Mouse not found!\n"); 
      exit(0); 
   } 
 
   /* the mouse cursor is invisible throughout the program */ 
   fg_mousevis(0); 
 
   /* load the level data from a file */ 
   load_level(); 
 
   /* edit the level */ 
   edit_level(); 
 
   /* save the level */ 
   save_level(); 
 
   /* release the Mode X mouse handler */ 
   fg_mousefin(); 
 
   /* reset the video mode and exit */ 
   fg_setmode(3); 
   fg_reset(); 
   exit(0); 
} 
 
/*****************************************************************/ 
 
void delete_tiles() 
{ 
   register int i,j; 
   unsigned char key,aux; 
   int tile; 
 
   /* pop up a message: what do you want to delete? */ 
   fg_setcolor(WHITE); 
   fg_rect(screen_orgx+60,screen_orgx+260, 
           screen_orgy+90+vpo,screen_orgy+110+vpo); 
   fg_setcolor(BLACK); 
   fg_move(screen_orgx+80,screen_orgy+105+vpo); 
   fg_print("Delete Row or Column?",21); 
 
   /* wait for a key press */ 
   fg_getkey(&key,&aux); 
 
   /* delete a column at the current mouse position */ 
   if ((key|32) == 'c' && ncols > 22) 
   { 
      /* calculate the current tile column */ 
      tile = tile_orgx + xmouse/16; 
 
      /* shift all the tiles left by one column */ 
      for (j = 0; j < nrows; j++) 
         for (i = tile; i < ncols-1; i++) 
            level_map[i][j] = level_map[i+1][j]; 
 
      /* zero out the last column */ 
      i = ncols-1; 
      for (j = 0; j < nrows; j++) 
         level_map[i][j] = 0; 
 
      /* decrement the number of columns */ 
      ncols--; 
   } 
 
   /* delete a row at the current mouse position */ 
   else if ((key|32) == 'r' && nrows > 15) 
   { 
      /* calculate the current tile row */ 
      tile = tile_orgy + (ymouse-vpo)/16; 
 
      /* shift all the tiles up by one row */ 
      for (j = tile; j < nrows-1; j++) 
         for (i = 0; i < ncols; i++) 
            level_map[i][j] = level_map[i][j+1]; 
 
      /* zero out the last row */ 
      j = nrows-1; 
      for (i = 0; i < ncols; i++) 
          level_map[i][j] = 0; 
 
      /* decrement the number of rows */ 
      nrows--; 
   } 
 
   /* fix the screen by redrawing all the tiles */ 
   redraw_screen(); 
   return; 
} 
 
/*****************************************************************/ 
 
void edit_level() 
{ 
   register int i,j; 
   unsigned char key,aux; 
   int xbox,ybox,oldx,oldy;       /* mouse coordinates */ 
   int cursor_flag;               /* flag for mouse cursor */ 
   int tile;                      /* tile to get or put */ 
 
   /* start with the mouse at the center of the visual screen */ 
   fg_mousemov(160,100+vpo); 
   fg_mousepos(&xmouse,&ymouse,&buttons); 
 
   /* normalize the x and y coordinates to a tile boundary */ 
   xbox = xmouse&0xfff0; 
   ybox = ymouse&0xfff0; 
 
   /* update oldx and oldy */ 
   oldx = xbox; 
   oldy = ybox; 
 
   /* draw the cursor */ 
   fg_setcolor(WHITE); 
   fg_boxx(xbox,xbox+15,ybox, ybox+15); 
   cursor_flag = ON; 
   tile = 0; 
 
   /* loop continuously and handle events as they are detected */ 
   for(;;) 
   { 
      fg_intkey(&key,&aux); 
 
      /* no key press detected, take care of mouse functions */ 
      if (key+aux == 0) 
      { 
         /* get the current mouse status */ 
         fg_mousepos(&xmouse,&ymouse,&buttons); 
 
         /* normalize for tile space */ 
         xbox = xmouse&0xfff0; 
         ybox = ymouse&0xfff0; 
 
         /* mouse has moved to a new tile position */ 
         if (xbox != oldx || ybox != oldy) 
         { 
            /* xor the old cursor box to get rid of it */ 
            fg_setcolor(WHITE); 
            if (cursor_flag)               
               fg_boxx(oldx,oldx+15,oldy,oldy+15); 
 
            /* draw the cursor box at new position */ 
            fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
            /* update the cursor flag */ 
            cursor_flag = ON; 
 
            /* the new coordinates become the old coordinates */ 
            oldx = xbox; 
            oldy = ybox; 
         } 
 
         /* if the mouse cursor is off, turn it on */ 
         else if (!cursor_flag) 
         { 
            fg_setcolor(WHITE); 
            fg_boxx(xbox,xbox+15,ybox,ybox+15); 
            cursor_flag = ON; 
         } 
 
         /* the left mouse button puts down a tile */ 
         if (buttons == 1) 
         { 
            /* first turn off the mouse cursor */ 
            fg_setcolor(WHITE); 
            fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
            /* set the cursor flag to OFF */ 
            cursor_flag = OFF; 
 
            /* calculate the level array indices */ 
            i = xbox/16; 
            j = (ybox-vpo)/16; 
 
            /* update the level array */ 
            level_map[i+tile_orgx][j+tile_orgy]=(unsigned char)tile; 
 
            /* draw the tile */ 
            put_tile(i,j); 
         } 
 
         /* right button picks up a tile */ 
         else if (buttons == 2) 
         { 
            /* calculate the level array indices */ 
            i = tile_orgx + xmouse/16; 
            j = tile_orgy + (ymouse-vpo)/16; 
 
            /* find the tile in the tile map */ 
            tile = level_map[i][j]; 
         } 
      } 
 
      /* key press detected -- process the key */ 
      else      { 
         /* turn off the mouse cursor and set the flag to OFF */ 
         if (cursor_flag) 
         { 
            fg_setcolor(WHITE); 
            fg_boxx(xbox,xbox+15,ybox,ybox+15); 
            cursor_flag = OFF; 
         } 
 
         /* Escape key was pressed, we are finished */ 
         if (key == ESC) 
            return; 
 
         /* arrow keys were pressed -- scroll around */ 
         else if (aux == LEFT_ARROW) 
            scroll_left(16); 
         else if (aux == RIGHT_ARROW) 
            scroll_right(16); 
         else if (aux == UP_ARROW) 
            scroll_up(16); 
         else if (aux == DOWN_ARROW) 
            scroll_down(16); 
 
         /* Spacebar gets at tile from the tile page */ 
         else if (key == SPACE) 
            tile = get_tile(); 
 
         /* delete a row or column of tiles */ 
         else if (aux == DELETE) 
            delete_tiles(); 
 
         /* insert a row or column of tiles */ 
         else if (aux == INSERT) 
            insert_tiles(); 
      } 
   } 
} 
 
/*****************************************************************/ 
 
int get_tile() 
{ 
   int xbox,ybox; 
   int oldx,oldy; 
   int old_xmouse, old_ymouse; 
   int tile_num; 
   unsigned char key, aux; 
 
   /* keep track of the current mouse position */ 
   old_xmouse = xmouse; 
   old_ymouse = ymouse; 
 
   /* pan to the tile page area */ 
   fg_pan(0,tpo); 
 
   /* change the mouse limits and move the mouse */ 
   fg_mouselim(0,319,tpo,tpo+176); 
   fg_mousemov(tile_xmouse,tile_ymouse); 
 
   /* calculate the mouse cursor position */
   xbox = tile_xmouse&0xfff0; 
   ybox = tile_ymouse&0xfff0; 
   oldx = xbox; 
   oldy = ybox; 
 
   /* draw the mouse cursor */ 
   fg_setcolor(WHITE); 
   fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
   for(;;) 
   { 
      fg_intkey(&key,&aux); 
      if (key == ESC) 
         break; 
 
      /* check the mouse position and normalize for tile space */ 
      fg_mousepos(&xmouse,&ymouse,&buttons); 
      xbox = xmouse&0xfff0; 
      ybox = ymouse&0xfff0; 
 
      /* mouse has moved, redraw the mouse cursor */ 
      if (xbox != oldx || ybox != oldy) 
      { 
         /* clear the old cursor */ 
         fg_boxx(oldx,oldx+15,oldy,oldy+15); 
 
         /* draw the new cursor */ 
         fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
         /* update the old x and y values */ 
         oldx = xbox;         oldy = ybox; 
      } 
 
      /* button_press detected, we have chosen our tile */ 
      if (buttons == 1) 
         break; 
   } 
 
   /* calculate the tile number from the mouse position */ 
   tile_num = ((ybox-tpo)/16) * 20 + (xbox/16); 
 
   /* clear the mouse cursor */ 
   fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
   /* keep track of the position for the next time */ 
   tile_xmouse = xbox; 
   tile_ymouse = ybox; 
 
   /* pan back to the visual page */ 
   fg_pan(screen_orgx,screen_orgy+vpo); 
 
   /* reset mouse limits and move the mouse back where it was */ 
   fg_mouselim(0,336,vpo,vpo+224); 
   fg_mousemov(old_xmouse,old_ymouse); 
 
   /* give yourself enough time to get your finger off the button */ 
   fg_waitfor(5);   return(tile_num); 
} 
 
/*****************************************************************/ 
 
void insert_tiles() 
{ 
   register int i,j; 
   unsigned char key,aux; 
   int tile; 
 
   /* pop up a message: what do you want to insert? */ 
   fg_setcolor(WHITE); 
   fg_rect(screen_orgx+60,screen_orgx+260, 
           screen_orgy+90+vpo,screen_orgy+110+vpo); 
   fg_setcolor(BLACK); 
   fg_move(screen_orgx+80,screen_orgy+105+vpo); 
   fg_print("Insert Row or Column?",21); 
 
   /* wait for a key press */ 
   fg_getkey(&key,&aux); 
 
   /* insert a column at the current mouse position */ 
   if ((key|32) == 'c' && ncols < MAXCOLS) 
   { 
      /* increment the number of columns */ 
      ncols++; 
 
      /* calculate the current column */ 
      tile = tile_orgx + xmouse/16; 
 
      /* shift all the columns right by one */ 
      for (j = 0; j < nrows; j++) 
         for (i = ncols-1; i > tile; i--) 
            level_map[i][j] = level_map[i-1][j]; 
   } 
 
   /* insert a row at the current mouse position */ 
   else if ((key|32) == 'r' && nrows < MAXROWS) 
   { 
      /* increment the number of rows */ 
      nrows++; 
 
      /* calculate the current row */ 
      tile = tile_orgy + (ymouse-vpo)/16; 
 
      /* shift all the rows down by one */ 
      for (j = nrows-1; j > tile; j--) 
         for (i = 0; i < ncols; i++) 
            level_map[i][j] = level_map[i][j-1]; 
   } 
 
   /* fix the screen by redrawing all the tiles */ 
   redraw_screen(); 
   return; 
} 
 
/*****************************************************************/ 
 
void load_level() 
{ 
   register int i,j; 
 
   /* initialize some global variables */ 
   tile_orgx = 0; 
   tile_orgy = 0; 
   screen_orgx = 0; 
   screen_orgy = 0; 
   vpo = 0; 
   hpo = 240; 
   tpo = 480; 
   tile_xmouse = 0; 
   tile_ymouse = 480; 
   /* set the mouse limits */ 
   fg_mouselim(0,336,vpo,vpo+224); 
 
   /* display the tiles in the tile area */ 
   fg_move(0,tpo); 
   fg_showpcx("tiles.pcx",2); 
 
   /* open the level file and read the level information */ 
   if ((stream = fopen("ripper.lev","rb")) != NULL) 
   { 
      fread(&ncols,sizeof(int),1,stream); 
      fread(&nrows,sizeof(int),1,stream); 
 
      for (i = 0; i < ncols; i++) 
         fread(&level_map[i][0],sizeof(char),nrows,stream); 
      fclose(stream); 
   } 
 
   /* if you didn't find the file, just initialize the tiles to 0 */ 
   else 
   { 
      ncols = 22; 
      nrows = 15; 
      for (i = 0; i < ncols; i++) 
         for (j = 0; j < nrows; j++) 
            level_map[i][j] = 0; 
   } 
 
   /* fix the screen by redrawing all the tiles */ 
   redraw_screen(); 
} 
 
/*****************************************************************/ 
 
void put_tile(int xtile, int ytile) 
{ 
   int tile_num; 
   int x,y; 
   int x1,x2,y1,y2; 
 
   /* get the tile information from the tile map */ 
   tile_num = (int)level_map[xtile+tile_orgx][ytile+tile_orgy]; 
 
   /* calculate the destination coordinates */ 
   x = xtile * 16; 
   y = ytile * 16 + 15 + vpo; 
 
   /* calculate the source coordinates */ 
   x1 = (tile_num%20)*16; 
   x2 = x1+15; 
   y1 = (tile_num/20)*16 + tpo; 
   y2 = y1+15; 
 
   /* copy the tile */ 
   fg_transfer(x1,x2,y1,y2,x,y,0,0); 
} 
 
/*****************************************************************/ 
 
void redraw_screen() 
{ 
   register int i,j; 
 
   /* copy all the tiles to the visual page */ 
   for (i = 0; i < 22; i++) 
      for (j = 0; j < 15; j++) 
         put_tile(i,j); 
} 
 
/************************** save_level ****************************/ 
 
void save_level() 
{ 
   register int i; 
 
   /* open a binary file for writing */ 
   if ((stream = fopen("ripper.lev","wb")) != NULL) 
   { 
      /* write out the number of columns and rows */ 
 
      fwrite(&ncols,sizeof(int),1,stream); 
      fwrite(&nrows,sizeof(int),1,stream); 
 
      /* write each column, in sequence */ 
 
      for (i = 0; i < ncols; i++) 
         fwrite(&level_map[i][0],sizeof(char),nrows,stream); 
      fclose(stream); 
   } 
} 
 
/*****************************************************************/ 
 
int scroll_down(int npixels) 
{ 
   register int i; 
 
   /* no tiles need to be redrawn */ 
   if (screen_orgy <= 40-npixels) 
   { 
      screen_orgy+=npixels; 
      fg_pan(screen_orgx,screen_orgy); 
   } 
 
   /* redraw one row of tiles and do a page swap */ 
   else if (tile_orgy < nrows - 15) 
   { 
      tile_orgy++; 
      screen_orgy-=(16-npixels); 
      fg_transfer(0,351,16+vpo,vpo+239,0,223+hpo,0,0); 
      swap(); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
   } 
 
   /* can't scroll down */ 
   else 
      return(-1); 
 
   return(OK); 
} 
 
/*****************************************************************/ 
 
int scroll_left(int npixels) 
{ 
   register int i; 
 
   /* no tiles need to be redrawn */ 
   if (screen_orgx >= npixels) 
   { 
      screen_orgx-=npixels; 
      fg_pan(screen_orgx,screen_orgy); 
   } 
 
   /* redraw one column of tiles and do a page swap */ 
   else if (tile_orgx > 0) 
   { 
      tile_orgx--; 
      screen_orgx+=(16-npixels); 
      fg_transfer(0,335,vpo,vpo+239,16,hpo+239,0,0); 
      swap(); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
   } 
 
   /* can't scroll left */ 
   else      return(ERR); 
 
   return(OK); 
} 
 
/*****************************************************************/ 
 
int scroll_right(int npixels) 
{ 
   register int i; 
 
   /* no tiles need to be redrawn */ 
   if (screen_orgx <= 32-npixels) 
   { 
      screen_orgx+=npixels; 
      fg_pan(screen_orgx,screen_orgy); 
   } 
 
   /* redraw one column of tiles and do a page swap */ 
   else if (tile_orgx < ncols - 22) 
   { 
      tile_orgx++; 
      screen_orgx-=(16-npixels); 
      fg_transfer(16,351,vpo,vpo+239,0,hpo+239,0,0); 
      swap(); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
   } 
 
   /* can't scroll right */ 
   else      return(ERR); 
 
   return(OK); 
} 
 
/*****************************************************************/ 
 
int scroll_up(int npixels) 
{ 
   register int i; 
 
   /* no tiles need to be redrawn */ 
   if (screen_orgy >= npixels) 
   { 
      screen_orgy-=npixels; 
      fg_pan(screen_orgx,screen_orgy); 
   } 
   /* redraw one row of tiles and do a page swap */ 
   else if (tile_orgy > 0) 
   { 
      tile_orgy--; 
      screen_orgy+=(16-npixels); 
      fg_transfer(0,351,vpo,223+vpo,0,hpo+239,0,0); 
      swap(); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
   } 
 
   /* can't scroll up */ 
   else      return(ERR); 
 
   return(OK); 
} 
 
/*****************************************************************/ 
 
void swap() 
{ 
   /* reverse the hidden page and visual page offsets */ 
   vpo = 240 - vpo; 
   hpo = 240 - hpo; 
 
   /* set the origin to the visual page */ 
   fg_pan(screen_orgx,screen_orgy+vpo); 
 
   /* calculate the new mouse position */ 
   ymouse -= hpo; 
   ymouse += vpo; 
 
   /* reset the mouse limits and move the mouse */ 
   fg_mouselim(0,336,vpo,vpo+224); 
   fg_mousemov(xmouse,ymouse); 
} 

Exploring the Level Editor Code

Although the level editor might seem like a big complex program to you, it is actually very easy to follow. Let's take a quick look at all of the functions that are defined and then we'll move in a little closer and examine some of the important details. Table 6.1 shows all of the functions listed in order of their appearance.

Table 6.1 The Functions Defined in LEVEL.C

FunctionDescription
main()Initializes the graphics environment and mouse, and launches the editing function
delete_tiles()Deletes a row or column of tiles
edit_level()Handles mouse and keyboard events
get_tile()Selects a tile from the tile library
insert_tiles()Inserts a row or column of tiles
load_level()Loads tile library and level data from disk
put_tile()Copies a tile from the tile library to the level
redraw_screen()Rebuilds the entire screen by copying all the necessary tiles
save_level()Writes the level data to disk
scroll_down()Scrolls the screen down
scroll_left()Scrolls the screen left
scroll_right()Scrolls the screen right
scroll_up()Scrolls the screen up
swap()Toggles visible page between 0 and 1 (do a page flip)

Declarations, Definitions, and Preprocessor Directives

As with the tile ripper code, we begin by including the standard C header files and the Fastgraph header file containing the Fastgraph function declarations. Then, we define some constant values, including integer values for keystroke characters and colors. Giving commonly used constants a logical name is a standard practice for writing clean, readable C code. We declare a number of global variables, starting with the variables used to keep track of the mouse status. The level editor makes heavy use of the mouse, and keeping track of the mouse cursor in a resized Mode X video mode is tricky. Global variables keep track of the mouse position on both the level and the tile page, and, as we swap between those two areas, we will reposition the mouse to where it was previously.

We can use the same global variables introduced in Chapter 5 to keep track of tile and screen coordinates::

 
int tile_orgx;            /* tile coords of upper-left corner */ 
int tile_orgy; 
int screen_orgx;          /* screen coords of upper-left corner */ 
int screen_orgy; 

The tile_orgx and tile_orgy variables represent the screen origin in terms of tile space. That is, if the 22 horizontal tiles currently in video memory range from column 10 to column 31, then tile_orgx will be 10. Similarly, if the horizontal tiles range from row 100 to row 114, then tile_orgy will be 100 (see Figure 6.2).

Figure 6.2 The tile origin designates the position of the screen in tile space

The screen_orgx and screen_orgy variables are the coordinates in physical video memory of the origin of the screen. They will range in value from 0 to 31 for screen_orgx and 0 to 39 for screen_orgy, as shown in Figure 6.3.

Figure 6.3 The screen floats around in video memory.

Three global offset variables are declared, vpo, hpo, and tpo. The vpo variable (discussed briefly in Chapter 5) is the visual page offset. It is the y coordinate of the top of the visual page. It will always be either 0 or 240. In general, we'll add the value vpo to screen_orgy every time we do a page flip. Similarly, hpo is the hidden page offset. It is the y coordinate of the top of the hidden page. We will add this value to a tile's y coordinate whenever we want to draw the tile on the hidden page. The tpo variable is the tile page offset; it is located at y = 480. This is the top y coordinate of the area where the tiles are located in video memory. The mappings for these global variables are shown in Figure 6.4.

Figure 6.4 Mappings for the global variables used in the level editor.

The level_map array is used to hold the tile information for the level. This is the same array we used in the tile ripper, but it is declared with a bigger size this time. MAXROWS is defined to be 200 and MAXCOLS is defined to be 240. That gives us a nice large rectangular level. You can change these values if you want; for example, you may want to design a level that is very tall but not too wide. Remember, these values represent the maximum values for the rows and columns. In general, the size of our level will be somewhat smaller than this.

Here's main()

LEVEL.C begins with function main(), which starts by initializing the video mode and resizing video memory (as discussed in Chapter 5). Then the mouse is initialized. Using the mouse in Mode X is tricky. Since most commercial mouse drivers are unaware of Mode X (in fact, I'm not aware of one that is), we use Fastgraph to handle the mouse functions. Fastgraph controls the mouse cursor in Mode X by hooking its own mouse handler to the mouse driver. It accomplishes this through function 12 of interrupt 33 hex. The Fastgraph call to initialize the mouse is fg_mouseini(). Here's the code in main() that performs this task:

/* initialize the Mode X mouse handler */ 
   if (fg_mouseini() <= 0) 
   { 
      fg_setmode(3); 
      fg_reset(); 
      printf("Mouse not found!\n"); 
      exit(0); 
   } 
Note that if Fastgraph is unable to initialize the mouse, the level editor exits with an error message. The mouse is required to run the level editor.

Fastgraph's mouse handler remains in effect until it is explicitly disabled, so it's important to unhook the handler before your program exits, which is the purpose of Fastgraph's fg_mousefin() function. You'll find a call to this function at the end of main().

After the mouse is initialized by loading Fastgraph's mouse handler, we need to turn off the mouse cursor. This is accomplished by calling fg_mousevis(), as shown here:

/* the mouse cursor is invisible throughout the program */ 
fg_mousevis(0); 

Loading the Level Data

Once all of the initialization tasks are completed, it's time to call the load_level() function to load the level data from the disk. This includes the tiles, which are stored in the PCX file, TILES.PCX, and the level array, which is stored in a binary data file, RIPPER.LEV. The PCX file is displayed in the tile area that we defined in Chapter 5, as shown in Figure 6.5. This is accomplished with just a few function calls:

/* display the tiles in the tile area */ 
fg_move(0,tpo); 
fg_showpcx("tiles.pcx",2); 

Figure 6.5 Background tiles are stored in video memory.

Notice that the load_level() function also sets up the mouse limits.

/* set the mouse limits */ 
fg_mouselim(0,336,vpo,vpo+224); 
The mouse limits will need to be changed often. Since we have resized video memory to one large page, it is quite easy to move the mouse cursor off the edge of the screen. Because the mouse cursor is only useful when we can see it, we want to keep it visible.

The fg_mouselim() function constrains the movement of the mouse to a rectangular area. We will use this function to keep the mouse cursor within the visible part of video memory. Since the visible screen floats around in video memory, we will need to recalculate and change the mouse limits every time we change the screen origin.

Next, we need to read the level data from the RIPPER.LEV file. Here's the code that opens the file and reads the data into the level_map array:

/* open the level file and read the level information */ 
if ((stream = fopen("ripper.lev","rb")) != NULL) 
{ 
   fread(&ncols,sizeof(int),1,stream); 
   fread(&nrows,sizeof(int),1,stream); 
 
   for (i = 0; i < ncols; i++) 
      fread(&level_map[i][0],sizeof(char),nrows,stream); 
   fclose(stream); 
} 

If the RIPPER.LEV file is not found, this code is skipped and the load_level() function initializes the level for editing. It begins by setting the level size to the size of one page--22 columns by 15 rows--and then sets that part of the level_map array to all zeros. :

/* if you didn't find the file, just initialize the tiles to 0 */ 
else 
{ 
   ncols = 22; 
   nrows = 15; 
   for (i = 0; i < ncols; i++) 
      for (j = 0; j < nrows; j++) 
         level_map[i][j] = 0; 
} 

Finally, load_level() calls redraw_screen(), which simply draws the screen by copying all the tiles from the tile area to the visual page.

After initializing the video mode and the mouse, and loading the level data, it's time to start editing the level. This is handled in the edit_level() function.

Editing a Level

The first thing the edit_level() function does is move the mouse to the center of the visual page and then read the position of the mouse using two useful Fastgraph functions:

/* start with the mouse at the center of the visual screen */ 
fg_mousemov(160,100+vpo); 
fg_mousepos(&xmouse,&ymouse,&buttons); 

The Mouse Cursor

One of the level editor's tricks is keeping track of the mouse cursor. Even though the cursor is only 16x16 pixels and we could let Fastgraph's default mouse cursor handle it, we are choosing to draw the cursor ourselves. There are several reasons for this, the obvious being we want to highlight tiles in 16-pixel increments. Instead of trying to figure out how to move the mouse in 16-pixel jumps, it is easier to just move the mouse smoothly and redraw the cursor after the mouse has moved 16 pixels. As we add features to our editor, we'll want to increase the size of the mouse cursor. Picking up tiles one at a time works only for the most rudimentary level editing. To really whip out levels in a hurry, we want to pick up entire blocks of tiles--whole doors or platforms, for example. If we pick up blocks of 10 or 20 tiles, we'll need a bigger mouse cursor to highlight them. Also, sometimes we don't want a cursor at all. When importing graphics, for example, we may want to use the mouse to move crosshairs. Since we know we are going to have to eventually take control of the mouse cursor, let's do it from the beginning when it's easy.

The algorithm is simple, but requires a bit of bookkeeping. We need to keep track of not only where the mouse is now, but where it was the last time a cursor was drawn. We don't want to redraw the cursor every frame, because that would cause an unacceptable level of flickering. The only time the cursor is redrawn is when the mouse has moved at least 16 pixels so that it is sitting on a different tile. Then the old tile is unhighlighted and the new tile is highlighted.

The xmouse and ymouse variables are the current mouse coordinates, as returned by the function fg_mousepos(). These need to be normalized to tile space. We use this code to normalize the mouse coordinates:

 
xbox = xmouse&0xfff0; 
ybox = ymouse&0xfff0; 

This code has the effect of reducing xmouse and ymouse to multiples of 16. As long as the mouse moves around in the same tile, xbox and ybox will remain the same. If xbox or ybox change, then the mouse has moved outside a tile and we need to draw a new cursor. To do the comparison, we'll need some variables to store the old mouse values. We'll call them oldx and oldy.

 
oldx = xbox; 
oldy = ybox; 
To draw the mouse cursor, we display a box in exclusive or (xor) mode to outline the current tile. First we call the fg_setcolor() function to set the current color to white, then we call the fg_boxx() function to draw the xor box.

 
/* draw the cursor */ 
fg_setcolor(WHITE); 
fg_boxx(xbox,xbox+15,ybox, ybox+15); 

The box we have just drawn outlines the tile the mouse is currently positioned over. We will consider this outline box to be the mouse cursor, and we will say the mouse is currently pointing to the tile that is highlighted. Since the cursor consists of an xor box, we need to keep track of whether the cursor is on or off. If it's off, drawing an xor box turns it on. If it's on, drawing an xor box turns it off. If you try to turn it on or off twice in a row, you are going to have problems. You may get cursor remnants on the screen, or you may lose your current cursor. The easiest way to keep track of the cursor status is to set a flag to ON every time the cursor is turned on, and set it to OFF every time the cursor is turned off. Then you only turn the cursor on if it is currently off, and vice versa. We'll call this flag cursor_flag, and declare it to be a local variable. After drawing the box, we'll set the cursor flag to ON:

 
cursor_flag = ON; 

Moving the Mouse Cursor

To move the mouse cursor around in the editor, we need to poll the mouse continuously and compare it to the previous position. As I said before, we don't want to redraw the mouse cursor every iteration because that would cause blinking, so we just need to draw it when it changes position. We handle this by including the following code in a continuous loop:

 
/* loop continuously and handle events as they are detected */ 
for(;;) 
{ 
   fg_intkey(&key,&aux); 
 
   /* no key press detected, take care of mouse functions */ 
   if (key+aux == 0) 
   { 
    /* get the current mouse status */ 
    fg_mousepos(&xmouse,&ymouse,&buttons); 
 
    /* normalize position for tile space */ 
    xbox = xmouse&0xfff0; 
    ybox = ymouse&0xfff0; 
 
    /* mouse has moved to a new tile position */ 
    if (xbox != oldx || ybox != oldy) 
    { 
       /* xor the old cursor box to get rid of it */ 
       fg_setcolor(WHITE); 
       if (cursor_flag) 
           fg_boxx(oldx,oldx+15,oldy,oldy+15); 
 
       /* draw the cursor box at new position */ 
       fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
       /* update the cursor flag */ 
       cursor_flag = ON; 
 
       /* the new coordinates become the old coordinates */ 
       oldx = xbox; 
       oldy = ybox; 
    } 
Executing this code in a loop allows us to move the mouse cursor around the screen smoothly, outlining tiles as the mouse passes over them.

Handling Keyboard and Mouse Events

The edit_level() function accepts input from both the mouse and the keyboard, and processes the input, or events, as it encounters them. Keyboard input and mouse status are polled in the same loop. The keyboard is checked first, using Fastgraph's key intercept function, fg_intkey().

If no key press is detected, the mouse code is executed. However, if a key press is detected, the mouse is temporarily ignored while the key is processed. Since most of the keyboard functions require the mouse cursor to be turned off, we immediately turn it off as soon as a key press is detected.

      /* turn off the mouse cursor and set the flag to OFF */ 
      if (cursor_flag) 
      { 
          fg_setcolor(WHITE); 
          fg_boxx(xbox,xbox+15,ybox,ybox+15); 
          cursor_flag = OFF; 
      } 

This code turns off the mouse cursor by xoring the cursor box, then it sets the cursor flag to OFF. We then need to decide what to do with the key we just detected. The following code handles the keystrokes in the edit_level() event loop:

      if (key == ESC) 
          return; 
 
         /* arrow keys were pressed -- scroll around */ 
         else if (aux == LEFT_ARROW) 
            scroll_left(16); 
         else if (aux == RIGHT_ARROW) 
            scroll_right(16); 
         else if (aux == UP_ARROW) 
            scroll_up(16); 
         else if (aux == DOWN_ARROW) 
            scroll_down(16); 
 
         /* Spacebar gets at tile from the tile page */ 
         else if (key == SPACE) 
            tile = get_tile(); 
 
         /* insert a row or column of tiles */ 
         else if (aux == INSERT) 
            insert_tiles(); 
 
         /* delete a row or column of tiles */ 
         else if (aux == DELETE) 
            delete_tiles(); 
      } 

Selecting Tiles

There are two ways to get a tile. The first--and most common--way is to use the right mouse button to grab a tile from the level. This is very simple to do, and it is handled in the event loop in the edit_level() function, as follows:


    /* right button picks up a tile */ 
    else if (buttons == 2) 
    { 
       /* calculate the level array indices */ 
       i = tile_orgx + xmouse/16; 
       j = tile_orgy + (ymouse-vpo)/16; 
 
       /* find the tile in the tile map */ 
       tile = level_map[i][j]; 
    } 

The level indices, i and j, are calculated based on the mouse coordinates and the tile origin. The tile is "marked" by storing it in a byte-sized variable called tile. This variable represents a value that is found in the level_map array at the [i][j] position.

A second way to select a tile is to use the keyboard to make the tile library visible, and then use the mouse to select a tile from the tile library. The event loop handles this by calling the get_tile() function whenever the Spacebar has been pressed. The get_tile() function similarly loads a byte value from the level_map into the byte variable tile.

Copying Tiles

The tile value that is stored in the tile variable may be placed anywhere on the level by pointing at a tile location with the mouse and pressing the left mouse button. Since this is also a simple matter, the code is handled in the edit_level() event loop, the same way we handled selecting a tile with the mouse:

    /* the left mouse button puts down a tile */ 
    if (buttons == 1) 
    { 
       /* first turn off the mouse cursor */ 
       fg_setcolor(WHITE); 
       fg_boxx(xbox,xbox+15,ybox,ybox+15); 
 
       /* set the cursor flag to OFF*/ 
       cursor_flag = OFF; 
 
       /* calculate the level array indices */ 
       i = xbox/16; 
       j = (ybox-vpo)/16; 
 
       /* update the level array */ 
       level_map[i+tile_orgx][j+tile_orgy]=(unsigned char)tile; 
 
       /* draw the tile */ 
       put_tile(i,j); 
    } 

The first thing that happens is the mouse cursor is turned off and the flag is set to OFF. Then the tile coordinates are determined based on the position of the mouse. The level map is updated to show the new tile at that location and the tile is drawn on the screen.

Functions Called in the Editing Event Loop

The edit_level() event loop gives the keyboard precedence over the mouse. Thus, when a keystroke is detected, it is processed first, despite anything that may be going on with the mouse. Usually when a keystroke is detected, a function is called to handle the event. Unlike the mouse events (moving, selecting a tile, copying a tile), the keyboard events are more complex and are best handled in function calls. The keystrokes presented in the following sections are detected by edit_level() and result in function calls.

Arrow Keys

The functions scroll_left(), scroll_right(), scroll_up() and scroll_down() are called when the arrow keys are pressed. These functions are described in Chapter 4.

Spacebar

When the Spacebar is pressed, the get_tile() function is called. This function repositions the visible part of video memory to the tile area and allows you to select a tile from the tile library using the mouse. Most of this code is fairly straightforward; there are only a few tricks. For example, you must change the mouse limits when the screen changes:

/* change the mouse limits and move the mouse */ 
fg_mouselim(0,319,tpo,tpo+176); 
fg_mousemov(tile_xmouse,tile_ymouse); 
The mouse coordinates are stored in two global variables, tile_xmouse and tile_ymouse. We keep track of these so that the next time we flip to the tile page, we'll be highlighting the same tile as the last time. The logic to move the mouse cursor around is the same as in the edit_level() function. When a button press is detected, we break out of the loop. The tile we have chosen is calculated based on the x and y position of the mouse. We have the option of exiting the function without getting a tile by pressing the Esc key.

When we exit the get_tile() function, we pause for five clock ticks. This is important; we don't want to exit back to the edit_level() function with our finger still on the mouse button. If we do that, we'll place a tile on the level immediately upon returning to edit_level(), which is undesirable. It's a non-fatal error, but an aggravating one nonetheless. Usually, we want to move the mouse around a little before we drop a new tile.

Delete Key

When the Delete key is pressed, the delete_tiles() function is called. This function will delete either a row or column of tiles. The first thing that happens is the Row/Column dialog box is displayed, as shown in Figure 6.6, prompting you to delete a row or column. You then press R for row, C for column, or Esc to exit the deletion operation.

Figure 6.6 The Row/Column dialog box allows you to specify column or row deletion.

The code that displays the Row/Column dialog box consists of a few Fastgraph functions:

/* pop up a message: what do you want to delete? */ 
fg_setcolor(WHITE); 
fg_rect(screen_orgx+60,screen_orgx+260, 
        screen_orgy+90+vpo,screen_orgy+110+vpo); 
fg_setcolor(BLACK); 
fg_move(screen_orgx+80,screen_orgy+105+vpo); 
fg_print("Delete Row or Column?",21); 
 
/* wait for a key press */ 
fg_getkey(&key,&aux); 

A column is deleted by shifting all the columns to the left and decrementing the column count. The code to delete a column looks like this:

   /* delete a column at the current mouse position */ 
   if ((key|32) == 'c' && ncols > 22) 
   { 
      /* calculate the current tile column */ 
      tile = tile_orgx + xmouse/16; 
 
      /* shift all the tiles left by one column */ 
      for (j = 0; j < nrows; j++) 
         for (i = tile; i < ncols-1; i++) 
            level_map[i][j] = level_map[i+1][j]; 
 
      /* zero out the last column */ 
      i = ncols-1; 
      for (j = 0; j < nrows; j++) 
         level_map[i][j] = 0; 
 
      /* decrement the number of columns */ 
      ncols--; 
   } 
As Figure 6.7 shows, all of the columns in the level map are shifted to the left by one column. The last column is set to all zeros. The value ncols, which is the number of columns, is decremented. At the end of the function, we call redraw_screen() to redraw all the tiles.

Figure 6.7 Deleting a column from a level.

Deleting a row is similar to deleting a column. When the user selects R to delete a row, the row at the current mouse position is removed. This is accomplished by first calculating the current tile row

tile = tile_orgy + (ymouse-vpo)/16; 

and then shifting all of the tiles up by one row. To complete the row deletion, the last row in the level_map array is set to all zeros.

Insert Key

When the Insert key is pressed, the insert_tiles() function is called. This function will insert either a row or column of tiles. The first thing that happens is the Row/Column dialog box is displayed prompting you to insert either a row or column. You press R to select row, C for column, or the Esc key to exit the insertion operation.If you look closely at the code in insert_tiles() you'll see that it looks very similar to the code found in delete_tiles(). For example, when a column of tiles is inserted, the steps are the same as those used to remove a column except all the tiles are shifted to the right, duplicating one column of tiles instead of shifting to the left to remove a column.

Exiting the Program

Only one other keystroke is processed in the edit_level() event loop--the Esc key. This event does not cause a function to be called, rather it causes the edit_level() function to return control to main(). The main() function then calls the save_level() function, which writes the level data to the binary file, RIPPER.LEV. As the code shows, this function first stores the number of columns and rows in the level file:

/* write out the number of columns and rows */ 
fwrite(&ncols,sizeof(int),1,stream); 
fwrite(&nrows,sizeof(int),1,stream); 

Then, it spins through a loop and writes each column to the file:

/* write each column, in sequence */ 
for (i = 0; i < ncols; i++) 
   fwrite(&level_map[i][0],sizeof(char),nrows,stream); 
fclose(stream); 

Finally, the main() function finishes up and exits, taking care to disable the mouse handler and reset the video mode on its way out.

Finishing Up

As mentioned before, the level editor code has been simplified for this chapter, and there is a much more complete level editor on disk. Some of the advanced features on the disk include the ability to pick up more than one tile at a time, undo your mistakes, and view tile coordinates, tile numbers, and tile attributes. The additional functionality is important, because you need all the help you can get to design excellent levels. In today's competitive market, good level design is essential to making a game playable and marketable. Level design is an important part of the game design process and should be given priority attention.

Next Chapter

Recommended books for game developers

_______________________________________________

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.