Action Arcade Adventure Set
Diana Gruber

Chapter 9
Wrapping Up the Game Editor

Last stop on the game editor tour. You'll learn about the file manager and the tile editor in preparation for our dive into the actual game code.

We've now explored a number of the features of the game editor, including the tile ripper in Chapter 4, the level editor in Chapter 6, and the sprite editor in Chapter 7. We also looked at the powerful scrolling technique used in the game editor. But if you are keeping count, you probably remember that the game editor provides a few additional features, including the file manager and the tile editor. So, before we leave the game editor and move on to the fun of creating our game, Tommy's Adventures, we need to finish up our programming tour of the game editor.

We'll begin by taking a closer look at the game editor's interface. Then, we'll look at some of the main code highlights for the file manager and the tile editor. You'll want to know something about these components if you ever decide to add new features to the editor. We'll also take a quick look at how the character font is displayed, and introduce an interesting concept--a utility program that generates source code that is then used by another utility program.

Creating the Game Editor Interface

As we discovered in Chapter 3, the game editor provides an easy-to-use menu bar that allows you to select different operations. Table 9.1 shows the functions in the file MENU.C that control the menu bar and the pull- down menus.

Table 9.1 The Main Functions Used to Support the Interface

FunctionDescription
highlight_option()Highlights a menu option (reverses colors)
horizontal_menu()Displays and selects an option from the horizontal menu
submenu1()Calls vertical_menu with FILE options
submenu2()Calls vertical_menu with LEVEL options
submenu3()Calls vertical_menu with TILE options
submenu4()Calls vertical_menu with SPRITE options
vertical_menu()Display and select option from vertical menu
wait_for_keystroke()Waits for keyboard input
wait_for_mouse_buttons()Waits for mouse input
exit_program()Menu selection to exit to DOS

The menu code is really quite simple. There are two kinds of menus. The horizontal menu (menu bar) launches the vertical (pull-down) menus, as shown in Figure 9.1.

Figure 9.1 Horizontal and vertical menus of the game editor.

Designing the Menus

The vertical menus launch the other functions, such as edit_level() or edit_sprite(). The data structures are very helpful in simplifying the job of launching functions from the menus. We include a pointer to an integer function as a structure member in the menu structure. This pointer, called PFI in remembrance of our roots (that's what Brian Kernigan and Dennis Richie called it in their classic language definition, The C Programming Language), points to the integer function we wish to launch. Similarly, the menu command structures are defined as type CMD in remembrance of the good old days when three letter structure labels were considered descriptive. In the horizontal_menu() function, the PFI points to one of the submenus. In the vertical_menu() function, the PFI points to the level editor, sprite editor, and so on. Here is how the PFI, the CMD structure, and the main_menu structure array are declared:

typedef int (*PFI)();   /* pointer to an integer function */ 
#define ITEMS 4         /* number of items on main menu */ 
 
/* command structure */ 
typedef struct cmd 
{ 
   PFI menu_func;       /* function to carry out the command */ 
   char *menu_item;     /* the menu item as written on the screen */ 
   int x1;              /* coordinates of location of menu_item */ 
   int x2; 
}  CMD; 
 
extern CMD main_menu[ITEMS]; 
extern int mouse_limits[ITEMS+1]; 
DECLARE int main_option; 
These declarations are found in the file EDITDEFS.H, which is included in all the C files. The declarations are global because they will need to be seen by more than one function. In particular, the main_menu array will need to be seen by the edit_menu() function which is in the file FGE.C. We'll get to that in a minute.

Managing the Menus

The menu data structures greatly simplify the job of building and executing menus. Each menu is put together by defining an array of structures of type CMD. A pointer to the CMD array is passed to the horizontal_menu() or vertical_menu() function. Changing the menus is as easy as changing the declarations and the function call. The menus are defined in the file MENU.C, and the code for handling the menus is in that file as well. The contents of the file MENU.C is shown here:

 
/******************************************************************\ 
*  menu.c -- game editor source code                               * 
*  copyright 1994 Diana Gruber                                     * 
*  compile using large model, link with Fastgraph (tm)             * 
\******************************************************************/ 
 
#include "editdefs.h" 
 
int (*menu_func)(void); 
int selection = 0; 
CMD main_menu[] = 
{ 
   submenu1, "FILE",       2,  76, 
   submenu2, "LEVEL",     80, 156, 
   submenu3, "TILES",    160, 236, 
   submenu4, "SPRITES",  240, 316 
}; 
 
CMD menu1[] = 
{ 
 load_game_file,  "load/save", 2,  76, 
   exit_program,  "Exit",      2,  76 
}; 
 
CMD menu2[] = 
{ 
      edit_level, "edit",    80, 156,
      save_level_name, "save",    80, 156}; 
 
CMD menu3[] = 
{ 
   do_background, "background",160,236, 
   do_foreground, "foreground",160,236, 
       do_ripper, "ripper",    160,236 
}; 
 
CMD menu4[] = 
{ 
   edit_sprites,      "edit ",   240, 316, 
   load_edit_sprites, "load",    240, 316, 
   load_sprites,      "save",    240, 316 
}; 
 
int mouse_limits[] = {2,80,160,240,320}; 
/******************************************************************/ 
horizontal_menu(CMD *cmdtab,int n,int current) 
{ 
   register int i; 
   int c; 
   int new; 
   int ymin, ymax; 
 
   if (current >= abs(n)) 
      return(ERR); 
 
   ymin = menu_top; 
   ymax = ymin + 10; 
 
   fg_mousevis(OFF); 
   fg_setpage(0); 
 
   /* set up the list of options */ 
   if (n < 0) 
   { 
      for (i = 0; i < abs(n); i++) 
      { 
         fg_setcolor(white); 
         fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
         fg_setcolor(black); 
         center_string(cmdtab[i].menu_item, 
                        cmdtab[i].x1,cmdtab[i].x2,ymax-2); 
      } 
      fg_save(0,319,menu_top,menu_bottom); 
   } 
 
   /* highlight the current option */ 
   i = current; 
 
   fg_setcolor(black); 
   fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
   fg_setcolor(white); 
   center_string(cmdtab[i].menu_item, 
                  cmdtab[i].x1,cmdtab[i].x2,ymax-2); 
 
   /* if we're just displaying the menu options, return */ 
   if (n < 0) return(OK); 
 
   flushkey(); 
 
   /* choose an option */ 
   new = current; 
   fg_mousevis(ON); 
   for(;;) 
   { 
      /* activate the corresponding vertical menu */ 
      main_option = i; 
 
#ifdef __TURBOC__ 
      c = cmdtab[i].menu_func(); 
#else 
      *menu_func = *cmdtab[i].menu_func; 
      c = menu_func(); 
#endif 
 
      /* cycle through the choices */ 
      if (c == LEFT_ARROW || c == BS) 
      { 
         selection = 0; 
         new = i-1; 
         if (new < 0) new = n-1; 
      } 
      else if (c == RIGHT_ARROW || c == SPACEBAR) 
      { 
         selection = 0; 
         new = i+1; 
         if (new >= n) new = 0; 
      } 
 
      /* Esc exits to DOS */ 
      else if (c == ESC) 
      { 
         exit_program(); 
         return(i); 
      } 
 
      else 
      { 
         main_option = i; 
         selection = 0; 
         return(i); 
      } 
 
      if (i != new) 
      { 
         /* unmark previous option */ 
         fg_mousevis(OFF); 
         fg_setcolor(white); 
         fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
         fg_setcolor(black); 
         center_string(cmdtab[i].menu_item, 
                        cmdtab[i].x1,cmdtab[i].x2,ymax-2); 
 
         /* mark new option */ 
         i = new; 
         fg_setcolor(black); 
         fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
         fg_setcolor(white); 
         center_string(cmdtab[i].menu_item, 
                        cmdtab[i].x1,cmdtab[i].x2,ymax-2); 
         fg_mousevis(ON); 
      } 
   } 
} 
/******************************************************************/ 
int submenu1() 
{ 
   return(vertical_menu(menu1,0,2)); 
} 
int submenu2() 
{ 
   return(vertical_menu(menu2,1,2)); 
} 
int submenu3() 
{ 
   return(vertical_menu(menu3,2,3)); 
} 
int submenu4() 
{ 
   return(vertical_menu(menu4,3,3)); 
} 
/******************************************************************/ 
vertical_menu(CMD *cmdtab,int index,int n) 
{ 
   register int i, j; 
   int new; 
   int height; 
   int left, right; 
   int string_x; 
   int x1, x2, y1, y2; 
   int ymin, ymax; 
   int count; 
   char key, aux; 
 
   /* height in pixels of an individual menu item */ 
   height = 10; 
 
   /* the first menu item determines the x coordinate for the other 
items */ 
   string_x = get_center(cmdtab[0].menu_item,cmdtab[0].x1,cmdtab[0].x2); 
 
   /* define the menu extremes */ 
   x1 = cmdtab[0].x1 - 1; 
   x2 = cmdtab[0].x2 + 3; 
   y1 = menu_bottom+1; 
   y2 = menu_bottom + n*height + 1; 
 
   /* define the associated horizontal mouse limits */ 
   left  = mouse_limits[index]; 
   right = mouse_limits[index+1] - 2; 
 
   /* display the vertical menu if necessary */ 
   fg_setpage(hidden); 
 
   /* draw the menu outline and the shadow around it */ 
   fg_mousevis(OFF); 
   fg_setcolor(white); 
   fg_box(x1,x2-2,y1,y2-1); 
 
   fg_setcolor(black); 
   fg_box(x1,x2-2,y1,y2); 
 
   /* set up list of options */ 
   ymax = menu_bottom; 
   for (i = 0; i < n; i++) 
   { 
      ymin = ymax + 1; 
      ymax = ymin + height-1; 
      fg_setcolor(white); 
      fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
      fg_setcolor(black); 
      put_string(cmdtab[i].menu_item,string_x,ymax-2); 
   } 
 
   /* highlight first or previously selected option */ 
   i = selection; 
   if (i >= n) i = 0; 
   ymin = menu_bottom + i*height; 
   ymax = ymin + height; 
   fg_setcolor(black); 
   fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
   fg_setcolor(white); 
   put_string(cmdtab[i].menu_item,string_x,ymax-2); 
 
   /* restore the menu to the visual page */ 
   fg_setpage(visual); 
   fg_restore(x1,x2,y1,y2+2); 
   fg_setpage(hidden); 
 
   /* clear the hidden page under the menu */ 
   fg_setcolor(blue); 
   fg_rect(x1,x2,y1,y2+2); 
 
   fg_setpage(visual); 
   fg_mousevis(ON); 
 
   /* choose an option */ 
   new = i; 
   fg_setnum(OFF); 
   flushkey(); 
 
   for(;;) 
   { 
      /* read a keystroke */ 
      fg_mousevis(ON); 
      fg_waitfor(1); 
      fg_intkey(&key,&aux); 
 
      /* if using a mouse, check its position */ 
      if (key+aux == 0) 
      { 
         fg_mousebut(1,&count,&xmouse,&ymouse); 
 
         if (count > 0) 
         { 
            if (BETWEEN(xmouse,x1,x2) && BETWEEN(ymouse,y1,y2-2)) 
            { 
               new = (ymouse - y1) / height; 
 
               /* check if this is the second click of a double-click */ 
               if (i == new) 
                  key = CR; 
            } 
            else if (!BETWEEN(xmouse,left,right)
                      && BETWEEN(ymouse,menu_top,y1-1)) 
            { 
               fg_mousevis(OFF); 
               fg_restore(0,xlimit,menu_bottom,ylimit); 
               selection = 0; 
               for (j = 0; j <= ITEMS; j++) 
               { 
                 if (BETWEEN(xmouse,mouse_limits[j],mouse_limits[j+1])) 
                    return(j); 
               } 
            } 
            else 
            { 
               fg_mousevis(OFF); 
               fg_restore(0,xlimit,menu_bottom,ylimit); 
               selection = 0; 
               return(ERR); 
            } 
         } 
      } 
 
      /* cycle through choices */ 
      if (aux == UP_ARROW || key == BS) 
      { 
         new = i-1; 
         if (new < 0) new = n-1; 
      } 
      else if (aux == DOWN_ARROW || key == SPACEBAR) 
      { 
         new = i+1; 
         if (new >= n) new = 0; 
      } 
      else if (aux == HOME || aux == PGUP) 
         new = 0; 
 
      else if (aux == END || aux == PGDN) 
         new = n - 1; 
 
      else if (aux == LEFT_ARROW || aux == RIGHT_ARROW) 
      { 
         fg_mousevis(OFF); 
         fg_restore(0,xlimit,menu_bottom,ylimit); 
         selection = 0; 
         return((int)aux); 
      } 
 
      /* pick one choice */ 
      else if (key == CR) 
      { 
#ifdef __TURBOC__ 
         cmdtab[i].menu_func(); 
#else 
         (*menu_func) = *cmdtab[i].menu_func; 
         menu_func(); 
#endif 
         wait_for_mouse_buttons(); 
         selection = i; 
         return(index); 
      } 
      else if (key == ESC) 
      { 
         selection = 0; 
         return(ESC); 
      } 
      else if (key+aux > 0) /* any other key */ 
      { 
         return(ERR); 
      } 
      if (i != new) 
      { 
         /* unmark previous option */ 
         ymin = menu_bottom + i*height; 
         ymax = ymin + height; 
         fg_mousevis(OFF); 
         fg_setcolor(white); 
         fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
         fg_setcolor(black); 
         put_string(cmdtab[i].menu_item,string_x,ymax-2); 
 
         /* mark new option */ 
         i = new; 
         ymin = menu_bottom + i*height; 
         ymax = ymin + height; 
         fg_setcolor(black); 
         fg_rect(cmdtab[i].x1,cmdtab[i].x2,ymin,ymax); 
         fg_setcolor(white); 
         put_string(cmdtab[i].menu_item,string_x,ymax-2); 
 
         /* move mouse cursor to the new option */ 
         fg_mousepos(&xmouse,&ymouse,&buttons); 
         if (BETWEEN(xmouse,x1,x2)) 
            fg_mousemov(xmouse,(ymin+ymax)/2); 
         fg_mousevis(ON); 
      } 
   } 
} 
/******************************************************************/ 
void wait_for_keystroke() 
{ 
   int buttons; 
   int count; 
   int x, y; 
   unsigned char key, aux; 
 
   flushkey(); 
   fg_mousebut(1,&count,&x,&y); 
   fg_mousebut(2,&count,&x,&y); 
 
   /* if the mouse is loaded, must loop and wait for button or keystroke */ 
   fg_mousevis(ON); 
   for(;;) 
   { 
      fg_waitfor(1); 
      fg_intkey(&key,&aux); 
      if (key+aux > 0) break; 
      fg_mousebut(1,&count,&x,&y); 
      if (count > 0) break; 
      fg_mousebut(2,&count,&x,&y); 
      if (count > 0) break; 
   } 
   do 
      fg_mousepos(&x,&y,&buttons); 
   while (buttons&3); 
   fg_mousevis(OFF); 
} 
/******************************************************************/ 
void wait_for_mouse_buttons() 
{ 
   int buttons; 
   int x, y; 
 
   do 
      fg_mousepos(&x,&y,&buttons); 
   while (buttons&3); 
} 
/******************************************************************/ 
int exit_program() 
{ 
   /* Called from menu. This would be a good place to prompt for 
      "save before quitting?" */ 
 
   quit_graphics(); 
   return(0); 
} 

The menu functions wait for keyboard and mouse input, highlight options, and launch functions in a very straightforward manner. The only tricky part is calling the PFI, which doesn't seem to work the same way in the Microsoft and Borland compilers--at least the compiler versions I have. To make the compilers happy, we launch the functions differently according to which compiler we are currently using:

 
#ifdef __TURBOC__ 
         cmdtab[i].menu_func(); 
#else 
         (*menu_func) = *cmdtab[i].menu_func; 
         menu_func(); 
#endif 
I am not completely satisfied with this approach; I would prefer to write portable ANSI C code. (And since the PFI's are defined in Kernigan and Richie, if they are not ANSI C, they should be.) I don't know why Microsoft and Borland don't agree on how to declare and execute a PFI. All you can do in a situation like this is find a way to work around the compilers to get a clean compile. If this presents a problem, I suggest you call the Borland and Microsoft technical support lines and bombard them with questions and suggestions.

A Look at the File Manager Code

The file manager lets you organize your data files and store them in a convenient data file, called GAME.DAT. It also loads the tiles, sprites, and level files as required for the current level. The file manager is pictured in Figure 9.2.

Figure 9.2 The file manager helps us to organize data files. Most of the file-management code is handled in the load_game_file() function in the MAIN.C file. The functions in MAIN.C are listed in Table 9.2.

Table 9.2 Functions in the MAIN.C File

FunctionDescription
main()It all starts here
edit_menu()Main controlling event loop; calls horizontal_menu()
draw_screen()Draws the game editor screen
load_game_file()Loads GAME.DAT and intializes variables
check_suffixes()Checks filenames for PCX, LEV, and so on
file_help_screen()Displays online help
level_to_array()Copies current level to a structure array
array_to_level()Copies data from structure array to working variables
show_level_names()Displays filenames on file manager screen

Like the menu code, the file-manager code is very straightforward. It lets the user move between fields and enter filenames using the get_string() function described in Chapter 8. The load_game_file() function is a bit long, but it is not at all tricky. The code is listed here:

 
load_game_file() 
{ 
   register int i,j; 
   int k; 
   char fname[13]; 
   unsigned char key,aux; 
   int error; 
   char string[50]; 
   static char *stringlist[] =   { 
      game_fname, 
      level_fname, 
      background_fname, 
      foreground_fname, 
      spritelist_fname 
   }; 
 
   static int y[] = {52,62,72, 92,112,132}; 
 
   /* copy the level information from the semi-permanent array to the 
      temporary working copy */ 
 
   array_to_level(current_level); 
   strcpy(fname,game_fname); 
 
   /* draw some rectangles */ 
   fg_mousevis(OFF); 
   fg_setcolor(blue); 
   fg_rect(0,319,25,199); 
 
   fg_setcolor(white); 
   fg_rect(48,271,40,170); 
 
   fg_setcolor(black); 
   fg_rect(0,319,24,24); 
   fg_box(48,271,40,170); 
 
   /* display the information */ 
   fg_setcolor(black); 
   put_string("Game file:",76,52); 
   put_string("level:",76,62); 
   put_string("background:",76,72); 
   put_string("foreground:",76, 92); 
   put_string("sprite list:",76,112); 
 
   fg_setcolor(blue); 
   put_string("F10:DONE",204,152); 
   put_string("F1:HELP",210,162); 
 
   /* display the current filenames */ 
   show_level_names(); 
 
   /* change the filenames */ 
   i = 0; 
   error = FALSE; 
   for(;;) 
   { 
      strcpy(string,stringlist[i]); 
      fg_setcolor(blue); 
      j = get_string(string,160,y[i],12,0,0); 
 
      /* abandon file editing */ 
      if (j == ESC) 
      { 
         error = TRUE; 
         break; 
      } 
 
      /* done */ 
      else if (j == F10) 
         break; 
 
      /* press F1 for help */ 
      else if (j == F1) 
         file_help_screen(); 
 
      else if (j == DOWN_ARROW || j == UP_ARROW || j == ENTER) 
      { 
         /* load a different game file */ 
         if (i == GAMEFILE  && strcmpi(string,game_fname)!=0) 
         { 
            strcpy(game_fname,string); 
            if ((tstream = fopen(game_fname,"rt")) != NULL) 
            { 
               fscanf(tstream,"%d",&nlevels); 
               for (k = 0; k < nlevels; k++) 
               { 
                  fscanf(tstream,"%s",level_fname); 
                  fscanf(tstream,"%s",background_fname); 
                  fscanf(tstream,"%s",backattr_fname); 
                  fscanf(tstream,"%s",foreground_fname); 
                  fscanf(tstream,"%s",foreattr_fname); 
                  fscanf(tstream,"%s",spritelist_fname); 
                  level_to_array(k); 
               } 
               fclose(tstream); 
               current_level = 0; 
            } 
         } 
         else 
         { 
            check_suffixes(string,i); 
            level_to_array(current_level); 
         } 
         show_level_names(); 
      }      /* edit the sprite list */ 
      if (j == F2) 
      { 
         fg_copypage(0,1); 
         load_sprites(); 
         fg_copypage(1,0); 
         fg_setpage(0); 
         fg_setcolor(white); 
         fg_rect(160,270,y[4]-8,y[4]); 
         fg_setcolor(black); 
         put_string(stringlist[4],160,y[4]); 
      } 
 
      /* next field */ 
      else if (j == DOWN_ARROW || j == TAB) 
      { 
         i++; 
         if (i > 4) i = 0; 
      } 
 
      /* previous field */ 
      else if (j == UP_ARROW) 
      { 
         i--; 
         if (i < 0) i = 4; 
      } 
 
      /* next level */ 
      else if (j == PGDN && current_level < MAXLEVELS-1) 
      { 
         level_to_array(current_level); 
         current_level++; 
         if (current_level >= nlevels) 
         { 
            fg_setcolor(white); 
            fg_rect(48,271,180,192); 
            fg_setcolor(black); 
            fg_box(48,271,180,192); 
            sprintf(string,"add level %d?",current_level); 
            center_string(string,48,271,188); 
            fg_getkey(&key,&aux); 
            fg_setcolor(blue); 
            fg_rect(48,271,180,192); 
 
            if ((key|32) == 'y') 
               nlevels++; 
            else 
               current_level--; 
         } 
         show_level_names(); 
      } 
 
      /* previous level */ 
      else if (j == PGUP && current_level > 0) 
      { 
         level_to_array(current_level); 
         current_level--; 
         show_level_names(); 
      } 
 
      /* insert a new level */ 
      else if (j == INSERT && current_level < MAXLEVELS-1) 
      { 
         fg_setcolor(white); 
         fg_rect(48,271,180,192); 
         fg_setcolor(black); 
         fg_box(48,271,180,192); 
         sprintf(string,"insert level %d?",current_level); 
         center_string(string,48,271,188); 
 
         fg_getkey(&key,&aux); 
         fg_setcolor(blue); 
         fg_rect(48,271,180,192); 
 
         if ((key|32) == 'y') 
            nlevels++; 
         else 
            continue; 
 
         for (k = nlevels-1; k > current_level+1; k--) 
         { 
            array_to_level(k-1); 
            level_to_array(k); 
         } 
         show_level_names(); 
      } 
 
      /* delete the current level */ 
      else if (j == DELETE && nlevels > 1) 
      { 
         fg_setcolor(white); 
         fg_rect(48,271,180,192); 
         fg_setcolor(black); 
         fg_box(48,271,180,192); 
         sprintf(string,"delete level %d?",current_level); 
         center_string(string,48,271,188); 
 
         fg_getkey(&key,&aux); 
         fg_setcolor(blue); 
         fg_rect(48,271,180,192); 
 
         if ((key|32) == 'y') 
            nlevels--; 
         else 
            continue; 
 
         if (current_level >= nlevels) 
            current_level--; 
         else 
         { 
            for (k = current_level; k < nlevels; k++) 
            { 
               array_to_level(k+1); 
               level_to_array(k); 
            } 
         } 
         show_level_names(); 
      } 
   } 
 
   /* if we have changed the filenames, save the information */ 
   if (!error) 
   { 
      /* update the array */ 
      level_to_array(current_level); 
 
      /* display new background and foreground tiles */ 
      init_tiles(); 
 
      /* write the game data out to a file */ 
      tstream = fopen(game_fname,"wt"); 
      if (tstream != NULL) 
      { 
         fprintf(tstream,"%d\n",nlevels); 
         for (i = 0; i< nlevels; i++) 
         { 
            array_to_level(i); 
            fprintf(tstream,"%s\n",level_fname); 
            fprintf(tstream,"%s\n",background_fname); 
            fprintf(tstream,"%s\n",backattr_fname); 
            fprintf(tstream,"%s\n",foreground_fname); 
            fprintf(tstream,"%s\n",foreattr_fname); 
            fprintf(tstream,"%s\n",spritelist_fname); 
         } 
         fclose(tstream); 
      } 
   } 
 
   /* now, put the current level back */ 
   array_to_level(current_level); 
 
   /* clear the dialog box and return */ 
   fg_setpage(0); 
   fg_setcolor(blue); 
   fg_rect(0,319,25,199); 
   return(0); 
} 

In general, I prefer to write shorter functions than this. I shortened this function considerably by extracting the filename display functions and putting them in a separate function, show_level_names(), but it is still a very long function.

Storing Level Data

Level data is stored in an array of structures. The current level is stored in temporary working variables, which are moved in and out of the structure array as needed. For example, when we use the PgUp key to select the previous level, the current level is copied into the level array, and the previous level is retrieved from the level array, like this:

 
/* previous level */ 
else if (j == PgUp && current_level > 0) 
{ 
   level_to_array(current_level); 
   current_level--; 
   show_level_names(); 
} 
The current_level variable is an integer value that tells us what level we are currently editing. Level numbers start at 0, so you if the current level is 0 you obviously can't edit the previous one. The levdef structure and the level array are declared as globals in the EDITDEFS.H file, and look like this:

 
/* max 6 levels per episode */ 
#define MAXLEVELS 6 
 
typedef struct levdef 
{ 
   char level_fname[13]; 
   char background_fname[13]; 
   char backattr_fname[13]; 
   char foreground_fname[13]; 
   char foreattr_fname[13]; 
   char sprite_fname[13]; 
}  LEVDEF; 
DECLARE LEVDEF far level[MAXLEVELS];

If you need more than six levels in your game, you can change the value of MAXLEVELS. The code to copy the level data from the level array into the temporary working variables looks like this:

 
void array_to_level(int n) 
{ 
   /* copy from the array into the temporary working variables */ 
 
   strcpy(level_fname,     level[n].level_fname); 
   strcpy(background_fname,level[n].background_fname); 
   strcpy(backattr_fname,  level[n].backattr_fname); 
   strcpy(foreground_fname,level[n].foreground_fname); 
   strcpy(foreattr_fname,  level[n].foreattr_fname); 
   strcpy(sprite_fname,level[n].spritelist_fname); 
} 

Similarly, the code to copy the current level data from temporary working variables into the level array looks like this:

 
void level_to_array(int n) 
{ 
   /* level information is stored in a semi-permanent RAM array, 
      and copied to some temporary working variables. This function 
      copies from the variables to the array. */ 
 
   strcpy(level[n].level_fname,     level_fname); 
   strcpy(level[n].background_fname,background_fname); 
   strcpy(level[n].backattr_fname,  backattr_fname); 
   strcpy(level[n].foreground_fname,foreground_fname); 
   strcpy(level[n].foreattr_fname,  foreattr_fname); 
   strcpy(level[n].sprite_fname,spritelist_fname); 
} 

The only reason we use temporary working variables in this context is to keep the code small and easy to read. For example, we will use the variable level_fname in many places throughout the game editor. I find it much easier to type and read a simple variable name than an element of a structure array. In other words, I find

level_fname

easier to deal with than:

level[current_level].level_fname

This form reflects of my personal preference, and as with other style preferences, you may change it if you wish.

Supporting the Tile Editor

Because the tile editor is similar to the sprite editor, a lot of our work has already been completed. As you can see from Figure 9.3, the tile editor has the same elements as the sprite editor:

In addition, the tile editor has a multiple tile area below the fat bit grid. This is so we can see how several tiles look when they fit together. This is especially useful when we are drawing floor tiles, walls, or bricks.

Figure 9.3 Elements of the tile editor. I am not going to describe tile editor functions in detail because they are so similar to the sprite editor functions. I will list them in Table 9.3, though, so you can see in general what the tile editor does.

Table 9.3 The Main Functions Used for the Tile Editor

FunctionDescription
activate_tile_editor()Main controlling event loop for tile editor
clear_tile()Sets all tile pixels to background color
do_background()Launches tile editor for background tiles
do_foreground()Launches tile editor for foreground tiles
draw_this_tile()Copies tiles to tile area and fat bit grid
draw_tile_editor()Draws the tile editor screen
fill_tile()Flood fills an area on the tile
get_attributes()Retrieves the tile attributes from the array
get_tile()Gets a tile from the tile library
horizontal_flip()Flips the tile around a vertical axis
import_tiles()Imports one or more tiles from a PCX file
init_tiles()Initializes the variables and arrays
rotate_tile()Rotates a tile 90 degrees clockwise
save_tiles()Writes the tile library and attributes to disk
set_attribute()Sets a tile attribute
set_attributes()Sets all eight tile attributes
set_background_color()Highlights the background color
set_bit()Sets a bit in a byte (for tile attributes)
set_foreground_color()Highlights the foreground color
set_grid()Draws a rectangle on the fat bit grid
test_bit()Gets a bit from a byte (for tile attributes)
tile_put()Puts a tile in the tile library
transpose_tile_colors()Sets all the background color pixels to foreground color
undo_tiles()Undoes the last editing command (toggle)
update_attributes()Copies the tile attributes to the array
update_old()Updates the undo information
update_tiles()Displays multiple tiles below fat bit grid
vertical_flip()Flips the tile around a horizontal axis

Notice the same functions are used to edit both the foreground and background tiles. To differentiate between the two tile types, we define a global integer variable called tile_type that can be defined as either FOREGROUND or BACKGROUND. This variable acts as a flag and is referred to frequently in the tile editor code. For example, in the get_tile() function

 
   if (tile_type == FOREGROUND) 
      page_no = 2; 
   else 
      page_no = 3; 

the foreground tiles are stored on page 2, the background tiles are stored on page 3, and the tile_type flag tells us which page is the proper one to flip to.

How Foreground Tiles Are Stored

Foreground tiles were designed to squeeze in around the edges of video memory, as we will see in Chapter 10. In order to make them fit, we must display them as two columns of fourteen tiles each, as shown in Figure 9.4.

Figure 9.4 How foreground tiles are stored in the game.

However, to edit foreground tiles in the tile editor, it is more convenient to display them in four columns of seven tiles each, as shown in figure 9.5.

Figure 9.5 How foreground tiles are stored in the tile editor.

Supporting Bitmapped Characters

There are many ways to incorporate a bitmapped character font into a game or utility. One convenient method is to use Fastgraph/Fonts, which provides 40 bitmapped fonts and the code to display them quickly. I found it convenient to generate my own font for the game editor, however. Our font needs are simple, and a small, simple font can easily be generated and compiled into a program like this.

All the code for displaying characters and strings is in the file CHAR.C. Table 9.4 lists the functions in CHAR.C.

Table 9.4 Functions in the CHAR.C File

FunctionDescription
center_string()Displays a string centered around a point
erase_char()Erases a character
get_center()Calculates the center position of a string
put_char()Displays one character
put_cursor()Displays a cursor
get_string()Gets user input in string format
put_string()Displays a character string

The contents of the file CHAR.C is shown here:

 
/******************************************************************\ 
*  char.c -- game editor source code                               * 
*  copyright 1994 Diana Gruber                                     * 
*  compile using large model, link with Fastgraph (tm)             * 
\******************************************************************/ 
 
#include "editdefs.h" 
#include "font5.h"   /* bitmap data for 5x5 font */ 
/******************************************************************/ 
void center_string(char *string,int x1,int x2,int y) 
{ 
   /* center a string between x1 and x2 */ 
 
   register int nchar,x; 
 
   nchar = strlen(string); 
   x = ((x1 + x2) / 2) - nchar*3; 
   put_string(string,x,y); 
} 
/******************************************************************/ 
void erase_char(int x,int y) 
{ 
   /* erase a character (when doing character input) */ 
   register int color; 
 
   color = fg_getcolor(); 
   fg_setcolor(white); 
   fg_rect(x,x+5,y-5,y); 
   fg_setcolor(color); 
} 
/**********************************************************************/ 
get_center(char *string,int x1,int x2) 
{ 
   return(((x1 + x2) / 2) - strlen(string)*3); 
} 
/******************************************************************/ 
void put_char(unsigned char key,int x,int y) 
{ 
   /* just put one character */ 
 
   int index; 
 
   index = (char)(key-33) * 5; 
   fg_move(x,y); 
   fg_drawmap(&font5[index],1,5); 
} 
/******************************************************************/ 
void put_cursor(int x,int y,int cursor_color) 
{ 
   /* the text cursor is just a little rectangle */ 
   register int color; 
 
   color = fg_getcolor(); 
   fg_setcolor(cursor_color); 
   fg_rect(x,x+5,y,y); 
   fg_setcolor(color); 
} 
/******************************************************************/ 
get_string(char *string,int x,int y,int max_length, 
          unsigned char key,unsigned char aux) 
{ 
   register int i; 
   int color; 
   int cursor_timer; 
   int foreground; 
   int background; 
   int xmax, ymin; 
   int first; 
   first = TRUE; 
 
   foreground = fg_getcolor(); 
   background = white; 
 
   xmax = x + 6*max_length; 
   ymin = y - 6; 
 
   i = 0; 
   cursor_timer = 16; 
   color = foreground; 
   fg_setcolor(foreground); 
 
   for (;;) 
   { 
      cursor_timer--; 
      if (cursor_timer == 8) 
         color = background; 
      else if (cursor_timer == 0) 
      { 
         cursor_timer = 16; 
         color = foreground; 
      } 
      if (i < max_length) put_cursor(x,y+1,color); 
      if (key+aux > 0) 
         if (i < max_length) put_cursor(x,y+1,background); 
 
      if (i == 0 && islower(key)) key ^= 32; 
 
      /* printable character or Spacebar */ 
      if ((isalnum(key) || key == SPACE || ispunct(key)) && i < max_length) 
      { 
         if (first) 
         { 
            string[i] = '\0'; 
            fg_setcolor(background); 
            fg_rect(x-2,xmax+1,ymin,y+1); 
            first = FALSE; 
            fg_setcolor(foreground); 
         } 
 
         put_cursor(x,y+1,background); 
         put_char(key,x,y); 
         x += 6; 
         string[i++] = key; 
         string[i] = '\0'; 
      } 
 
      /* Backspace deletes previous character */ 
      else if (key == BS && i > 0) 
      { 
         if (i < max_length) put_cursor(x,y+1,background); 
         x -= 6; 
         erase_char(x,y); 
         i--; 
         string[i] = '\0'; 
      } 
 
      /* done entering string */ 
      else if (key == ESC || key == ENTER || key == TAB || aux > 0) 
      { 
         if (i < max_length) put_cursor(x,y+1,background); 
         return(key+aux); 
      } 
 
      fg_waitfor(1); 
      fg_intkey(&key,&aux); 
   } 
} 
/******************************************************************/ 
void put_string(unsigned char *string,int ix,int iy) 
{ 
   /* draw the letters one at a time as bitmaps */ 
   register int i; 
   int index, nchar; 
   char ch; 
   nchar = strlen(string); 
 
   for (i = 0; i < nchar; i++) 
   { 
      ch = (char)(string[i]-33); 
      if (ch >= 0) 
      { 
         index = ch*5; 
 
         /* move to the x,y location */ 
         fg_move(ix,iy); 
 
         /* display one letter */ 
         fg_drawmap(&font5[index],1,5); 
      } 
      ix += 6; 
   } 
} 

The code to display a single character or a string of characters is fairly straightforward. A character is stored in a mode-independent bitmap, and Fastgraph's fg_drawmap() function is used to display the bitmap.

Tommy's Tips: The Proper Bitmap Image for Fonts

The mode-independent bitmap format is well suited for character fonts because images can be displayed in any color, and look about the same in any video mode. Bits in each byte are either on (displayed in current color) or off (transparent)-- which is the usually the way we want a font to work.

The data for the 5x5 bitmapped font is included in a header file, FONT5.H, and compiled into the executable program. So the font data is not read from a separate file at runtime.

The MAKEFONT Program

You may be wondering where the FONT5.H file came from originally. Here is the answer: I drew the font in a paint program, stored the image in a PCX file, and then wrote a utility program to capture the font. This is an interesting process--writing a utility program to generate source code which is used in another utility program which is used to process artwork used in a game. We are getting several layers deep into utilities now.

In case you are interested, here is the code I wrote to display the PCX file and capture the font:

 
/******************************************************************\ 
*  MAKEFONT.C -- source code to turn a PCX file into a header file * 
*                containing 5x5 font data.                         * 
*  copyright 1994 Diana Gruber                                     * 
*  compile using large model, link with Fastgraph (tm)             * 
\******************************************************************/ 
 
#include <stdio.h> 
#include <stdlib.h> 
#include <Fastgraf.h> 
 
FILE *stream; 
unsigned char font5[480]; 
 
void main() 
{ 
   unsigned char i,j; 
   int x,y; 
   int index; 
 
   /* set the video mode to 320x200x256 VGA */ 
   fg_setmode(19); 
 
   /* display the PCX file */ 
   fg_showpcx("font5.pcx",0); 
 
   /* open the header file */ 
   stream = fopen("font5.h","wt"); 
 
   /* write the array declaration in the header file */ 
   fprintf(stream,"static unsigned char font5[] = {\n"); 
 
   /* get the characters */ 
   fg_setcolor(15); 
   index = 0; 
   x = 16; 
   y = 16; 
 
   /* move down the columns */ 
   for (i = 33; i <= 126; i++) 
   { 
      fg_move(x,y); 
      fg_getmap(&font5[index],1,5); 
      for (j = 0; j<5; j++) 
      { 
        fprintf(stream," 0x%4.4X,",font5[index++]); 
      } 
      fprintf(stream,"  /* %c */",i); 
      fprintf(stream,"\n"); 
      y+= 16; 
 
      /* end of column, go to next column */ 
      if (y > 199) 
      { 
         y = 16; 
         x += 16; 
      } 
   } 
 
   fprintf(stream,"};\n\n"); 
   fclose(stream); 
 
   fg_waitkey(); 
   fg_setmode(3); 
   fg_reset(); 
   exit(0); 
} 

The MAKEFONT program created the FONT5.H file, which looks like this:

 
static unsigned char font5[] = { 
 0x0020, 0x0000, 0x0020, 0x0020, 0x0020,  /* ! */ 
 0x0000, 0x0000, 0x0000, 0x0050, 0x0050,  /* " */ 
 0x0050, 0x00F8, 0x0050, 0x00F8, 0x0050,  /* # */ 
 0x0020, 0x0070, 0x0020, 0x0070, 0x0020,  /* $ */ 
 0x0088, 0x0040, 0x0020, 0x0010, 0x0088,  /* % */ 
 0x0030, 0x0058, 0x0020, 0x0050, 0x0020,  /* & */ 
 0x0000, 0x0000, 0x0000, 0x0020, 0x0020,  /* ' */ 
 0x0020, 0x0040, 0x0040, 0x0040, 0x0020,  /* ( */ 
 0x0020, 0x0010, 0x0010, 0x0010, 0x0020,  /* ) */ 
 0x0088, 0x0050, 0x00A8, 0x0050, 0x0088,  /* * */ 
 0x0020, 0x0020, 0x00F8, 0x0020, 0x0020,  /* + */ 
 0x0080, 0x0040, 0x0000, 0x0000, 0x0000,  /* , */ 
 0x0000, 0x0000, 0x0070, 0x0000, 0x0000,  /* - */ 
 0x0040, 0x0000, 0x0000, 0x0000, 0x0000,  /* . */ 
 0x0080, 0x0040, 0x0020, 0x0010, 0x0008,  /* / */ 
 0x0070, 0x0088, 0x0088, 0x0088, 0x0070,  /* 0 */ 
 0x0070, 0x0020, 0x0020, 0x0060, 0x0020,  /* 1 */ 
 0x00F0, 0x0040, 0x0020, 0x0090, 0x0060,  /* 2 */ 
 0x00E0, 0x0010, 0x0060, 0x0010, 0x00E0,  /* 3 */ 
 0x0010, 0x0010, 0x00F0, 0x0090, 0x0090,  /* 4 */ 
 0x0070, 0x0008, 0x00F0, 0x0080, 0x00F8,  /* 5 */ 
 0x0070, 0x0088, 0x00F0, 0x0080, 0x0070,  /* 6 */ 
 0x0040, 0x0040, 0x0020, 0x0010, 0x00F8,  /* 7 */ 
 0x0070, 0x0088, 0x0070, 0x0088, 0x0070,  /* 8 */ 
 0x0010, 0x0008, 0x0078, 0x0088, 0x0070,  /* 9 */ 
 0x0040, 0x0000, 0x0000, 0x0040, 0x0000,  /* : */ 
 0x0040, 0x0020, 0x0000, 0x0020, 0x0000,  /* ; */ 
 0x0010, 0x0020, 0x0040, 0x0020, 0x0010,  /* < */ 
 0x0000, 0x0070, 0x0000, 0x0070, 0x0000,  /* = */ 
 0x0040, 0x0020, 0x0010, 0x0020, 0x0040,  /* > */ 
 0x0010, 0x0000, 0x0010, 0x0048, 0x0030,  /* ? */ 
 0x0070, 0x0080, 0x00B0, 0x00B0, 0x0060,  /* @ */ 
 0x0088, 0x00F8, 0x0088, 0x0050, 0x0020,  /* A */ 
 0x00F0, 0x0088, 0x00F0, 0x0088, 0x00F0,  /* B */ 
 0x0078, 0x0080, 0x0080, 0x0080, 0x0078,  /* C */ 
 0x00F0, 0x0088, 0x0088, 0x0088, 0x00F0,  /* D */ 
 0x00F8, 0x0080, 0x00F0, 0x0080, 0x00F8,  /* E */ 
 0x0080, 0x0080, 0x00F0, 0x0080, 0x00F0,  /* F */ 
 0x0070, 0x0088, 0x0098, 0x0080, 0x0078,  /* G */ 
 0x0088, 0x0088, 0x00F8, 0x0088, 0x0088,  /* H */ 
 0x0070, 0x0020, 0x0020, 0x0020, 0x0070,  /* I */ 
 0x0060, 0x0090, 0x0010, 0x0010, 0x0038,  /* J */ 
 0x0088, 0x0090, 0x00E0, 0x0090, 0x0088,  /* K */ 
 0x00F8, 0x0080, 0x0080, 0x0080, 0x0080,  /* L */ 
 0x0088, 0x00A8, 0x00A8, 0x00D8, 0x0088,  /* M */ 
 0x0088, 0x0098, 0x00A8, 0x00C8, 0x0088,  /* N */ 
 0x0070, 0x0088, 0x0088, 0x0088, 0x0070,  /* O */ 
 0x0080, 0x0080, 0x00F0, 0x0088, 0x00F0,  /* P */ 
 0x0078, 0x00A8, 0x0088, 0x0088, 0x0070,  /* Q */ 
 0x0090, 0x00A0, 0x00F0, 0x0088, 0x00F0,  /* R */ 
 0x00F0, 0x0008, 0x0070, 0x0080, 0x0078,  /* S */ 
 0x0020, 0x0020, 0x0020, 0x0020, 0x00F8,  /* T */ 
 0x0070, 0x0088, 0x0088, 0x0088, 0x0088,  /* U */ 
 0x0020, 0x0050, 0x0088, 0x0088, 0x0088,  /* V */ 
 0x0088, 0x00D8, 0x00A8, 0x00A8, 0x0088,  /* W */ 
 0x0088, 0x0050, 0x0020, 0x0050, 0x0088,  /* X */ 
 0x0020, 0x0020, 0x0020, 0x0050, 0x0088,  /* Y */ 
 0x00F8, 0x0040, 0x0020, 0x0010, 0x00F8,  /* Z */ 
 0x0070, 0x0040, 0x0040, 0x0040, 0x0070,  /* [ */ 
 0x0008, 0x0010, 0x0020, 0x0040, 0x0080,  /* \ */ 
 0x0070, 0x0010, 0x0010, 0x0010, 0x0070,  /* ] */ 
 0x0000, 0x0000, 0x0088, 0x0050, 0x0020,  /* ^ */ 
 0x00F8, 0x0000, 0x0000, 0x0000, 0x0000,  /* _ */ 
 0x0000, 0x0000, 0x0000, 0x0020, 0x0020,  /* ` */ 
 0x0088, 0x00F8, 0x0088, 0x0050, 0x0020,  /* a */ 
 0x00F0, 0x0088, 0x00F0, 0x0088, 0x00F0,  /* b */ 
 0x0078, 0x0080, 0x0080, 0x0080, 0x0078,  /* c */ 
 0x00F0, 0x0088, 0x0088, 0x0088, 0x00F0,  /* d */ 
 0x00F8, 0x0080, 0x00F0, 0x0080, 0x00F8,  /* e */ 
 0x0080, 0x0080, 0x00F0, 0x0080, 0x00F0,  /* f */ 
 0x0070, 0x0088, 0x0098, 0x0080, 0x0078,  /* g */ 
 0x0088, 0x0088, 0x00F8, 0x0088, 0x0088,  /* h */ 
 0x0070, 0x0020, 0x0020, 0x0020, 0x0070,  /* i */ 
 0x0060, 0x0090, 0x0010, 0x0010, 0x0038,  /* j */ 
 0x0088, 0x0090, 0x00E0, 0x0090, 0x0088,  /* k */ 
 0x00F8, 0x0080, 0x0080, 0x0080, 0x0080,  /* l */ 
 0x0088, 0x00A8, 0x00A8, 0x00D8, 0x0088,  /* m */ 
 0x0088, 0x0098, 0x00A8, 0x00C8, 0x0088,  /* n */ 
 0x0070, 0x0088, 0x0088, 0x0088, 0x0070,  /* o */ 
 0x0080, 0x0080, 0x00F0, 0x0088, 0x00F0,  /* p */ 
 0x0078, 0x00A8, 0x0088, 0x0088, 0x0070,  /* q */ 
 0x0090, 0x00A0, 0x00F0, 0x0088, 0x00F0,  /* r */ 
 0x00F0, 0x0008, 0x0070, 0x0080, 0x0078,  /* s */ 
 0x0020, 0x0020, 0x0020, 0x0020, 0x00F8,  /* t */ 
 0x0070, 0x0088, 0x0088, 0x0088, 0x0088,  /* u */ 
 0x0020, 0x0050, 0x0088, 0x0088, 0x0088,  /* v */ 
 0x0088, 0x00D8, 0x00A8, 0x00A8, 0x0088,  /* w */ 
 0x0088, 0x0050, 0x0020, 0x0050, 0x0088,  /* x */ 
 0x0020, 0x0020, 0x0020, 0x0050, 0x0088,  /* y */ 
 0x00F8, 0x0040, 0x0020, 0x0010, 0x00F8,  /* z */ 
 0x0030, 0x0020, 0x0060, 0x0020, 0x0030,  /* { */ 
 0x0020, 0x0020, 0x0000, 0x0020, 0x0020,  /* | */ 
 0x0060, 0x0020, 0x0030, 0x0020, 0x0060,  /* } */ 
 0x0000, 0x0000, 0x0090, 0x0068, 0x0000,  /* ~ */ 
}; 

As you can see, the MAKEFONT program generated nicely formatted and commented source code, just the way we want it.

This concludes our discussion of the game editor program and the various utilities we use in game design, and the strategies we used to create those utilities. By now you should have a good understanding of how to program in a Mode X graphics mode using Fastgraph, and the types of data and images we will be working with. In the next chapter, we will get to the good stuff--we will take our first look at the source code for the Tommy's Adventures game.

Next Chapter

Diana Gruber's 3D Casino Las Vegas

_______________________________________________

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.