Action Arcade Adventure Set
Diana Gruber

Chapter 14
It's Show Time


Ready to put it all together? Find out how we combine all the pieces we've discussed so far into a real live game.

Now that we've assembled all of the ingredients we need for fast, side-scrolling-style animation, it's time to put them all together. We have built our game editor, and then we have used it to build our sprites and our levels. We have designed our data structures and action functions. Now it's time to look at the main controlling functions for our game. These functions are found in the file TOMMY.C. In this chapter, I'll present this source code file so you can see how the game is loaded and how the other functions are called. We'll also look at the details of completing a frame of animation.

Lights . . . Camera . . . Action!

Every C program starts with a main() function and ours is no exception. The main() function, along with the other useful functions for game initialization and termination, are found in the file TOMMY.C. Let's examine the functions, listed in Table 14.1, found in this file.

Table 14.1 Functions Defined in TOMMY.C

FunctionDescription
main()Initializes the game by loading in the files and starting the main event loop
activate_level()Provides the main event loop to control the game and it calls the action functions for sprite animation
apply_sprite()Draws a bitmapped sprite and updates the layout array
array_to_level()Loads level data from far array into variables
fix_palettes()Adjusts the first 32 colors to match sprite colors
flushkey()Clears out the keystroke buffer
getseed()Obtains a seed for the random number generator
increment_timer()Increments the game timer
init_graphics()Initializes the graphics for the game
irandom()Generates a random number
level_to_array()Copies level data from a specified location in in layout array to the level variables
load_sprite()Loads sprite data from the sprite list file
terminate_game()Shuts down the game by reseting the video mode and clock speed, and returning to DOS

TOMMY.C

The source code for TOMMY.C is bigger than it looks! That's because ACTION.C is included. Since we already listed ACTION.C in the last chapter, let's look at the rest of the file now.

 
/******************************************************************\ 
*  Tommy.c -- Tommy game source code file                          * 
*  copyright 1994 Diana Gruber                                     * 
*  compile using large model, link with Fastgraph (tm)             * 
\******************************************************************/ 
 
#define tommy_c 
#include "gamedefs.h" 
#include "action.c" 
 
/* #define debug */ 
/******************************************************************/ 
void main() 
{ 
   register int i; 
   char *bitmap; 
   SPRITE *score_sprite; 
   OBJp node; 
   OBJp next_node; 
 
   if (fg_testmode(20,4) == 0)        /* VGA or better required */ 
   { 
      printf("\nVGA required\n"); 
      exit(0); 
   } 
 
#ifdef debug 
   /* text file used for debugging purposes */ 
   dstream = fopen("debug.dat","wt"); 
#endif 
 
#ifdef __TURBOC__ 
   oldhandler = getvect(0x1C);         /* get the vector for 0X1C */ 
   setvect(0x1C,increment_timer);      /* set timer interrupt function */ 
#else 
   oldhandler = _dos_getvect(0x1C);    /* get the vector for 0X1C */ 
   _dos_setvect(0x1C,increment_timer); /* set timer interrupt function */ 
#endif 
 
   /* open the game data file */ 
   if ((stream = fopen("game.dat","rt")) == NULL) 
   { 
      sprintf(abort_string,"Bad or missing file: Game.dat."); 
      terminate_game(); 
   } 
 
   /* read all the file names, store in structures */ 
   fscanf(stream,"%d",&nlevels); 
   for (i = 0; i < nlevels; i++) 
   { 
      fscanf(stream,"%s",level_fname); 
      fscanf(stream,"%s",background_fname); 
      fscanf(stream,"%s",backattr_fname); 
      fscanf(stream,"%s",foreground_fname); 
      fscanf(stream,"%s",foreattr_fname); 
      fscanf(stream,"%s",sprite_fname); 
      level_to_array(i); 
   } 
   fclose(stream); 
 
   current_level = 0;               /* start with the first level */ 
   array_to_level(current_level);   /* get level data from array */ 
 
   init_graphics();                 /* initialize the VGA graphics */ 
   fg_showpcx("tommy.pcx",0);       /* display intro screen */ 
   fg_waitkey();                    /* wait for a keystroke */ 
   fg_setcolor(0);                  /* clear the screen */ 
   fg_rect(0,351,0,479); 
   load_level();                    /* load the level data */ 
   load_sprite();                   /* load the sprite data */ 
   load_status_screen();            /* load the status screen data */ 
 
   player = (OBJp)malloc(sizeof(OBJ));  /* allocate the player */ 
   player->tile_xmin = 4; 
   player->tile_xmax = 14; 
   player->tile_ymin = 5; 
   player->tile_ymax = 11; 
 
   /* initialize the score sprite */ 
   if ((bitmap = (char *)malloc(160*42)) == (char *) NULL) 
   { 
       sprintf(abort_string,"Out of sprite memory."); 
       terminate_game(); 
   } 
 
   if ((score_sprite = (SPRITE *)malloc(sizeof(SPRITE))) 
      == (SPRITE *)NULL) 
   { 
       sprintf(abort_string,"Out of sprite memory."); 
       terminate_game(); 
   } 
   score_sprite->bitmap  = bitmap; 
   score_sprite->width   = 160; 
   score_sprite->height  = 42; 
   score_sprite->xoffset = 0; 
   score_sprite->yoffset = 0; 
 
   /* initialize the score object */ 
   score = (OBJp)malloc(sizeof(OBJ)); 
   score->sprite = score_sprite; 
   score->action = update_score; 
   score->direction = RIGHT; 
 
   /* start the linked list */ 
   bottom_node = (OBJp)NULL; 
   top_node = (OBJp)NULL; 
   next_node = (OBJp)NULL; 
 
   /* initialize some global variables */ 
   max_time = 500L; 
   player_score = 0L; 
   show_score = TRUE; 
 
   fg_tcmask(1);                       /* mask for fg_tcxfer */ 
 
   fg_kbinit(1);                       /* start the keyboard handler */ 
   game_time = 0;                      /* start the timer */ 
   set_rate(8);                        /* speed up the clock rate */ 
   score->action(score);               /* start the score */ 
 
   for (;;)                            /* main program loop */ 
   { 
      /* initialize some global variables */ 
      nbullets = 0; 
      nenemies = 0; 
      player_blink = FALSE; 
      nblinks = 0; 
      nhits = 0; 
      nlives = 3; 
      blink_time = 0; 
      kicking = FALSE; 
 
      warp(16,80);                     /* warp to starting position */ 
      swap();                          /* swap pages */ 
      page_copy(vpo);                  /* copy visual page to hidden */ 
 
      player->x = 32;                  /* initialize player */ 
      player->y = 200; 
      player->frame = 0; 
      player->time = 0; 
      player->xspeed = 0; 
      player->yspeed = 0; 
      player->sprite = tom_stand[0]; 
      player->direction = RIGHT; 
      player->action = player_run; 
 
      launch_enemy(120,120,0);         /* launch some enemies */ 
      launch_enemy(900,120,1); 
 
      warp_to_next_level = FALSE; 
      do                               /* continuous loop */ 
      {                                /* activate the level */ 
         activate_level(); 
         if (warp_to_next_level) 
            break; 
      } 
      while(!status_screen());         /* check for program exit */ 
 
      if (!warp_to_next_level)         /* done -- exit */ 
      { 
         abort_string[0] = '\n'; 
         terminate_game(); 
      } 
 
      for (node=bottom_node; node!=(OBJp)NULL; node=next_node) 
      { 
          next_node = node->next; 
          kill_object(node); 
      } 
      current_level++;                 /* do the next level */ 
      if (current_level == nlevels) 
         current_level = 0; 
      array_to_level(current_level); 
 
      fg_setcolor(0);                  /* clear the screen */ 
      fg_rect(0,351,0,479); 
      set_rate(0);                     /* set the clock rate to normal */ 
      load_level();                    /* load the level */ 
      set_rate(8);                     /* reset the clock rate */ 
   } 
} 
/******************************************************************/ 
void activate_level() 
{ 
   register int i,j; 
   unsigned long time; 
   OBJp node; /* index for linked list */ 
   OBJp next_node; /* pointer to next node */ 
 
   next_node = (OBJp)NULL; 
   for(;;)  /* loop continuously */ 
   { 
      /* determine how much time has passed since the last frame */ 
      time = game_time; 
      delta_time = time - last_time; 
      delta_time = MAX(1L,delta_time); 
      last_time = time; 
 
      if (fg_kbtest(KB_ESC))  /* check for escape key */ 
         return; 
 
      if (fg_kbtest(KB_F1))   /* check for the score board toggle */ 
         show_score = TRUE; 
      else if (fg_kbtest(KB_F2)) 
         show_score = FALSE; 
      else if (fg_kbtest(KB_W)) 
      { 
         warp_to_next_level = TRUE; 
         fg_waitfor(10); 
         return; 
      } 
 
      /* do the action function for the player */ 
      player->action(player); 
 
      /* if any scrolling occurred, adjust the screen & arrays */ 
      page_fix(); 
 
      /* do the action functions for all the sprites */ 
      for (node=bottom_node; node!=(OBJp)NULL; node=next_node) 
      { 
         next_node = node->next; 
         node->action(node);        /* do the action function */ 
      } 
 
      /* do the action function for the score */ 
      score->action(score); 
 
      /* rebuild all the background tiles on the hidden page */ 
      rebuild_background(); 
      rebuild_foreground(); 
      for (i = 0; i < 22; i++) 
         for (j = 0; j < 15; j++) 
            layout[hidden][i][j] = FALSE; 
 
      /* apply all the sprites on the hidden page */ 
      for (node=bottom_node; node!=(OBJp)NULL; node=node->next) 
         apply_sprite(node); 
 
      /* apply the player sprite */ 
      apply_sprite(player); 
 
      /* apply any foreground tiles */ 
      rebuild_foreground(); 
 
      /* if the scoreboard is visible, put it on last */ 
      if (show_score) 
         apply_sprite(score); 
 
      /* swap the pages */ 
      swap(); 
 
      /* if the page has scrolled, copy the visual page to the 
         hidden page */ 
 
      if (scrolled_left || scrolled_right || scrolled_up || 
          scrolled_down || warped) 
         page_copy(vpo); 
 
      /* reset all the scrolling globals to false */ 
      scrolled_left = FALSE; 
      scrolled_right = FALSE; 
      scrolled_up = FALSE; 
      scrolled_down = FALSE; 
      warped = FALSE; 
   } 
} 
/******************************************************************/ 
void apply_sprite(OBJp objp) 
{ 
   register int i,j; 
   int x,y; 
   int tile_x1,tile_y2; 
   int tile_x2,tile_y1; 
   int width, height; 
   char *p; 
 
   /* calculate the location, width and height */ 
   x = objp->x + objp->sprite->xoffset; 
   y = objp->y + objp->sprite->yoffset; 
   width = objp->sprite->width; 
   height = objp->sprite->height; 
 
   /* which tiles are going to be covered up? */ 
   tile_x1 = x/16 - tile_orgx; 
   tile_y2 = y/16 - tile_orgy; 
   tile_x2 = (x+width)/16 - tile_orgx; 
   tile_y1 = (y-height)/16 - tile_orgy; 
 
   /* if we are off the screen, forget it */ 
   if (tile_x2 < 0 || tile_x1 > 21 || tile_y1 > 14 || tile_y2 < 0) 
      return; 
 
   tile_x1 = MAX(tile_x1,0); 
   tile_x2 = MIN(21,tile_x2); 
   tile_y1 = MAX(tile_y1,0); 
   tile_y2 = MIN(14,tile_y2); 
 
   /* update the layout array */ 
   for (i = tile_x1; i <= tile_x2; i++) 
   { 
      p = layout[hidden][i] + tile_y1; 
      for (j = tile_y1; j <= tile_y2; j++) 
      { 
         *p++ = TRUE; 
      } 
   } 
 
   /* convert world space coordinates to screen space */ 
   x = x - (tile_orgx*16); 
   y = y - (tile_orgy*16) + hpo; 
 
   /* set the clipping limits */ 
   fg_setclip(0,351,hpo,hpb); 
   fg_move(x,y); 
 
   /* if the player is blinking, alternate black and regular */ 
   if (objp == player && player_blink) 
   { 
      blink_time += delta_time; 
      if (blink_time > 5) 
      { 
         blink_time = 0; 
         nblinks++; 
         if (nblinks == 30) 
         { 
           player_blink = FALSE; 
           nblinks = 0; 
           blink_time = 0; 
         } 
      } 
      if (nblinks%2 == 0) 
      { 
         get_blinkmap(objp); 
         if (objp->direction == RIGHT) 
             fg_clpimage(blink_map,width,height); 
         else 
             fg_flpimage(blink_map,width,height); 
         fg_setclip(0,351,0,726); 
         return; 
      } 
   } 
 
   /* not blinking, just display the bitmap */ 
   if (objp->direction == RIGHT) 
      fg_clpimage(objp->sprite->bitmap,width,height); 
   else 
      fg_flpimage(objp->sprite->bitmap,width,height); 
 
   fg_setclip(0,351,0,726); 
} 
/******************************************************************/ 
void array_to_level(int n) 
{ 
   /* update the current level */ 
   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].sprite_fname); 
} 
/******************************************************************/ 
void fix_palettes() 
{ 
   /* the first 32 palettes are fixed sprite colors */ 
   static char game_palette[] = { 
    0, 0, 0, 18, 7, 0, 27,13, 3, 36,21,10, 45,31,19, 54,42,32, 63,55,47, 
    0, 0, 0, 14,14,14, 21,21,21, 28,28,28, 35,35,35, 42,42,42, 49,49,49, 
   56,56,56, 63,63,63,  0, 0,42,  8, 8,52, 21,21,63, 21,37,61, 21,53,60, 
   36, 0, 0, 45, 0, 0, 54, 0, 0, 63, 0, 0, 56,44,47,  0,35, 0,  0,57, 0, 
   21,63, 0, 63,63, 0, 51, 0,51, 63, 0,63}; 
 
   register int i; 
   int color; 
   int white_value,black_value; 
   int blue_value; 
   int distance; 
 
   /* set the palettes for the first 32 colors (sprite colors) */ 
   fg_setdacs(0,32,game_palette); 
 
   /* find the closest colors to white, black and blue. */ 
   white_value = 0; 
   black_value = 63*63; 
   white = 15; 
   black = 0; 
   blue_value = 63*63*3; 
 
   for (i = 0; i < 32*3; i+=3) 
   { 
      color = game_palette[i]+game_palette[i+1]+game_palette[i+2]; 
      if (color > white_value) /* biggest total color is white */ 
      { 
         white = i/3; 
         white_value = color; 
      } 
      if (color < black_value) /* smallest total color is black */ 
      { 
         black = i/3; 
         black_value = color; 
      } 
      /* find closest blue color using least squares method */ 
      distance = 
         (63 - game_palette[i+2]) * (63 - game_palette[i+2]) + 
         (21 - game_palette[i+1]) * (21 - game_palette[i+1]) + 
         (21 - game_palette[i]) * (21 - game_palette[i]); 
 
      if (distance < blue_value) 
      { 
         blue = i/3; 
         blue_value = distance; 
      } 
   } 
} 
/******************************************************************/ 
void flushkey() 
{ 
   unsigned char key,aux; 
 
   /* clear out the keystroke buffer */ 
   do { 
         fg_intkey(&key,&aux); 
      } 
      while (key+aux > 0); 
} 
/******************************************************************/ 
void getseed() 
{ 
   /* get a seed for the random number generator */ 
   seed = (int)(fg_getclock() & 0x7FFF); 
} 
/*****************************************************************/ 
void interrupt increment_timer() 
{ 
   game_time++; 
} 
/******************************************************************/ 
void init_graphics() 
{ 
   fg_setmode(20);            /* set the video mode to Mode X */ 
   fg_resize(352,744);        /* resize video memory */ 
   fg_setclip(0,351,0,726);   /* set the clipping limits */ 
   getseed();                 /* start the random number generator */ 
   fix_palettes();            /* get the palette information */ 
} 
/******************************************************************/ 
int irandom(int min, int max) /* random number generator */ 
{ 
   register int temp; 
 
   temp = seed ^ (seed >> 7); 
   seed = ((temp << 8) ^ temp) & 0x7FFF; 
   return((seed % (max-min+1)) + min); 
} 
/******************************************************************/ 
void level_to_array(int n)    /* update all the levels */ 
{ 
   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,    sprite_fname); 
} 
/******************************************************************/ 
void load_sprite()            /* load sprite data from files */ 
{ 
   SPRITE *new_sprite; 
   register int i,j; 
   int n,nbytes; 
   int width,height; 
   int xorg,yorg; 
   int bound_x,bound_y; 
   int bound_width,bound_height; 
   char far *bitmap; 
 
   if ((stream = fopen(sprite_fname,"rt")) == NULL) 
   { 
      sprintf(abort_string,"%s not found",sprite_fname); 
      terminate_game(); 
   } 
 
   i = 0; 
   fscanf(stream,"%d",&nspritelists); 
   for (j = 0; j < nspritelists; j++) 
   { 
      fscanf(stream,"%s",list_fname); 
 
      if ((sprite_stream = fopen(list_fname,"rb")) == NULL) 
      { 
         sprintf(abort_string,"%s not found",list_fname); 
         terminate_game(); 
      } 
 
      fread(&nsprites,sizeof(int),1,sprite_stream); 
      for (n = 0; n < nsprites; n++) 
      { 
         fread(&width,sizeof(int),1,sprite_stream); 
         fread(&height,sizeof(int),1,sprite_stream); 
         nbytes = width * height; 
 
         if ((bitmap = (char far *)malloc(nbytes)) == (char *)NULL) 
         { 
            sprintf(abort_string,"out of bitmap memory"); 
            terminate_game(); 
         } 
 
         if ((new_sprite = (SPRITE *)malloc(sizeof(SPRITE))) 
            == (SPRITE *)NULL) 
         { 
            sprintf(abort_string,"out of sprite memory"); 
            terminate_game(); 
         } 
 
         sprite[i] = new_sprite; 
         fread(&xorg,sizeof(int),1,sprite_stream); 
         fread(&yorg,sizeof(int),1,sprite_stream); 
         fread(&bound_x,sizeof(int),1,sprite_stream); 
         fread(&bound_y,sizeof(int),1,sprite_stream); 
         fread(&bound_width,sizeof(int),1,sprite_stream); 
         fread(&bound_height,sizeof(int),1,sprite_stream); 
 
         fread(bitmap,sizeof(char),nbytes,sprite_stream); 
 
         sprite[i]->bitmap = bitmap; 
         sprite[i]->width = width; 
         sprite[i]->height = height; 
         sprite[i]->bound_x = bound_x; 
         sprite[i]->bound_y = bound_y; 
         sprite[i]->bound_width = bound_width; 
         sprite[i]->bound_height = bound_height; 
         sprite[i]->xoffset = 0; 
         sprite[i]->yoffset = 0; 
         i++; 
      } 
      fclose(sprite_stream); 
   } 
   fclose(stream); 
 
   /* assign the sprites to some more meaningful names */ 
   j = 0; 
   for (i = 0; i < STANDFRAMES; i++) 
      tom_stand[i] = sprite[j++]; 
   for (i = 0; i < RUNFRAMES; i++) 
      tom_run[i] = sprite[j++]; 
   for (i = 0; i < JUMPFRAMES; i++) 
      tom_jump[i] = sprite[j++]; 
   for (i = 0; i < KICKFRAMES; i++) 
      tom_kick[i] = sprite[j++]; 
   for (i = 0; i < SHOOTFRAMES; i++) 
      tom_shoot[i] = sprite[j++]; 
   for (i = 0; i < SCOREFRAMES; i++) 
      tom_score[i] = sprite[j++]; 
   for (i = 0; i < ENEMYFRAMES; i++) 
      enemy_sprite[i] = sprite[j++]; 
} 
/******************************************************************/ 
void terminate_game() 
{ 
   /* clean up and exit to DOS */ 
   fg_kbinit(0);         /* turn off the low-level keyboard handler */ 
 
   fg_setmode(3);        /* reset the video mode */ 
   fg_reset();           /* reset screen attributes */ 
 
   fg_setcolor(15);      /* print the exit string */ 
   printf(abort_string); 
   printf("\n"); 
 
   set_rate(0);          /* put the clock speed back to a normal rate */ 
#ifdef __TURBOC__ 
   setvect(0x1C,oldhandler);      /* restore the interrupt */ 
#else 
   _dos_setvect(0x1C,oldhandler); /* restore the interrupt */ 
#endif 
 
   exit(0); 
} 

Inside main()

Tommy's Adventures begins execution, as expected, with a function called main(). The main() function in TOMMY.C initializes the video environment and data structures, assigns values to global variables, and launches levels. We don't need to discuss the entire function in detail since much of the code is just simple assignment statements, however, we'll need to look at some of the control code.

One piece of interesting code that you'll encounter right away is this conditional macro used for debugging purposes:

 
#ifdef debug 
   /* text file used for debugging purposes */ 
   dstream = fopen("debug.dat","wt"); 
#endif 

We are checking for a debugging flag that can be set in TOMMY.C. If the flag is set, information to help us debug the game will be written to the DEBUG.DAT file. In Chapter 16, we'll see some helpful ways to use this file.

The code in the next section of main() consists of a number of assignment statements and simple function calls to set up the game. It is here that we initialize the interrupt vectors for the game timer, open the game data file (GAME.DAT) and read in the six game files, initialize the first level, initialize the VGA graphics, load the level data and status screen, and initialize the player, score sprite, score object, and a few other global variables. This sounds like a lot but the code is quite simple.

The actual game control begins with a continous for loop used as the main program loop:

 
for (;;)                            /* main program loop */ 
{ 
   /* initialize some global variables */ 
   nbullets = 0; 
   nenemies = 0; 
   player_blink = FALSE; 
   nblinks = 0; 
   nhits = 0; 
   nlives = 3; 
   blink_time = 0; 
   kicking = FALSE; 
 
   warp(16,80);                     /* warp to starting position */ 
   swap();                          /* swap pages */ 
   page_copy(vpo);                  /* copy visual page to hidden */ 
 
   player->x = 32;                  /* initialize player */ 
   player->y = 200; 
   player->frame = 0; 
   player->time = 0; 
   player->xspeed = 0; 
   player->yspeed = 0; 
   player->sprite = tom_stand[0]; 
   player->direction = RIGHT; 
   player->action = player_run; 
 
   launch_enemy(120,120,0);         /* launch some enemies */ 
   launch_enemy(900,120,1); 
 
   warp_to_next_level = FALSE; 
   do                               /* continuous loop */ 
   {                                /* activate the level */ 
      activate_level(); 
      if (warp_to_next_level) 
         break; 
   } 
   while(!status_screen());         /* check for program exit */ 
 
   if (!warp_to_next_level)         /* done -- exit */ 
   { 
      abort_string[0] = '\n'; 
      terminate_game(); 
   } 
 
   next_node = node->next;          /* clean up last level */ 
   for (node=bottom_node; node!=(OBJp)NULL; node=next_node) 
   { 
       next_node = node->next; 
       kill_object(node); 
   } 
   current_level++;                 /* do the next level */
   if (current_level == nlevels) 
      current_level = 0; 
   array_to_level(current_level); 
 
   fg_setcolor(0);                  /* clear the screen */ 
   fg_rect(0,351,0,479); 
   set_rate(0);                     /* set the clock rate to normal */ 
   load_level();                    /* load the level */ 
   set_rate(8);                     /* reset the clock rate */ 
} 
The loop controls the game and specifies when the game should terminate or when a new level should be displayed. The first part of the loop initializes the variables used in the current level. Then, we come to the main control code:

 
warp_to_next_level = FALSE; 
do                               /* continuous loop */ 
{                                /* activate the level */ 
   activate_level(); 
   if (warp_to_next_level) 
      break; 
} 
while(!status_screen());         /* check for program exit */ 
As this inner control loop begins, we initialize a flag called warp_to_next_level. It is set to FALSE, indicating we are not ready to warp. Then, we call activate_level() in a continuous loop. The activate_level() function controls all the level animation, as we'll see in the next section. It executes for many frames and it returns to main() if something interrupts it--if the user presses the Esc key, for example. We could interrupt the activate_level() function for many reasons. The user may want online help or to pause the game. We display a status screen by calling status_screen() with the purpose of asking the user, "what do you want to do now?" (The status screen is also a special effect, so we'll discuss it in detail in Chapter 15.) If status_screen() returns TRUE, we are ready to quit this level. If it returns FALSE, we continue.

When the level is terminated, two options are available. We can either warp to another level, or we can quit the game. The setting of the warp_to_next_level determines which path is taken. Pressing the W key, in either the status screen or during level play, will set this flag. If it is set, we clear the current level and load the next level:

 
if (!warp_to_next_level)         /* done -- exit */ 
{ 
   abort_string[0] = '\n'; 
   terminate_game(); 
} 
 
next_node = node->next;          /* clean up last level */ 
for (node=bottom_node; node!=(OBJp)NULL; node=next_node) 
{ 
    next_node = node->next; 
    kill_object(node); 
} 
current_level++;                 /* do the next level */
if (current_level == nlevels) 
   current_level = 0; 
array_to_level(current_level); 
 
fg_setcolor(0);                  /* clear the screen */ 
fg_rect(0,351,0,479); 
set_rate(0);                     /* set the clock rate to normal */ 
load_level();                    /* load the level */ 
set_rate(8);                     /* reset the clock rate */ 

Taking Control of the World with activate_level()

The activate_level() function called by main() is one big continuous loop as shown here:

 
void activate_level() 
{ 
   register int i,j; 
   unsigned long time; 
   OBJp node; /* index for linked list */ 
   OBJp next_node; /* pointer to next node */ 
 
   next_node = (OBJp)NULL; 
   for(;;)  /* loop continuously */ 
   { 
      /* determine how much time has passed since the last frame */ 
      time = game_time; 
      delta_time = time - last_time; 
      delta_time = MAX(1L,delta_time); 
      last_time = time; 
 
      if (fg_kbtest(KB_ESC))  /* check for escape key */ 
         return; 
 
      if (fg_kbtest(KB_F1))   /* check for the score board toggle */ 
         show_score = TRUE; 
      else if (fg_kbtest(KB_F2)) 
         show_score = FALSE; 
      else if (fg_kbtest(KB_W)) 
      { 
         warp_to_next_level = TRUE; 
         fg_waitfor(10); 
         return; 
      } 
 
      /* do the action function for the player */ 
      player->action(player); 
 
      /* if any scrolling occurred, adjust the screen & arrays */ 
      page_fix(); 
 
      /* do the action functions for all the sprites */ 
      for (node=bottom_node; node!=(OBJp)NULL; node=next_node) 
      { 
         next_node = node->next; 
         node->action(node);        /* do the action function */ 
      } 
 
      /* do the action function for the score */ 
      score->action(score); 
 
      /* rebuild all the background tiles on the hidden page */ 
      rebuild_background(); 
      rebuild_foreground(); 
      for (i = 0; i < 22; i++) 
         for (j = 0; j < 15; j++) 
            layout[hidden][i][j] = FALSE; 
 
      /* apply all the sprites on the hidden page */ 
      for (node=bottom_node; node!=(OBJp)NULL; node=node->next) 
         apply_sprite(node); 
 
      /* apply the player sprite */ 
      apply_sprite(player); 
 
      /* apply any foreground tiles */ 
      rebuild_foreground(); 
 
      /* if the scoreboard is visible, put it on last */ 
      if (show_score) 
         apply_sprite(score); 
 
      /* swap the pages */ 
      swap(); 
 
      /* if the page has scrolled, copy the visual page to the 
         hidden page */ 
 
      if (scrolled_left || scrolled_right || scrolled_up || 
          scrolled_down || warped) 
         page_copy(vpo); 
 
      /* reset all the scrolling globals to false */ 
      scrolled_left = FALSE; 
      scrolled_right = FALSE; 
      scrolled_up = FALSE; 
      scrolled_down = FALSE; 
      warped = FALSE; 
   } 
} 
Each iteration through the loop represents one frame. Here are the events that occur in each frame:

The order of these actions is important. For example, the background must be updated before the sprites can be applied. The foreground tiles must be placed after the sprites, so the sprites appear to walk behind them. Also, although we execute the player's action function before the other action functions, we draw the player after we draw the other sprites. So if our player intersects an enemy, the player will appear to walk in front of the enemy.

You may change the order in which objects are drawn to suit your preference. For example, you may want to draw the scoreboard before the sprites are drawn so it doesn't cover up the action--an easy change to make.

Notice that the foreground tiles are replaced twice, once before the sprites are drawn and once after. The first time, the foreground tiles that were affected by the last frame's sprites are redrawn. Then after the sprites are drawn this frame, the foreground tiles must be replaced again.

Processing User Input

The activate_level() function processes several keystrokes as follows:

Traversing a Linked List to Execute Action Functions

As we've seen, objects are stored in a linked list. The list is traversed twice each frame. The first time through, all the action functions are executed. The second time through, all the sprites are drawn. The list is traversed in the traditional way: we declare a pointer of type OBJp and call it node. The node pointer starts at one end of the list, and points to consecutive nodes until it comes to the end of the list. We know it has reached the end when it points to (OBJp)NULL, which is what the last (top) node points to. The list traversal code in the function activate_level() used to apply the sprite looks like this:

for (node=bottom_node; node!=(OBJp)NULL; node=node->next) 
      apply_sprite(node); 

An interesting thing happens when the action functions are executed. It is possible for an action function to delete an object from the list. For example, the kill_bullet() function will remove a bullet from the linked list. When this happens, pointers may become confused. We solve this problem by declaring another object pointer called next_node. This pointer is assigned before the action function is executed. That way, if the object is deleted during the action function, the pointer will not be lost. Here is the code in activate_level() that calls the action function for each sprite:

/* do the action functions for all the sprites */ 
for (node=bottom_node; node!=(OBJp)NULL; node=next_node) 
{ 
   next_node = node->next; 
   /* do the action function */ 
   node->action(node); 
} 

Completing the Frame

We have defined the end of a frame to be official when a page flip occurs. However, before starting the next frame, there are a couple more details to handle.

As we saw in Chapter 11, scrolling is done in two steps. In the process, some flags are set, including scrolled_left, scrolled_right, scrolled_down, scrolled_up, and warped. At the end of the frame, we look at these flags. If any of them have been set to TRUE, we know the hidden page and the visual page no longer match. We fix this situation right up with a call to page_copy():

if (scrolled_left || scrolled_right || scrolled_up || 
   scrolled_down || warped) 
   page_copy(vpo); 

Passing vpo (visual page offset) to page_copy() means we are copying the visual page to the hidden page. The visual page is just the way we want it, because we just completed the frame, and copying it to the hidden page prepares the hidden page for the next frame.

We are done with the flags, so if any of them were TRUE, we set them all to FALSE. This is the last thing done at the bottom of the loop:

/* reset all the scrolling globals to false */ 
scrolled_left = FALSE; 
scrolled_right = FALSE; 
scrolled_up = FALSE; 
scrolled_down = FALSE; 
warped = FALSE; 

That's it! We've done it! A frame of animation has been successfully completed. We will continue in this manner infinitely, as long as the game is played.

Next Level, Please

The activate_level() function we've been discussing is the primary function for controlling level animation. It is "blind" to the level contents. That is, it doesn't care if we are running Tommy's Egyptian level or his space platform level. The tiles can change, the sprites can change, the level size and shape can change, and even the action functions can change, and activate_level() won't care. It runs the same way no matter what the level looks like.

The Tommy's Adventures game has two levels to play with. Warping from one to the other is as easy as pressing the W key. When this happens, activate_level() returns control of program execution to function main(), which examines the warp_to_next_level flag, and initiates the warp. This involves the same initializations as the previous level: Tommy's coordinates are initialized, level data and sprite data is loaded from disk as needed, and then activate_level() is called. Game execution continues in this manner until the user has had enough and decides to quit.

It is nice to do a fancy little special effect when prompting the user to quit. We will discuss a nifty one in the next chapter.

Keeping Oriented in Time and Space

I'm a great believer in using global variables. In this game, I depend strongly on certain global variables, such as the time variables, to keep everthing organized. The time variables are changed once each frame, and may be accessed from any action function. This is especially useful for timing Tommy's motion, the release of bullets, and so on.

The screen origins, level origins, and tile origins are useful for making sure everything is displayed in the right location. When we rebuild a screen from tiles and sprites, we have to know where to apply everything relative to everything else.

Putting the Accelerated Clock to Good Use

One of the first things we do in the activate_level() function is update certain time variables. As you recall from Chapter 12, we have re- vectored an interrupt to give us a finer time resolution. The increment_timer() function updates a global variable called game_time approximately 145 times per second. This variable is available to us to regulate activities in our game. We actually only look at this variable once per frame. The real quantity that interests us is not the total number of clock ticks that have elapsed, but the number of ticks that have elapsed since the beginning of the last frame. We get this by subtracting the old time value, called last_time, from the current time value in activate_level(), as follows:

/* determine how much time has passed since the last frame */ 
time = game_time; 
delta_time = time - last_time; 
delta_time = MAX(1L,delta_time); 
last_time = time; 
The value delta_time is the number of clock ticks that have elapsed since the last frame, and it is the value we will be examining in the action functions.

Working in Multiple Coordinate Systems

Applying sprites involves several coordinate conversions. Before we get to actual sprite application (discussed in the next section), let's review the four overlapping coordinate systems we're using:

Figure 14.1 The overlapping coordinate systems used in the game.

Applying Sprites

There is more to applying sprites than just drawing a bitmap. You also must update the layout array in preparation of rebuilding the screen the next frame. (Recall the rebuild_hidden() function, discussed in Chapter 11, which examines the layout array and places tiles accordingly.) The apply_sprite() function handles the job of updating the layout array and drawing the sprite bitmap. All sprites are drawn using this function because it is assumed the layout array must be updated every time any sprite is drawn. In the next few sections we'll take a closer look at apply_sprite().

void apply_sprite(OBJp objp) 
{ 
   register int i,j; 
   int x,y; 
   int tile_x1,tile_y2; 
   int tile_x2,tile_y1; 
   int width, height; 
   char *p; 
 
   /* calculate the location, width and height */ 
   x = objp->x + objp->sprite->xoffset; 
   y = objp->y + objp->sprite->yoffset; 
   width = objp->sprite->width; 
   height = objp->sprite->height; 
 
   /* which tiles are going to be covered up? */ 
   tile_x1 = x/16 - tile_orgx; 
   tile_y2 = y/16 - tile_orgy; 
   tile_x2 = (x+width)/16 - tile_orgx; 
   tile_y1 = (y-height)/16 - tile_orgy; 
 
   /* if we are off the screen, forget it */ 
   if (tile_x2 < 0 || tile_x1 > 21 || tile_y1 > 14 || tile_y2 < 0) 
      return; 
 
   tile_x1 = MAX(tile_x1,0); 
   tile_x2 = MIN(21,tile_x2); 
   tile_y1 = MAX(tile_y1,0); 
   tile_y2 = MIN(14,tile_y2); 
 
   /* update the layout array */ 
   for (i = tile_x1; i <= tile_x2; i++) 
   { 
      p = layout[hidden][i] + tile_y1; 
      for (j = tile_y1; j <= tile_y2; j++) 
      { 
         *p++ = TRUE; 
      } 
   } 
 
   /* convert world space coordinates to screen space */ 
   x = x - (tile_orgx*16); 
   y = y - (tile_orgy*16) + hpo; 
 
   /* set the clipping limits */ 
   fg_setclip(0,351,hpo,hpb); 
   fg_move(x,y); 
 
   /* if the player is blinking, alternate black and regular */ 
   if (objp == player && player_blink) 
   { 
      blink_time += delta_time; 
      if (blink_time > 5) 
      { 
         blink_time = 0; 
         nblinks++; 
         if (nblinks == 30) 
         { 
           player_blink = FALSE; 
           nblinks = 0; 
           blink_time = 0; 
         } 
      } 
      if (nblinks%2 == 0) 
      { 
         get_blinkmap(objp); 
         if (objp->direction == RIGHT) 
             fg_clpimage(blink_map,objp->sprite->width, 
                         objp->sprite->height); 
         else 
             fg_flpimage(blink_map,objp->sprite->width, 
                         objp->sprite->height); 
         fg_setclip(0,351,0,726); 
         return; 
      } 
   } 
 
   /* not blinking, just display the bitmap */ 
   if (objp->direction == RIGHT) 
      fg_clpimage(objp->sprite->bitmap,width,height); 
   else 
      fg_flpimage(objp->sprite->bitmap,width,height); 
   fg_setclip(0,351,0,726); 
} 

Calculating Coordinates

The first task apply_sprite() performs is calculate the x and y positions in world space. This is done by adding the x and y offsets to the x and y fields in the object structure.

/* calculate the location, width and height */ 
x = objp->x + objp->sprite->xoffset;
y = objp->y + objp->sprite->yoffset; 

We also grab the width and the height of the sprite as follows:

width = objp->sprite->width; 
height = objp->sprite->height; 

Next, we calculate which tiles will be covered by this sprite. The x and y coordinates are then converted to layout space by dividing them by 16 and subtracting the tile origin:

tile_x1 = x/16 - tile_orgx; 
tile_y2 = y/16 - tile_orgy; 

Since a sprite will typically cover more than one tile, we also calculate the maximum x tile and the minimum y tile, based on the width and height of the sprite:

tile_x2 = (x+width)/16 - tile_orgx; 
tile_y1 = (y-height)/16 - tile_orgy; 

Now that we know which tiles the sprite will occupy, we can check if the sprite is completely off the screen. It is acceptable for the sprite to be partially off the screen because we are going to clip it when we display it. If it is totally off the screen, there is no point in drawing it at all. Here is the line of code that checks for this condition:

if (tile_x1 > 21 || tile_x2 < 0 || tile_y1 > 14 || tile_y2 < 0) 
   return; 

A sprite can go over the edge of the screen, but a tile cannot. When updating the layout array, we need to be careful not to write to an array element outside the array extents. That is, the sprite may cover a tile position at tile_x1 = -1, as in Figure 14.2, but we would not assign layout_array[-1] to TRUE. Doing so would result in an "array out of bounds" error. To be safe, we'll set the tile minimums and maximums to the edges of the screen:

tile_x1 = MAX(tile_x1,0); 
tile_x2 = MIN(21,tile_x2); 
tile_y1 = MAX(tile_y1,0); 
tile_y2 = MIN(14,tile_y2); 

Figure 14.2 A sprite going off the edge of the screen.

Updating the layout Array

Now we're ready to update the layout array. Each tile that is covered by a sprite will have its corresponding layout_array[] element marked TRUE. We work down the columns, starting with the first column covered by the sprite. We use a pointer, *p, to speed up the process:

/* update the 
layout array */ 
for (i = tile_x1; i <= tile_x2; i++) 
{ 
   p = layout[hidden][i] + tile_y1; 
   for (j = tile_y1; j <= tile_y2; j++) 
   { 
      *p++ = TRUE; 
   } 
} 
Notice we are updating the layout array for the hidden page, because that is where we are planning to put the sprite.

Displaying the Sprite

It's time to display the sprite. First, though, we need to do a few more coordinate calculations. We convert the world space x and y coordinates to screen space x and y coordinates by subtracting the origins in level space. This will give us a value for x between 0 and 351 and a value for y between 0+hpo and 239+hpo. (The hpo value will be either 0 or 240 depending on which page is currently the hidden page.)

x = x - (tile_orgx*16); 
y = y - (tile_orgy*16) + hpo; 
Notice how the screen origins are calculated on the fly by multiplying the tile origins by 16 (see Figure 14.3) .

Figure 14.3 Screen Origins are calculated on the fly.

Setting the Clipping Region

Since the sprite can go over the edge of the screen, it's important that we set the clipping limits. We're drawing the sprite on the hidden page, so we'll clip it at the top and bottom of the hidden page. If we neglected to do this, we would find remnants of our sprite on the hidden page or, worse, in the tile area. The clipping area is set by using the Fastgraph fg_setclip() function shown here:

fg_setclip(0,351,hpo,hpb); 

fg_move(x,y); 

If the sprite is facing right, we use fg_clpimage() to display it in a clipped, forward-facing direction. If the sprite is facing left, we use fg_flpimage() to display it in a reversed, or "flipped," direction. Here's the code that controls this operation:

   if (objp->direction == RIGHT) 
      fg_clpimage(objp->sprite->bitmap,width,hight); 
   else 
      fg_flpimage(objp->sprite->bitmap,width,height); 

Notice what we are passing to fg_clpimage() and fg_flpimage().

 
fg_clpimage(objp->image->bitmap,width,height); 

The object points to the sprite, which points to the bitmap data.

Changing Tommy's Orientation

Using the same bitmap for left- and right-facing sprites saves RAM. In the case of Tommy, it also causes an interesting anomaly. You may notice that when he is facing right, he holds the gun in his right hand, and when he faces left, he is a left-handed gunslinger. His gun arm is always the arm in front. That's because the same bitmap is being used for shooting in both directions. We allow this blooper because it saves RAM. Most players won't notice that Tommy changes gun hands, and if they do, it does not detract from the playability of the game. Once again, conservation of sprite memory is a priority. An ambidextrous Tommy is a small price to pay for the several kilobytes of RAM saved using this strategy.

The last thing apply_sprite() does is return the clipping limits to encompass all of video memory. The sprite is now drawn on the hidden page, and will be visible the next time we do a page flip.

You may have noticed there is a special case in apply_sprite where Tommy is blinking. In this case, instead of Tommy's sprite bitmap being displayed, a special bitmap called blinkmap is displayed. I'll describe where this bitmap comes from in the next chapter when we talk about special effects.

Exiting the Game

Graceful exit from any game involves restoring the system to the way it was. The terminate_game() function does this.

 
void terminate_game() 
{ 
   /* clean up and exit to DOS */ 
   fg_kbinit(0);         /* turn off the low-level keyboard handler */ 
 
   fg_setmode(3);        /* reset the video mode */ 
   fg_reset();           /* reset screen attributes */ 
 
   fg_setcolor(15);      /* print the exit string */ 
   printf(abort_string); 
   printf("\n"); 
 
   set_rate(0);          /* put the clock speed back to a normal rate */ 
   _dos_setvect(0x1C,oldhandler); /* restore the interrupt */ 
 
   exit(0); 
} 
Actually, this function should look familiar, as we terminated several programs in the earlier chapters when we talked about utilities like the tile ripper. The only differences we see here is there are some additional tasks involved in the clean-up process: we must set the clock speed back to normal, restore the clock interrupt to what it was before, and we must also terminate the low-level keyboard handler. Failing to do any of these things can be disasterous. If the low-level keyboard handler is not disabled, the user will not be able to type in any DOS commands or even reboot! And failing to restore the clock interrupt can cause problems with disk accesses.

The polite thing to do is leave the system in the same state in which you found it.

Next Chapter

_______________________________________________

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

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

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