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.
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
Function | Description |
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.
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.
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(); #endifI 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.
When writing code for multiple compilers, you can use the __TURBOC__ symbolic name to direct the compiler to only compile a part of the code. The Borland C/C++ and Turbo C/C++ compilers recognize this symbolic constant, and the other C compilers do not. We usually try to avoid doing this, but sometimes we can't help it.
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
Function | Description |
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.
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.
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
Function | Description |
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.
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.
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
Function | Description |
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.
The fg_drawmap() function displays an image stored as a mode- independent bitmap. The image will be positioned so that its lower-left corner is at the graphics cursor position.
fg_drawmap(char *map_array, int width, int height);*map_array is the arbitrary length array containing the bitmap. Each byte of map_array represents eight pixels. Bits that are set to 1 result in the corresponding pixel being displayed in the current color. Bits that are set to 0 leave the corresponding pixel unchanged.
*width is the width in bytes of the bitmap.
*height is the height in bytes (pixel rows) of 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.
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.
Cover |
Contents |
Downloads
Fastgraph Home Page |
books |
Magazine Reprints
Copyright © 1998 Ted Gruber Software Inc. All Rights Reserved.
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
So you want to be a Computer Game Developer