Action Arcade Adventure Set
Diana Gruber

Chapter 15
Creating Special Effects


Nothing adds more life and drama to your game than special effects. Take a look at this chapter to see how you can create interesting effects to make your game more appealing.

Special effects are the best part of game programming. They are the icing on the cupcake, so to speak. They are also fun to write, because when it comes to special effects, you can break all the rules. You don't have to use your game editor to create special effects. You can use other tools that you already have or you can experiment with new ones. Here's your chance to play with that frame grabber card that has been gathering dust in your system. You can also create some great effects with your scanner or use some interesting ray tracing and image processing techniques. When creating special effects, let your imagination run wild. Try to show your users something they've never seen before.I like to work on special effects when I get bored with programming the other parts of my games. Many of the game parts are just plain tedious and sometimes it feels good to take a break and work on something else. Once you visualize a special effect, try to think of ways to bring your ideas to life. When it comes to special effects, you're no longer constrained by tiles, sprites, and predefined data structures. Nearly any trick you can think of to accomplish your special effect is perfectly acceptable.

In this chapter, we'll explore the special effects file EFFECTS.C, which is used in Tommy's Adventures. This is the final source final we need to complete our game. We'll start by discussing a few general guidelines for programming special effects and then we'll look at how the two special effects in our game are programmed.

The Golden Rule for Special Effects--Save RAM

There are a few limitations on what you can do with special effects. In most cases, you won't want to wipe out everything that is currently in video memory. You have to find a way to squeeze your special effects into whatever room is available. When we designed our use of video memory in Chapter 5, we left a little room for our special effects. (Recall the room we left available at the bottom of the video page.) I always try to leave a little room in video memory for special effects; think of this as allowing room for future creativity.

Unfortunately, special effects can take up a lot of RAM. Thus, you should try to optimize your use of RAM. If you have two special effects that never occur at the same time, see if you can share memory space. Also, plan ahead in terms of disk accesses. There are times when disk access is appropriate, such as when you save a game or warp to another level. Other times, a disk access simply won't work. For example, if you put in an explosion effect, you'll need it to be instantaneous. You can't read explosion data from disk. If the player of your game has to wait for an explosion to occur, your game won't be very exciting!

One way to conserve RAM is to use the same array space for more than one item. For example, you can store data for a popup help window and an explosion in the same array. The way to do this is to load the explosion data in the array and leave it there. That way, it will always be accessible when you need it. When you need the popup help data, read it from disk and overwrite the explosion array. Then, display the help data. When you're done, read the explosion data from the disk and overwrite the help window again. With this technique, the explosion will be reloaded into RAM and ready for instant display when you need it. This strategy assumes that the player will never want popup help in the middle of an explosion. That is a pretty safe assumption. If the user pauses the game by asking for help in the middle of the explosion, he or she will have to wait the few seconds it takes for the two disk accesses.

Creating Special Effects with EFFECTS.C

Table 15.1 lists the five functions provided in EFFECTS.C. The two special effects we are going to look at are:

Table 15.1 Functions Defined in EFFECTS.C

FunctionDescription
get_blinkmap()Gets a mask bitmap for the blinking sprite
load_status screen()Reads the slide projector screen from a file
redraw_screen()Updates all of the tiles
status_screen()Displays the slide projector screen
status_shape()Helps build the slide projector screen

Here is the complete source file for EFFECTS.C:

 
/******************************************************************\ 
*  effects.c -- Tommy game source code file                        * 
*  copyright 1994 Diana Gruber                                     * 
*  compile using large model, link with Fastgraph (tm)             * 
\******************************************************************/ 
 
#include "gamedefs.h" 
/******************************************************************/ 
void get_blinkmap(OBJp objp) 
{ 
   /* create a bitmap based on the sprite bitmap. If the pixel is 
      any color except 0, make it black. 0 colors are transparent */ 
 
   register int i; 
   register int nchar; 
   char *p; 
 
   /* p points to the bitmap of the object we want to copy */ 
   p = objp->sprite->bitmap; 
 
   /* figure out the width and height */ 
   nchar = objp->sprite->width * objp->sprite->height; 
 
   /* make the silhouette copy */ 
   for (i = 0; i < nchar; i++) 
   { 
      if (p[i] == 0) 
         blink_map[i] = 0; 
      else 
         blink_map[i] = 7; 
   } 
} 
/******************************************************************/ 
void load_status_screen() 
{ 
   /* read in the slide projector screen */ 
   long filesize; 
 
   if ((stream = fopen("slide.spr","rb")) == NULL) 
   { 
      strcpy(abort_string,"slide.spr not found"); 
      terminate_game(); 
   } 
   /* allocate room for the slide projector screen */ 
   filesize = filelength(fileno(stream)); 
   slide_array = malloc((int)filesize); 
   if (slide_array == (int)NULL) 
   { 
      strcpy(abort_string,"Out of memory: slide_array"); 
      terminate_game(); 
   } 
   fread(slide_array,sizeof(char),(unsigned int)filesize,stream); 
   slide_arraysize = (int)filesize; 
   fclose(stream); 
} 
/******************************************************************/ 
void redraw_screen() 
{ 
   OBJp node; 
   register int i,j; 
 
   for (i = 0; i < 22; i++)       /* draw background tiles */ 
      for (j = 0; j < 15; j++) 
            put_tile(i,j); 
 
   apply_sprite(player);         /* draw the sprites */ 
   for (node=bottom_node; node!=(OBJp)NULL; node=node->next) 
      apply_sprite(node); 
                                 /* draw foreground tiles */ 
   for (i = 0; i < 22; i++) 
         if (layout[hidden][i][j]) 
            put_foreground_tile(i,j); 
 
   if (show_score)               /* draw the score sprite */ 
      apply_sprite(score); 
} 
/******************************************************************/ 
int status_screen() 
{ 
   /* assemble a slide  projector screen (special effect) */ 
 
   int nruns; 
   unsigned char key,aux; 
   register int y; 
   static char *string[] = { 
   "Tommy's", 
   "Adventures", 
   "Q - Quit", 
   "W - Warp", 
   "copyright 1994", 
   "Diana Gruber"}; 
 
   /* display the RLE in offscreen memory */ 
   nruns = slide_arraysize/2; 
   fg_move(0,726); 
   fg_display(slide_array,nruns,320); 
   fg_tcmask(1); 
 
   redraw_screen();            /* frame 1: screen & legs folded */ 
   status_shape(3,156,150); 
   status_shape(7,160,110); 
   status_shape(4,160,91); 
   status_shape(5,160,142); 
   status_shape(6,160,99); 
   swap(); 
   fg_waitfor(1); 
 
   redraw_screen();            /* frame 2: legs open */ 
   status_shape(2,128,168); 
   status_shape(7,160,136); 
   status_shape(7,160,110); 
   status_shape(4,160,91); 
   status_shape(5,160,142); 
   status_shape(6,160,99); 
   swap(); 
   fg_waitfor(1);
   redraw_screen();            /* frame 3: screen turns 90 degrees */ 
   status_shape(2,128,168); 
   status_shape(7,160,136); 
   status_shape(7,160,110); 
   status_shape(4,160,91); 
   status_shape(1,100,128); 
   swap(); 
 
   fg_waitfor(1);              /* frame 4: top goes up */ 
   redraw_screen(); 
   status_shape(2,128,168); 
   status_shape(7,160,136); 
   status_shape(7,160,110); 
   status_shape(4,160,74); 
   status_shape(1,100,128); 
   swap(); 
 
   for (y = 123; y > 50; y-= 8) /* move the screen up */ 
   { 
      fg_tcxfer(0,127,683,695,100+screen_orgx,y+vpo+screen_orgy,0,0); 
      fg_waitfor(1); 
   } 
   fg_tcxfer(0,127,683,695,100+screen_orgx,48+vpo+screen_orgy,0,0); 
 
   fg_setcolor(black); 
   center_string(string[0],105+screen_orgx,219+screen_orgx, 
                    50+vpo+screen_orgy); 
   center_string(string[1],105+screen_orgx,219+screen_orgx, 
                    60+vpo+screen_orgy); 
 
   fg_setcolor(blue); 
   center_string(string[2],105+screen_orgx,219+screen_orgx, 
                    77+vpo+screen_orgy); 
   center_string(string[3],105+screen_orgx,219+screen_orgx, 
                    87+vpo+screen_orgy); 
 
   fg_setcolor(black); 
   center_string(string[4],105+screen_orgx,219+screen_orgx, 
                    105+vpo+screen_orgy); 
   center_string(string[5],105+screen_orgx,219+screen_orgx, 
                    115+vpo+screen_orgy); 
 
   fg_kbinit(0);               /* turn off the keyboard handler */ 
   flushkey(); 
   fg_getkey(&key,&aux);       /* wait for a keypress */ 
 
 
   if ((key|32) == 'q')        /* quit */ 
   { 
      fg_kbinit(1); 
      return (TRUE); 
   } 
   else if ((key|32) == 'w')   /* warp */ 
   { 
      fg_kbinit(1); 
      warp_to_next_level = TRUE; 
      return (TRUE); 
   } 
   else                        /* don't quit */ 
   { 
      redraw_screen(); 
      swap(); 
      redraw_screen(); 
      fg_kbinit(1); 
      return (FALSE); 
   } 
} 
/******************************************************************/ 
void status_shape(int shape,int x,int y) 
{ 
   /* pieces of the slide projector screen */ 
   static int x1[] = {  0,  0,128,196,208,216,232,208}; 
   static int x2[] = {127,127,195,207,215,223,247,215}; 
   static int y1[] = {683,697,683,683,683,683,683,689}; 
   static int y2[] = {695,704,714,723,725,726,726,725}; 
 
   fg_tcxfer(x1[shape],x2[shape],y1[shape],y2[shape], 
             x+screen_orgx,y+screen_orgy+hpo,0,0); 
} 

Slides Ahead

When I was younger, my father used to torture my siblings and me by showing us slides of his travels to other countries. Usually his pictures would have some historical or geographical significance, but to us they always looked like a picture of a cow in a field. How little we appreciated such things back then. Even now I look forward to slide shows with something less than enthusiasm.

When I designed the Tommy's Adventures game, I pictured Tommy as a small child tormented by large crawling insects and adults with slide projectors. I decided to incorporate a slide projector screen in the game as a special effect. It has some advantages as special effects go. It's roughly rectangular in shape, it can be easily drawn and built by graphic components, it's easy to recognize, it moves but it doesn't move too much, and it has a large white area that can be used to frame whatever gems of information I want to pass along to my users, such as "Quit Y/N?"

I began creating my special effect by drawing the slide projector screen. I just pulled out the screen from the back of the closet and set it up in the living room. The result of my effort is shown in Figure 15.1.

Figure 15.1 The slide projector screen.

When I was satisfied with my drawing, I organized the drawing in such a way as to optimize space. Figure 15.2 shows the final state of the slide projector screen graphics. As you can see, the projector screen has been broken down into components. Since I planned to store the graphic in an RLE (discussed shortly), I organized the graphic into a shape that is long in the horizontal direction and narrow in the vertical direction.

Figure 15.2 Slide projector screen, reduced to components.

RLE Images

RLE images have properties that are different from other image formats. This gives them certain advantages and disadvantages:

Advantages:

Disadvantages:

Loading the Status Screen

Traditionally, RLE files have an SPR (standard pixel run) extension. The RLE file containing the slide projector screen is stored in the file SLIDE.SPR. Since I wanted the slide projector screen to display instantaneously, I decided to load the RLE into RAM and leave it there. The function load_status_screen() in the EFFECTS.C file loads the RLE like this:

void load_status_screen() 
{ 
   /* read in the slide projector screen */ 
   long filesize; 
 
   if ((stream = fopen("slide.spr","rb")) == NULL) 
   { 
      strcpy(abort_string,"slide.spr not found"); 
      terminate_game(); 
   } 
   /* allocate room for the slide projector screen */ 
   filesize = filelength(fileno(stream)); 
   slide_array = malloc((int)filesize); 
   if (slide_array == (int)NULL) 
   { 
      strcpy(abort_string,"Out of memory: slide_array"); 
      terminate_game(); 
   } 
   fread(slide_array,sizeof(char),(unsigned int)filesize,stream); 
   slide_arraysize = (int)filesize; 
   fclose(stream); 
} 

The slide RLE is stored in an array called slide_array [] which is declared in GAMEDEFS.H like this:

DECLARE unsigned char far *slide_array;

The room for the slide array must be allocated at runtime. Calculating the size of the slide array is as easy as looking at the file size. C's filelength() function returns the size of the SLIDE.SPR file, which is the size of the RLE. The SLIDE.SPR file is only 3K, which seemed like a reasonable investment in RAM usage for a special effect we'll be using often.

Building a Picture from Parts

There is a problem with storing the slide screen RLE in RAM. The screen is built in parts. First the legs are unfolded, then the bar is raised, the screen swings down, and the screen moves up incrementally. To accomplish this effect, I need to be able to blit parts of the image to the screen in sequence. It's very difficult to blit from RAM to video memory when you are extracting part of an RLE. The alternative is to store the image in RAM as a regular unpacked bitmap, but that would take more space--approximately 14K! My solution was to unpack the bitmap to video memory, and then perform video-to-video blits to copy the components of the slide projector from a hidden area in video memory to the visual page.

Recall the shape of video memory we designed earlier. It's a large rectangle with two pages at the top for page swapping, a foreground and background tile area, and an area below that is sometimes used to display the scoreboard. Our video memory scheme is shown in Figure 15.3.

Figure 15.3 Available video memory.

Sharing Space

Recall that the scoreboard is only displayed in video memory only periodically--when the score has changed and a new bitmap must be built. The rest of the time, the area below row 673 is not used. Since it's impossible for the score to change while the status screen (slide projector screen) is being displayed, we can safely re-use this area of video memory for our special effect.

The status_screen() function in the EFFECTS.C file handles the job of displaying the RLE in video RAM and reconstructing the slide projector screen from its parts:

 
int status_screen() 
{ 
   /* assemble a slide projector screen (special effect) */ 
 
   int nruns; 
   unsigned char key,aux; 
   register int y; 
   static char *string[] = { 
   "Tommy's", 
   "Adventures", 
   "Q - Quit", 
   "W - Warp", 
   "copyright 1994", 
   "Diana Gruber"}; 
 
   /* display the RLE in off-screen memory */ 
   nruns = slide_arraysize/2; 
   fg_move(0,726); 
   fg_display(slide_array,nruns,320); 
   fg_tcmask(1); 
 
   redraw_screen();            /* frame 1: screen & legs folded */ 
   status_shape(3,156,150); 
   status_shape(7,160,110); 
   status_shape(4,160,91); 
   status_shape(5,160,142); 
   status_shape(6,160,99); 
   swap(); 
   fg_waitfor(1); 
 
   redraw_screen();            /* frame 2: legs open */ 
   status_shape(2,128,168); 
   status_shape(7,160,136); 
   status_shape(7,160,110); 
   status_shape(4,160,91); 
   status_shape(5,160,142); 
   status_shape(6,160,99); 
   swap(); 
   fg_waitfor(1);
   redraw_screen();            /* frame 3: screen turns 90 degrees */ 
   status_shape(2,128,168); 
   status_shape(7,160,136); 
   status_shape(7,160,110); 
   status_shape(4,160,91); 
   status_shape(1,100,128); 
   swap(); 
 
   fg_waitfor(1);              /* frame 4: top goes up */ 
   redraw_screen(); 
   status_shape(2,128,168); 
   status_shape(7,160,136); 
   status_shape(7,160,110); 
   status_shape(4,160,74); 
   status_shape(1,100,128); 
   swap(); 
 
   for (y = 123; y > 50; y-= 8) /* move the screen up */ 
   { 
      fg_tcxfer(0,127,683,695,100+screen_orgx,y+vpo+screen_orgy,0,0); 
      fg_waitfor(1); 
   } 
   fg_tcxfer(0,127,683,695,100+screen_orgx,48+vpo+screen_orgy,0,0); 
 
   fg_setcolor(black); 
   center_string(string[0],105+screen_orgx,219+screen_orgx, 
                    50+vpo+screen_orgy); 
   center_string(string[1],105+screen_orgx,219+screen_orgx, 
                    60+vpo+screen_orgy); 
 
   fg_setcolor(blue); 
   center_string(string[2],105+screen_orgx,219+screen_orgx, 
                    77+vpo+screen_orgy); 
   center_string(string[3],105+screen_orgx,219+screen_orgx, 
                    87+vpo+screen_orgy); 
 
   fg_setcolor(black); 
   center_string(string[4],105+screen_orgx,219+screen_orgx, 
                    105+vpo+screen_orgy); 
   center_string(string[5],105+screen_orgx,219+screen_orgx, 
                    115+vpo+screen_orgy); 
 
   fg_kbinit(0);               /* turn off the keyboard handler */ 
   flushkey(); 
   fg_getkey(&key,&aux);       /* wait for a keypress */ 
 
 
   if ((key|32) == 'q')        /* quit */ 
   { 
      fg_kbinit(1); 
      return (TRUE); 
   } 
   else if ((key|32) == 'w')   /* warp */ 
   { 
      fg_kbinit(1); 
      warp_to_next_level = TRUE; 
      return (TRUE); 
   } 
   else                        /* don't quit */ 
   { 
      redraw_screen(); 
      swap(); 
      redraw_screen(); 
      fg_kbinit(1); 
      return (FALSE); 
   } 
} 

We display the RLE array from RAM to the desired part of video memory using Fastgraph's fg_display() function as follows.

 
fg_move(0,726); 
fg_display(far_slidearray,nruns,320); 

Notice how we calculate the number of runs in the RLE bitmap. Since each pixel run takes exactly two bytes, the number of runs is the array size divided by two:

 
nruns = slide_arraysize/2; 

Also, notice that the width of the RLE is 320, which is equal to the width of the screen. Actually, the image itself is slightly less than 320. I chose the 320 value because I found it easier to work with. Extra blank space at the right side of an RLE adds no additional overhead in terms of disk space or RAM space. The amount of extra time it takes to blit the RLE is negligible, but the savings in development time is significant. Recalculating the width every time the art changes, trimming the image, and then editing and recompiling the code is time consuming. Using the full screen width for this image is a reasonable shortcut.

Animating the Status Screen

We're calling the effect a status screen because its purpose is to report the status of the game, such as when the game has been paused or saved. It takes five frames to animate the setting up of the screen, and then another nine frames in a loop to slide the screen up. There are eight components of the slide projector screen. Not all these components are displayed in every frame.

I found it simplified the code to have a separate function called status_shape() to display the individual pieces of the screen. The x and y coordinates are stored in static arrays that are initialized in the function. The fg_tcxfer() function is used to do a transparent blit from video memory to video memory to build the screen:

 
void status_shape(int shape,int x,int y) 
{ 
   /* pieces of the slide projector screen */ 
   static int x1[] = {  0,  0,128,196,208,216,232,208}; 
   static int x2[] = {127,127,195,207,215,223,247,215}; 
   static int y1[] = {683,697,683,683,683,683,683,689}; 
   static int y2[] = {695,704,714,723,725,726,726,725}; 
 
   fg_tcxfer(x1[shape],x2[shape],y1[shape],y2[shape], 
             x+screen_orgx,y+screen_orgy+hpo,0,0); 
} 

When I originally wrote the function, I put multiple calls to fg_tcxfer() in the status_screen() function, but I didn't like the way the code looked (it looked messy). Isolating the calls to fg_tcxfer() in a separate function made the code easier to read and debug.

Each frame of animation is accomplished by redrawing the hidden page, applying the components to the hidden page, and swapping pages. Only a small part of the white and blue slide projector screen is stored in the RLE. This is expanded into a full-size screen by copying parts of it in sequence to give the effect of motion as the screen "unrolls" upward, as shown in Figure 15.4.

 
for (y = 123; y > 50; y -= 8) 
{ 
   fg_tcxfer(0,127,683,695,100+screen_orgx,y+vpo+screen_orgy,0,0); 
   fg_waitfor(1); 
} 

Figure 15.4 Sequential video blits give the effect of motion.

After the screen has been completely drawn, text is added to it. In a bigger game, I would probably use variable text. That is, I would re-use the effect but put different text on it depending on what the game was doing. For example, at the end of the game, I might display "You lose!" on the status screen.

Different Kinds of Keyboard Handlers

After the screen is displayed, animated, and has the text drawn on it, it's time to wait for user input. At this point, I disable the low-level keyboard handler by calling fg_kbinit(0). I find it easier to use the regular BIOS keyboard handler with the keyboard buffer for accepting text input. The low-level handler is more appropriate for the kind of high speed input we need during game play. Status screens work better when you can collect keystrokes and process them one at a time.

I only process three kinds of user input in this function. I'm looking for instructions to quit, warp to the next level, or continue. This is a somewhat simplified function. In a more sophisticated game, you would offer the user more choices, or perhaps a menu with a floating highlight bar. For our purposes, the three choices are enough to demonstrate this example of a special effect.

Making Tommy Blink

Most games give some kind of a blink effect when the character encounters an enemy or hazard. (I don't mean the character's eyes blink--I mean his whole body flashes on and off, as if in an emotional response to the trauma of having encountered a giant pink scorpion). I've seen this in so many games, I decided I needed to duplicate it. I consider blinking sprites a special effect, but it isn't really that special.The way I make a sprite blink is by creating another bitmap that is an exact mask of the original bitmap. That is, all the transparent pixels in the original bitmap remain transparent, and all the non-transparent pixels are set to black, as shown in Figure 15.5 . Then I alternate displaying the regular bitmap and the bitmap mask. The effect is a sprite that blinks to black at a specified interval.

Figure 15.5 A sprite along with an exact mask of the original bitmap.

The mask bitmap is made on the fly. We don't want to create a mask bitmap for every sprite ahead of time because that would waste a lot of space. We don't know exactly what Tommy is going to be doing when he needs to blink. He could touch an enemy while running, jumping, kicking, or shooting. To complicate matters further, Tommy doesn't stand still after he has been hit. He continues running, jumping, kicking, and shooting even while blinking. Whatever Tommy is doing, he has to continue doing it, and he has to simultaneously blink. Therefore, we will need to make many mask bitmaps as Tommy moves along.

The Blink Map

I allocate space at load- time for the blink mask bitmap. Four thousand bytes seems to be about enough space. The declaration for the blink mask bitmap is found in GAMEDEFS.H and shown here:

char far blink_map[4000]; 

This array will be used to hold the mask data. It will be filled on the fly by comparing it to the sprite bitmap (in this case, always Tommy), and setting the bytes to 0 or 1. This task is accomplished in the function get_blinkmap():

 
void get_blinkmap(OBJp objp) 
{ 
   /* create a bitmap based on the sprite bitmap. If the pixel is 
      any color except 0, make it black. 0 colors are transparent */ 
 
   register int i; 
   register int nchar; 
   char *p; 
 
   /* p points to the bitmap of the object we want to copy */ 
   p = objp->image->bitmap; 
 
   /* figure out the width and height */ 
   nchar = objp->image->width * objp->image->height; 
 
   /* make the silhouette copy */ 
   for (i = 0; i < nchar; i++) 
   { 
      if (p[i] == 0) 
         blink_map[i] = 0; 
      else 
         blink_map[i] = 7; 
   } 
} 
In GAMEDEFS.H we also declare a global variable to tell us when Tommy is blinking, and another one to tell us how many times he has blinked so far:

 
int player_blink; 
int nblinks; 

Also, since the blink is time dependent, we'll need another variable to keep track of the time since the last blink:

unsigned long blink_time; 

Since this is a time variable, we declare it unsigned long so it will match the other time variables.

Counting Blinks

Every time Tommy hits an enemy, player_blink is set to TRUE and nblinks is set to 0. Tommy will continue to blink until nblinks hits a target value. In our game, the target value is 30. That is, Tommy blinks 30 times each time he is hit. The code to handle the blinking is part of the apply_sprite() function from TOMMY.C, which we reviewed in the last chapter . Let's take another look at this function, especially the last part, where the bitmap is displayed:

 
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); 
} 

Recall that apply_sprite() is a general purpose function. It's called when any sprite is applied. The only case we are concerned with is when the sprite is Tommy, and Tommy is blinking. If that's the case, the first thing we do is add delta_time to blink_time. We are only going to change the blink status if five or more clock ticks have elapsed. Tommy is not going to change every frame. He could have two or three black frames in a row, followed by two or three regular frames. It depends on the speed of the computer, and what else is happening during the frame, such as scrolling. Tying the blink to the clock is the cleanest way to get a consistent looking blink.

If five clock ticks have passed, the nblinks variable is incremented. This variable serves two purposes. Besides counting the total number of blinks, we use it to keep track of the current blink state. If nblinks is an even number, then we display the mask bitmap. If it is odd, we display the regular bitmap.

Each time we display the mask bitmap, we build a new mask. We assume the sprite has changed since the last time we displayed the mask bitmap. It's a pretty safe assumption. If Tommy was recently hit, his first instinct will be to move really fast, either to get away or to shoot back. It's not worth the effort to check and see if the sprite image has changed. Skipping the get_blinkmap() function in the rare case where the sprite image has not changed is not worth the overhead in doing the check every time. Besides, get_bitmap() is a reasonably fast function, since all it does is manipulate an array in RAM.

The status screen effect and the sprite blinking effect are just two simple examples of the kinds of special effects people like to see in games. Special effects add drama and life to your game and should be used liberally and ingeniously. The suggestions given here are just guidelines. Let your imagination be your guide when it comes to special effects. The more of them you have, the better your game will be.

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.