Action Arcade Adventure Set
Diana Gruber

Chapter 11
Supporting Game Levels and Scrolling


The world we live in is constantly moving. How can we express this motion on a computer screen?

Many years ago, in my misspent youth, I took a cross-country trip on a motorcycle. I visited places I had never been to before, and it was quite an adventure. I traveled very light; everything I wore for two weeks fit in one saddlebag of a Honda Gold Wing. I took the bare minimum I needed to survive, and what I didn't have, I didn't miss.Now that I'm older I've become accustomed to traveling in a different style. With airlines, rental cars, porters, and bell captains, I don't have to travel light. I take everything I can pack; clothes, shoes (lots of them), cosmetics, hair-care supplies, hats, hat boxes, pajamas, books, toys--you name it. I take stuff I don't even need simply because I'm able to.

Software works the same way. When I began designing games years ago, software had to travel light. Our games needed to fit on a single 360K floppy and run in 256K RAM. The games had to work on all platforms and look good. These days, games can take up megabytes of hard disk and RAM space. If you run out of disk space, you can move up to a CD-ROM. Unfortunately, games are starting to get fat and extravagant. We put stuff in them we don't really need. So now, we need to return to the idea of traveling light, particularly where our game code is concerned. Code that is small and tight is easier to debug and port to other platforms. The smaller the code the better, because you can always find a use for any room that is left over. For example, music and sound effects tend to expand to fill any amount of available RAM, so you should leave some room for them.

In the true spirit of "lean and mean" game programming, we need to develop a set of tight functions for loading, displaying, and scrolling our level art. In this chapter, we'll present each of the functions we need to build the foundation of our game code.

Introducing the Game-Scrolling Functions

The scrolling functions, as well as the functions to draw the background for our game, are in the file MAP.C. Here we have functions to load levels, "blit" foreground and background tiles, scroll level art in all directions (up, down, left, right, and even diagonally), and warp to a position in a level. Table 11.1 lists the complete set of functions.

Table 11.1 Functions Used in MAP.C

FunctionDescription
load_level()Loads graphics and level data from disk
page_copy()Copies one page to another
page_fix()Adjusts tile graphics according to scrolling
put_foreground_tile()"Blits" a foreground tile
put_tile()"Blits" a background tile
rebuild_background()Copies all background tiles to hidden page
rebuild_foreground()Copies all foreground tiles to hidden page
scroll_down()Performs scrolling calculations to scroll down
scroll_left()Performs scrolling calculations to scroll left
scroll_right()Performs scrolling calculations to scroll right
scroll_up()Performs scrolling calculations to scroll up
swap()Performs a page flip
warp()Rebuilds a complete level at any location

Some of these functions may look similar to the ones we used in the game editor. In fact, the theory behind the scrolling is the same as what we saw in Chapter 5. Video memory is resized and rectangular areas for pages and tiles are assigned the same way. There are some differences in the way the code behaves, though, as we will see in a minute. But first, let's have a look at the source code in the file MAP.C.

 
/******************************************************************\ 
*  map.c -- Tommy game map/level source code                       * 
*  copyright 1994 Diana Gruber                                     * 
*  compile using large model, link with Fastgraph (tm)             * 
\******************************************************************/ 
 
#include "gamedefs.h" 
/******************************************************************/ 
void load_level() 
{ 
   register int i; 
 
   tile_orgx = 0;         /* initialize global level variables */ 
   tile_orgy = 0; 
   screen_orgx = 0; 
   screen_orgy = 0; 
   vpo = 0; 
   hpo = 240; 
   vpb = vpo+239; 
   hpb = hpo+239; 
   visual = 0; 
   hidden = 1; 
   tpo = 480; 
 
   /* display the foreground tiles */ 
   fg_move(0,tpo); 
   fg_showpcx(foreground_fname,2); 
 
   /* reorganize the foreground tiles to conserve video memory */ 
   fg_transfer(0,31,480,591,320,591,0,0); 
   fg_transfer(32,63,480,591,320,703,0,0); 
 
   /* display the background tiles */ 
   fg_move(0,480); 
   fg_showpcx(background_fname,2); 
 
   /* fix the foreground palettes */ 
   fix_palettes(); 
 
   /* load the level information */ 
   if ((level_stream = fopen(level_fname,"rb")) == NULL) 
   { 
      sprintf(abort_string,"%s not found",level_fname); 
      abort_game(); 
   } 
 
   /* get the rows and columns */ 
   fread(&ncols,sizeof(int),1,level_stream); 
   fread(&nrows,sizeof(int),1,level_stream); 
 
   /* load the background tiles */ 
   for (i = 0; i < ncols; i++) 
      fread(&background_tile[i][0],sizeof(char),nrows,level_stream); 
 
   /* load the foreground tiles */ 
   for (i = 0; i < ncols; i++) 
      fread(&foreground_tile[i][0],sizeof(char),nrows,level_stream); 
   fclose(level_stream); 
 
   /* load the background tile attributes */ 
   if ((level_stream = fopen(backattr_fname,"rb")) == NULL) 
   { 
      sprintf(abort_string,"%s not found",backattr_fname); 
      abort_game(); 
   } 
   fread(background_attributes,sizeof(char),240,level_stream); 
   fclose(level_stream); 
   /* calculate the maximum tile origin */ 
   world_maxx = (ncols - 20) * 16; 
   world_maxy = (nrows - 12) * 16 - 8; 
} 
/******************************************************************/ 
void page_copy(int ymin) 
{ 
   /* copy both the video memory and the layout array */ 
 
   if (ymin == vpo)         /* visual to hidden */ 
   { 
      fg_transfer(0,351,vpo,vpb,0,hpb,0,0); 
      memcpy(layout[hidden],layout[visual],22*15); 
   } 
   else                    /* hidden to visual */ 
   { 
      fg_transfer(0,351,hpo,hpb,0,vpb,0,0); 
      memcpy(layout[visual],layout[hidden],22*15); 
   } 
} 
/******************************************************************/ 
void page_fix() 
{ 
   /* if the scrolling flags were set, do the video blits and 
      update the layout array */ 
 
   register int i; 
 
   if (warped)            /* warped -- just replace all the tiles */ 
   { 
      warp(world_x,world_y); 
      return; 
   } 
   else if (scrolled_left && scrolled_up)    /* diagonal scrolls */ 
   { 
      fg_transfer(0,335,vpo,223+vpo,16,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
      for (i = 0; i < 21; i++) 
         memcpy(&layout[hidden][i+1][1],layout[visual][i],14); 
   } 
   else if (scrolled_left && scrolled_down) 
   { 
      fg_transfer(0,335,16+vpo,vpb,16,223+hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i+1],&layout[visual][i][1],14);
} 
   else if (scrolled_right && scrolled_up) 
   { 
      fg_transfer(16,351,vpo,223+vpo,0,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
 
      for (i = 0; i < 21; i++) 
         memcpy(&layout[hidden][i][1],layout[visual][i+1],14); 
   } 
   else if (scrolled_right && scrolled_down) 
   { 
      fg_transfer(16,351,16+vpo,vpb,0,223+hpo,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i],&layout[visual][i+1][1],14);

   
} 
   else if (scrolled_left)               /* horizontal scrolls */ 
   { 
      fg_transfer(0,335,vpo,vpb,16,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i+1],layout[visual][i],15); 
   } 
   else if (scrolled_right) 
   { 
      fg_transfer(16,351,vpo,vpb,0,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i],layout[visual][i+1],15); 
   } 
   else if (scrolled_up)                /* vertical scrolls */ 
   { 
      fg_transfer(0,351,vpo,223+vpo,0,hpb,0,0); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
      for (i = 0; i < 22; i++) 
         memcpy(&layout[hidden][i][1],layout[visual][i],14); 
   } 
   else if (scrolled_down) 
   { 
      fg_transfer(0,351,16+vpo,vpb,0,223+hpo,0,0); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
      for (i = 0; i < 22; i++) 
         memcpy(layout[hidden][i],&layout[visual][i][1],14); 
   } 
} 
/******************************************************************/ 
void put_foreground_tile(int xtile,int ytile) 
{ 
   int tile_num; 
   int x,y; 
   int x1,x2,y1,y2; 
 
   /* get the tile number */ 
   tile_num = (int)foreground_tile[xtile+tile_orgx][ytile+tile_orgy]; 
 
   /* 28 or greater == no foreground tile here */ 
   if (tile_num <= 27) 
   { 
 
      /* calculate the source and destination coordinates */ 
      y1 = (tile_num/2)*16+480; 
      y2 = y1+15; 
      x1 = 320+tile_num%2 * 16; 
      x2 = x1 + 15; 
      x = xtile*16; 
      y = ytile*16+15; 
 
      /* transfer the foreground tile (transparent video-video blit) */ 
      fg_tcxfer(x1,x2,y1,y2,x,y+hpo,0,0); 
   } 
} 
/******************************************************************/ 
void put_tile(int xtile,int ytile) 
{ 
   int tile_num; 
   int x,y; 
   int x1,x2,y1,y2; 
 
   /* get the tile number from the background array */ 
   tile_num = (int)background_tile[xtile+tile_orgx][ytile+tile_orgy]; 
 
   /* calculate source and destination coordinates */ 
   x1 = (tile_num%20)*16; 
   x2 = x1+15; 
   y1 = (tile_num/20)*16 + tpo; 
   y2 = y1+15; 
   x = xtile*16; 
   y = ytile*16+15; 
 
   /* transfer the tile */ 
   fg_transfer(x1,x2,y1,y2,x,y+hpo,0,0); 
} 
/******************************************************************/ 
void rebuild_background() 
{ 
   /* put all the necessary background tiles on the hidden page */ 
   register int i,j; 
 
   for (i = 0; i < 22; i++) 
   { 
      for (j = 0; j < 15; j++) 
      { 
         /* check and make sure you need a tile there */ 
         if (layout[hidden][i][j]) 
         { 
            put_tile(i,j); 
 
            /* reset the layout array */ 
            layout[hidden][i][j] = FALSE; 
         } 
      } 
   } 
} 
/******************************************************************/ 
void rebuild_foreground() 
{ 
   /* put all the necessary foreground tiles on the hidden page */ 
   register int i,j; 
 
   for (i = 0; i < 22; i++) 
   { 
      for (j = 0; j < 15; j++) 
      { 
         /* check and make sure you need a tile there */ 
         if (layout[hidden][i][j]) 
            put_foreground_tile(i,j); 
      } 
   } 
} 
/******************************************************************/ 
int scroll_down(int npixels) 
{ 
   /* scroll more than one column, just redraw the whole screen */ 
   if (npixels >= 16) 
   { 
      world_y = tile_orgy*16 + screen_orgy; 
      world_y = MIN(world_maxy,world_y+npixels); 
      world_x = tile_orgx*16 + screen_orgx; 
      warped = TRUE; 
   } 
 
   /* less than one column, no need to draw new tiles */ 
   else if (screen_orgy <= 40-npixels) 
   { 
      screen_orgy+=npixels; 
   } 
 
   /* need to scroll one row down */ 
   else if (tile_orgy < nrows - 15) 
   { 
      tile_orgy++; 
      screen_orgy-=(16-npixels); 
      scrolled_down = TRUE; 
   } 
   else /* can't scroll down */ 
   { 
      return(ERR); 
   } 
   return(OK); 
} 
/******************************************************************/ 
int scroll_left(int npixels) 
{ 
   /* scroll more than one column, just redraw the whole screen */ 
   if (npixels > 16) 
   { 
      world_x = tile_orgx*16 + screen_orgx; 
      world_x = MAX(0,world_x-npixels); 
      world_y = tile_orgy*16 + screen_orgy; 
      warped = TRUE; 
   } 
 
   /* less than one column, no need to draw new tiles */ 
   else if (screen_orgx >= npixels) 
   { 
      screen_orgx-= npixels; 
   } 
 
   /* need to scroll one column to the left */ 
   else if (tile_orgx > 0) 
   { 
      tile_orgx--; 
      screen_orgx+=(16-npixels); 
      scrolled_left = TRUE; 
   } 
 
   /* can't scroll left */ 
   else      return(ERR); 
 
   return(OK); 
} 
/******************************************************************/ 
int scroll_right(int npixels) 
{ 
   /* scroll more than one column, just redraw the whole screen */ 
   if (npixels > 16) 
   { 
      world_x = tile_orgx*16 + screen_orgx; 
      world_x = MIN(world_maxx,world_x+npixels); 
      world_y = tile_orgy*16 + screen_orgy; 
      warped = TRUE; 
   } 
 
   /* less than one column, no need to draw new tiles */ 
   else if (screen_orgx <= 32-npixels) 
   { 
      screen_orgx+=npixels; 
   } 
 
   /* need to scroll one column to the right */ 
   else if (tile_orgx < ncols - 22) 
   { 
      tile_orgx++; 
      screen_orgx-=(16-npixels); 
      scrolled_right = TRUE; 
   } 
   else /* can't scroll right */ 
   { 
      return(ERR); 
   } 
   return(OK); 
} 
/******************************************************************/ 
int scroll_up(int npixels) 
{ 
   /* scroll more than one column, just redraw the whole screen */ 
   if (npixels >= 16) 
   { 
      world_y = tile_orgy*16 + screen_orgy; 
      world_y = MAX(0,world_y-npixels); 
      world_x = tile_orgx*16 + screen_orgx; 
      warped = TRUE; 
   } 
 
   /* less than one column, no need to draw new tiles */ 
   else if (screen_orgy >= npixels) 
   { 
      screen_orgy-=npixels; 
   } 
 
   /* need to scroll one row up */ 
   else if (tile_orgy > 0) 
   { 
      tile_orgy--; 
      screen_orgy+=(16-npixels); 
      scrolled_up = TRUE; 
   } 
   else /* can't scroll up */ 
   { 
      return(ERR); 
   } 
   return(OK); 
} 
/******************************************************************/ 
void swap() 
{ 
   /* vpo = visual page offset, vpb = visual page bottom */ 
   /* hpo = hidden page offset, vpb = hidden page bottom */ 
 
   vpo = 240 - vpo;  /* toggle between 0 and 240 */ 
   hpo = 240 - hpo; 
 
   vpb = vpo+239; 
   hpb = hpo+239; 
 
   /* toggle hidden and visual page */ 
   visual = !visual; 
   hidden = !hidden; 
 
   /* pan to the new visual page */ 
   fg_pan(screen_orgx,screen_orgy+vpo); 
} 
/******************************************************************/ 
void warp(int x,int y) 
{ 
   register int i,j; 
 
   if (x < 16)                 /* calculate the tile X origin */ 
   { 
      tile_orgx = 0; 
      screen_orgx = x; 
   } 
   else if (x >= world_maxx) 
   { 
      x = world_maxx; 
      tile_orgx = x/16-2; 
      screen_orgx = 32; 
   } 
   else 
   { 
      tile_orgx = x/16 - 1; 
      screen_orgx = x%16 + 16; 
   } 
 
   if (y < 16)                 /* calculate the tile Y origin */ 
   { 
      tile_orgy = 0; 
      screen_orgy = y; 
   } 
   else if (y >= world_maxy) 
   { 
      y = world_maxy; 
      tile_orgy = y/16-2; 
      screen_orgy = 40; 
   } 
   else 
   { 
      tile_orgy = y/16 - 1; 
      screen_orgy = y%16 + 16; 
   } 
 
   for (i = 0; i < 22; i++)    /* draw all the tiles */ 
   { 
      for (j = 0; j < 15; j++) 
      { 
         put_tile(i,j); 
         put_foreground_tile(i,j); 
      } 
   } 
   /* update the layout array */ 
   memset(layout[hidden],0,15*22);
} 

Game Scrolling versus Level Editor Scrolling

Let's take a closer look at the game-scrolling functions and compare them to the code used for scrolling in the game editor. The scrolling action performed in the game uses the same video memory layout and data structures as the scrolling code in the level editor. There are, however, some differences in the way the code works. In the level editor, scrolling was a leisurely process. We only scrolled occasionally, and when we did, there was no particular need for speed. We never flipped pages, except when we scrolled beyond the edge of the screen. We also didn't need to perform any animation.

In our game, Tommy's Adventures, the action is fast-paced and furious. We expect the screen to be in constant motion, which means we must be ready to scroll immediately at the touch of a key. Not only that, we are constantly animating the screen and flipping pages, whether or not a scroll is involved.

In the level editor, scrolling only occurred in four directions: up, down, left and right. Scrolling was done in 16-pixel increments. In the game, scrolling can happen diagonally or at any angle. That is, we may want to scroll two pixels up and 10 pixels to the right in one frame. We need to be completely flexible in our ability to scroll.

The scrolling functions in the level editor were also self-contained. The scrolling calculations and blits were all performed in the same function. In the game, the scrolling tasks are handled differently. The scrolling code is spread out over several functions. First the calculations are done in one or more scrolling functions (scroll_up(), scroll_down(), scroll_left(), scroll_right(), or warp()), then the screen is updated in a function called page_fix(). We'll be exploring the scrolling functions soon, but before we get into that, let's back up a little and start at the beginning, where the level data is loaded and displayed.

Loading the Level

The level data is loaded by the load_level() function. Tiles are stored in a manner similar to the way they are stored in the level editor. The foreground tiles are displayed first, and then copied in two chunks to the area where they need to be displayed, as shown in Figure 11.1.

 
/* display the foreground tiles */ 
fg_move(0,tpo); 
fg_showpcx(foreground_fname,2); 
 
/* re-organize the foreground tiles to conserve video memory */ 
fg_transfer(0,31,480,591,320,591,0,0); 
fg_transfer(32,63,480,591,320,703,0,0); 

Figure 11.1 Loading the foreground tiles.

The background tiles are then loaded into the area where the foreground tiles used to be, covering them up. At this point, the screen looks like Figure 11.2.

 
/* display the background tiles */ 
fg_move(0,480); 
fg_showpcx(background_fname,2); 

Figure 11.2 Loading the background tiles.

PCX files can potentially change the palette colors. Since tile colors are unpredictable, it is possible the PCX files may have clobbered the sprite colors. To fix this, the next thing load_level() does is call fix_palettes() to fix the first 32 palette colors. (The fix_palettes() function is in the TOMMY.C source file.)

 
/* fix the foreground palettes */ 
fix_palettes(); 

We're now ready to load in the actual level data from a single binary file that is created by the game editor. Once the background tiles are read in, they are stored in the array background_tile; the foreground tiles are stored in foreground_tile. Later, these arrays will be used by the display functions, such as put_tile() and put_foreground_tile(), to display the tiles. To read in the data, only a few simple for loops are required:

 
/* load the level information */ 
if ((level_stream = fopen(level_fname,"rb")) == NULL) 
{ 
   sprintf(abort_string,"%s not found",level_fname); 
   abort_game(); 
} 
 
/* get the rows and columns */ 
fread(&ncols,sizeof(int),1,level_stream); 
fread(&nrows,sizeof(int),1,level_stream); 
 
/* load the background tiles */ 
for (i = 0; i < ncols; i++) 
   fread(&background_tile[i][0],sizeof(char),nrows,level_stream); 
 
/* load the foreground tiles */ 
for (i = 0; i < ncols; i++) 
   fread(&foreground_tile[i][0],sizeof(char),nrows,level_stream); 
fclose(level_stream); 

Once we've read in the foreground and background tile data for the level, we need to read in the attributes for the background tiles. Recall that the attribute data is stored in its own binary file. The attribute data is stored in the array background_attributes:

 
/* load the background tile attributes */ 
if ((level_stream = fopen(backattr_fname,"rb")) == NULL) 
{ 
   sprintf(abort_string,"%s not found",backattr_fname); 
   abort_game(); 
} 
fread(background_attributes,sizeof(char),240,level_stream); 
fclose(level_stream); 

Once the tile data and attributes are safe and snug in their arrays, we need to set two key variables that are used by the scrolling functions:

 
/* calculate the maximum tile origin */ 
world_maxx = (ncols - 20) * 16; 
world_maxy = (nrows - 12) * 16 - 8; 
} 

We're now ready to draw our first screen.

Building Screens

Recall that all of our game screens are built of tiles. The rebuild_background() function builds the background from tiles. It does this quickly, because it does not blit every tile in the background. It only blits the tiles that have changed since the last frame. The function knows which tiles to blit because it examines the layout array. The layout array (discussed Chapter 10) holds a Boolean value for each tile on the screen. If the value in the layout array is 0, the tile has not been changed and does not need to be redrawn. If the layout array value is 1, the tile has been changed, and must be redrawn. When rebuild_background() recognizes that a tile must be redrawn, it calls the put_tile() to blit the tile . After the tile is drawn, the associated value in the layout array is set to 0. This process is simple, but it is quite important because it is responsible for the speed of screen updates. The process of examining and updating the layout array ensures only the parts of the screen that have changed since the last frame will be redrawn.

 
void rebuild_background() 
{ 
   /* put all the necessary background tiles on the hidden page */ 
   register int i,j; 
 
   for (i = 0; i < 22; i++) 
   { 
      for (j = 0; j < 15; j++) 
      { 
         /* check and make sure you need a tile there */ 
         if (layout[hidden][i][j]) 
         { 
            put_tile(i,j); 
 
            /* reset the layout array */ 
            layout[hidden][i][j] = FALSE; 
         } 
      } 
   } 
} 

Notice that we are using register variables for loop indexes to give the funtion a further speed boost.

When the put_tile() function is called, it obtains the actual tile number from the background_tile array, calculates the coordinates for displaying the tile, and then displays the tile by calling Fastgraph's video-to-video blit function, fg_transfer().

 
void put_tile(int xtile,int ytile) 
{ 
   int tile_num; 
   int x,y; 
   int x1,x2,y1,y2; 
 
   /* get the tile number from the background array */ 
   tile_num = (int)background_tile[xtile+tile_orgx][ytile+tile_orgy]; 
 
   /* calculate source and destination coordinates */ 
   x1 = (tile_num%20)*16; 
   x2 = x1+15; 
   y1 = (tile_num/20)*16 + tpo; 
   y2 = y1+15; 
   x = xtile*16; 
   y = ytile*16+15; 
 
   /* transfer the tile */ 
   fg_transfer(x1,x2,y1,y2,x,y+hpo,0,0); 
} 

Rebuilding the foreground is very similar to rebuilding the background. In this case, the function rebuild_foreground() function is called which in turn calls put_foreground_tile() to display the foreground tiles, if there are any.

Using Foreground Tiles

We've already encountered foreground tiles in the game editor. Let's take a closer look at them now. Foreground tiles are stored and displayed in a manner similar to background tiles, but with some important differences. Since foreground tiles are displayed after the sprite, they show up in front of it. This gives the appearance of a sprite moving behind things, such as walking behind a pillar or a wall. Foreground tiles also have a transparent color so you can see through them. This is useful for odd-shaped tiles like bushes and trees, and also for semi-transparent tiles like screens or tunnels with small windows.

I have allowed for 28 foreground tiles in the our game. That isn't very many, but we are starting to run out of video memory, and that's how many I could conveniently fit in on the right side of video memory, as shown in Figure 11.3.

Figure 11.3 Foreground tiles in video memory.

I arranged the foreground in two columns of 14 tiles in the area just to the right of the background tiles. They occupy an area 32x224 pixels. Both the game and the level editor store the foreground tiles in this area. As we saw in Chapter 10, foreground tile information for the level is stored in an array similar to the background tile array:

 
unsigned char far foreground_tile[MAXCOLS][MAXROWS]; 
Foreground tiles are displayed using the put_foreground() function:

 
void put_foreground_tile(int xtile,int ytile) 
{ 
   int tile_num; 
   int x,y; 
   int x1,x2,y1,y2; 
 
   /* get the tile number */ 
   tile_num = (int)foreground_tile[xtile+tile_orgx][ytile+tile_orgy]; 
 
   /* 28 or greater == no foreground tile here */ 
   if (tile_num <= 27) 
   { 
 
      /* calculate the source and destination coordinates */ 
      y1 = (tile_num/2)*16+480; 
      y2 = y1+15; 
      x1 = 320+tile_num%2 * 16; 
      x2 = x1 + 15; 
      x = xtile*16; 
      y = ytile*16+15; 
 
      /* transfer the foreground tile (transparent video-video blit) */ 
      fg_tcxfer(x1,x2,y1,y2,x,y+hpo,0,0); 
   } 
} 
The put_foreground_tile() function is very similar to the put_tile() function listed earlier, except that the source coordinates are calculated differently, and a different Fastgraph video-to- video blit function is called. The fg_tcxfer() function is the transparent color version of Fastgraph's fg_transfer() function. It's a bit slower than fg_transfer(), so when speed is a consideration (as it always is), the use of foreground tiles should be kept to a minimum. Areas in the level with no foreground tiles are given a value of 255 to indicate no foreground tile is there. The put_foreground_tile() function checks the foreground_tile array to see if a foreground tile number is greater than 28. If it is, the put_foreground_tile() function returns without trying to place a foreground tile.

The fg_tcxfer() function does a video-to-video blit with any number of transparent colors. Fastgraph's fg_tcmask() function is used to define the transparent colors. I usually use palette 0 as the transparent color, to be consistent with the sprite transparent color.

Warping

There will be times when we want to update an entire screen of tiles, for example when we display the first frame of a new level. This process is called warping.

To support warping, we've included a function called warp().

 
void warp(int x,int y) 
{ 
   register int i,j; 
 
   if (x < 16)                 /* calculate the tile X origin */ 
   { 
      tile_orgx = 0; 
      screen_orgx = x; 
   } 
   else if (x >= world_maxx) 
   { 
      x = world_maxx; 
      tile_orgx = x/16-2; 
      screen_orgx = 32; 
   } 
   else 
   { 
      tile_orgx = x/16 - 1; 
      screen_orgx = x%16 + 16; 
   } 
 
   if (y < 16)                 /* calculate the tile Y origin */ 
   { 
      tile_orgy = 0; 
      screen_orgy = y; 
   } 
   else if (y >= world_maxy) 
   { 
      y = world_maxy; 
      tile_orgy = y/16-2; 
      screen_orgy = 40; 
   } 
   else 
   { 
      tile_orgy = y/16 - 1; 
      screen_orgy = y%16 + 16; 
   } 
 
   for (i = 0; i < 22; i++)    /* draw all the tiles */ 
   { 
      for (j = 0; j < 15; j++) 
      { 
         put_tile(i,j); 
         put_foreground_tile(i,j); 
      } 
   } 
   /* update the layout array */ 
   memset(layout[hidden],0,15*22);} 
The warp() function calculates the new screen origin and then draws all the tiles. Since this effectively clears all the tiles, the layout array is set to all 0s. We call C's memset() function to accomplish this task quickly.

Flipping Pages

The warp() function only updates the hidden page. This is desireable, because we want the tiles to be drawn in offscreen video memory so we don't create any kind of flickering or screen fragmentation. After the warp, we want to make the hidden page visible. This involves a page flip. We will do many page flips in our game, and all of them are done by calling the function swap().

 
void swap() 
{ 
   /* vpo = visual page offset, vpb = visual page bottom */ 
   /* hpo = hidden page offset, vpb = hidden page bottom */ 
 
   vpo = 240 - vpo;  /* toggle between 0 and 240 */ 
   hpo = 240 - hpo; 
 
   vpb = vpo+239; 
   hpb = hpo+239; 
 
   /* toggle hidden and visual page */ 
   visual = !visual; 
   hidden = !hidden; 
 
   /* pan to the new visual page */ 
   fg_pan(screen_orgx,screen_orgy+vpo); 
} 

The swap() function changes the variables that define the hidden and visual pages. Recall the vpo is the visual page offset and the hpo is the hidden page offset. These values that will be added to the y coordinate of any item that is displayed on one of the pages, such as tiles, sprites, or text. For example, to display text at y=100 on the visual page, you would display the text at y=100+vpo.

Two global variables, hidden and visual, are toggled. These variables will be equal to either 0 or 1. If hidden is 0, then the page at the top is the hidden page and the page at the bottom is the visual page. Similarly, if hidden is 1, the page at the bottom is the hidden page and the page at the top is the visual page. Which ever page is not the hidden page is the visual page. We keep track of these values in the swap() function so we don't need to worry about them elsewhere.

Finally, the swap() function changes the screen origin with a call to fg_pan(). This is where the page flip occurs. We are now looking at a completely different part of video memory as shown in Figure 11.5.

Figure 11.5 The swap() function changes the origin of the visual page.

Copying Pages

Let's assume we are performing our first frame of animation. First we perform a warp to draw the screen on the hidden page. Then we do a page flip to make the hidden page visible. There is one more step we need to perform. At this point, the hidden page and the visual page won't match. In fact, the new hidden page will be blank, because we haven't drawn anything on it yet. We could rebuild all the tiles on the hidden page with another call to warp(), but that is not very efficient. A call to warp() causes 330 tile blits (22 columns x 15 rows) to be performed, each one involving a bit of unnecessary overhead. It will be much faster to just copy the visual page to the hidden page. Then we will have matching pages, which is what we want. The page_copy() function handles the work of copying the visual page to the hidden page (or, less commonly, it can be used to copy the hidden page to the visual page):

 
void page_copy(int ymin) 
{ 
   /* copy both the video memory and the layout array */ 
 
   if (ymin == vpo)         /* visual to hidden */ 
   { 
      fg_transfer(0,351,vpo,vpb,0,hpb,0,0); 
      memcpy(layout[hidden],layout[visual],22*15); 
   } 
   else                    /* hidden to visual */ 
   { 
      fg_transfer(0,351,hpo,hpb,0,vpb,0,0); 
      memcpy(layout[visual],layout[hidden],22*15); 
   } 
} 

We pass the value ymin, which is the y coordinate of the top of either the hidden page or the visual page, to page_copy(). Usually we will pass vpo to page_copy(), to signify we want to copy the visual page to the hidden page.

The page_copy() function calls Fastgraph's fg_transfer() function to do a video-to- video blit. The entire page is copied in one quick function call. Since the two pages now match, their layout arrays should also match. The simplest way to make the layout arrays match is to use C's memcpy() function to copy the appropriate array elements.

Once the pages and the layout arrays match, we can go about the business of updating the hidden page for the next frame. Since the next frame will probably closely resemble the last frame, this will probably involve changing only a few tiles. In general, it is more efficient to call the page_copy() function after every warp and scroll, and then update only the tiles that need to change, than to rebuild the hidden page from scratch.

Scrolling Functions

As you've seen, warping involves rebuilding the entire hidden page from the set of tiles. But the warping function is not the fastest way to scroll a background because it requires 330 blits to redraw the page. Whenever possible, we want to minimize the number of blits. One large blit will always execute faster than several hundred small blits. That is the basis of our fast scrolling technique. As we saw in Chapter 5, we can copy large areas from the visual page to the hidden page, and then fill in a row or column of tiles around the edges to achieve a fast scroll.

There are four scrolling functions in the MAP.C file. These are scroll_down(), scroll_left(), scroll_right(), and scroll_up(). They all work approximately the same way. Let's look at one of them in closer detail to see what is going on behind the scenes:

int scroll_right(int npixels) 
{ 
   /* scroll more than one column, just redraw the whole screen */ 
   if (npixels > 16) 
   { 
      world_x = tile_orgx*16 + screen_orgx; 
      world_x = MIN(world_maxx,world_x+npixels); 
      world_y = tile_orgy*16 + screen_orgy; 
      warped = TRUE; 
   } 
 
   /* less than one column, no need to draw new tiles */ 
   else if (screen_orgx <= 32-npixels) 
   { 
      screen_orgx+=npixels; 
   } 
 
   /* need to scroll one column to the right */ 
   else if (tile_orgx < ncols - 22) 
   { 
      tile_orgx++; 
      screen_orgx-=(16-npixels); 
      scrolled_right = TRUE; 
   } 
   else /* can't scroll right */ 
   { 
      return(ERR); 
   } 
   return(OK); 
} 

The scroll_right() function shown here is a little different than the scroll_right() function we saw in the level editor. In this version of scroll_right(), nothing is changed on the screen. No tiles are redrawn, no areas are copied to the hidden page, and no page flipping occurs. All that happens is some global variables are adjusted. The world_x and world_y variables are updated to reflect the new coordinates of the upper-left corner of the screen. The screen_x and screen_y coordinates are similarly updated. Two Boolean variables, scrolled_right and warped, are updated if necessary to indicate a scroll or a warp has occurred. The work of updating the screen is done after the scroll, in the page_fix() function.

The page_fix() Function

The page_fix() function is called in any frame where a scroll or warp has occurred. Its job is to do the necessary blits to fix the hidden page. It also updates the layout array as needed to reflect the new state of the tiles. Here is the complete function:

void page_fix() 
{ 
   /* if the scrolling flags were set, do the video blits and 
      update the layout array */ 
 
   register int i; 
 
   if (warped)            /* warped -- just replace all the tiles */ 
   { 
      warp(world_x,world_y); 
      return; 
   } 
   else if (scrolled_left && scrolled_up)    /* diagonal scrolls */ 
   { 
      fg_transfer(0,335,vpo,223+vpo,16,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
      for (i = 0; i < 21; i++) 
         memcpy(&layout[hidden][i+1][1],layout[visual][i],14); 
   } 
   else if (scrolled_left && scrolled_down) 
   { 
      fg_transfer(0,335,16+vpo,vpb,16,223+hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i+1],&layout[visual][i][1],14);

   
} 
   else if (scrolled_right && scrolled_up) 
   { 
      fg_transfer(16,351,vpo,223+vpo,0,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
 
      for (i = 0; i < 21; i++) 
         memcpy(&layout[hidden][i][1],layout[visual][i+1],14); 
   } 
   else if (scrolled_right && scrolled_down) 
   { 
      fg_transfer(16,351,16+vpo,vpb,0,223+hpo,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i],&layout[visual][i+1][1],14);

   
} 
   else if (scrolled_left)               /* horizontal scrolls */ 
   { 
      fg_transfer(0,335,vpo,vpb,16,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(0,i); 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i+1],layout[visual][i],15); 
   } 
   else if (scrolled_right) 
   { 
      fg_transfer(16,351,vpo,vpb,0,hpb,0,0); 
      for(i = 0; i< 15; i++) 
         put_tile(21,i); 
      for (i = 0; i < 21; i++) 
         memcpy(layout[hidden][i],layout[visual][i+1],15); 
   } 
   else if (scrolled_up)                /* vertical scrolls */ 
   { 
      fg_transfer(0,351,vpo,223+vpo,0,hpb,0,0); 
      for(i = 0; i< 22; i++) 
         put_tile(i,0); 
      for (i = 0; i < 22; i++) 
         memcpy(&layout[hidden][i][1],layout[visual][i],14); 
   } 
   else if (scrolled_down) 
   { 
      fg_transfer(0,351,16+vpo,vpb,0,223+hpo,0,0); 
      for(i = 0; i< 22; i++) 
         put_tile(i,14); 
      for (i = 0; i < 22; i++) 
         memcpy(layout[hidden][i],&layout[visual][i][1],14); 
   } 
} 

The page_fix() function is called once for each frame. It is called before the sprites are displayed, and after the scrolling functions, which may or may not be called during a frame. The globals that were set in the scrolling functions tell page_fix() whether to do a simple horizontal or vertical scroll, a diagonal scroll, or a complete screen redraw.

The following are the ten cases that page_fix() must consider:

  1. warp
  2. scroll left and up
  3. scroll left and down
  4. scroll right and up
  5. scroll right and down
  6. scroll left
  7. scroll right
  8. scroll up
  9. scroll down
  10. no scroll

If no scrolling functions were called during a frame, the page_fix() function returns without doing anything.

Adjusting the Layout Array

The screen updates in the page_fix() function do not have any affect on the sprites. If there were sprites on the visual page, they will be copied to the hidden page.That means the hidden page will contain both clean tiles and tiles covered with sprites. We will need to have access to that information for the next frame. We will need to know which tiles will need to be updated. The layout array for the hidden page will need to be updated to show the location of the sprites that were copied from the visual page. Unfortunately, we can't simply copy the visual page layout array to the hidden page layout array as we did in the page_copy() function. This time, the graphics on the hidden page have shifted slightly, in one or two directions. We will have to copy the layout array in such a way as to reflect the shift.

A horizontal scroll is simpler than a diagonal scroll, so let's look at that cases first. When the screen scrolls to the right, the code looks like this:

 
else if (scrolled_right) 
{ 
   fg_transfer(16,351,vpo,vpb,0,hpb,0,0); 
   for(i = 0; i< 15; i++) 
      put_tile(21,i); 
   for (i = 0; i < 21; i++) 
      memcpy(layout[hidden][i],layout[visual][i+1],15); 
} 

The graphics are copied from the visual page to the hidden page, shifted 16 pixels to the left. A new row of graphics will appear on the right. Then the visual page layout array is copied into the hidden page layout array, and all the columns are shifted, as shown in Figure 11.6.

Figure 11.6 Copying the layout array during a scroll.

The C runtime library function memcpy() is used to copy 21 columns of tiles, shifted one column to the left. Column 1 becomes column 0, column 2 becomes column 1, and so on. Fifteen tiles are copied in each column, representing the 15 rows of tiles.

Diagonal Scrolling

To scroll the screen diagonally, we must handle both the horizontal and the vertical cases at the same time. This is tricky. The code for copying the graphics and the layout array left and up looks like this:

 
else if (scrolled_left && scrolled_up)    /* diagonal scrolls */ 
{ 
   fg_transfer(0,335,vpo,223+vpo,16,hpb,0,0); 
   for(i = 0; i< 15; i++) 
      put_tile(0,i); 
   for(i = 0; i< 22; i++) 
      put_tile(i,0); 
   for (i = 0; i < 21; i++) 
      memcpy(&layout[hidden][i+1][1],layout[visual][i],14); 
}

This code is condensed and not too obvious, but it works well and is quite fast. In this example, the screen is being scrolled up and to the left. As Figure 11.7 shows, area 'A' is copied from the visual page to the hidden page. Row 14 and column 21 on the visual page are discarded. A new row 0 and column 0 are generated on the hidden page. The visual page layout array is then copied into the hidden page layout array, properly shifted up and to the left.

Figure 11.7 The process of scrolling diagonally.

The page_fix() function is necessary to handle the case of diagonal scrolling. If it were not for the case of diagonal scrolling, we could do the screen blits and level array updates in the scrolling functions. It would be simpler if we could perform the diagonal scrolling in two steps--first scrolling left, then scrolling down--but unfortunately, this won't work. Not only would it be slower, it would also cause the vertical scroll to wipe out the horizontal scroll. Figure 11.8 shows what the hidden page and visual page look like during a diagonal scroll.

Figure 11.8 Diagonal scrolling.

If you need to scroll both left and up in one frame, you can not do it in two functions. You must handle the diagonal scrolling as a separate case in the page_fix() function.

Speed Considerations

Throughout this chapter I have stressed certain optimizations. The most important is to try to only update the part of the screen that needs to be changed. Screen blits, either from RAM to video, or from video to video, are one of the most time consuming parts of game programming. It is usually worthwhile to take whatever steps you can to reduce screen blits. Clever background processing of the layout array can help to optimize operations, such as diagonal scrolling. It is much faster to perform tricky string copies in RAM than to do unnecessary screen blits.

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.