Channels ▼
RSS

Parallel

Designing Isometric Game Environments

Source Code Accompanies This Article. Download It Now.


SP 96: Designing Isometric Game Environments

Nate is a computer engineering major at Lehigh University. He can be reached at nsg2@lehigh.edu.


Tile-based games have been around almost as long as two-dimensional (2-D) arrays. I can remember, for instance, sitting in front of a Commodore 64, carefully maneuvering a tiny "pixelesque" humanoid from square to square, thinking, "Hey, this is great!"

As computer games became more sophisticated, however, simple, top-down, tile-based games gave way to bigger and better things. Why? Because someone decided to look at things from a slightly different angle. The emergence of the isometric point of view took tile-based games somewhere they had never been before--into the third dimension. After playing through a prime example of this isometric ingenuity (Microprose's award-winning X-com), I decided to create my own multilevel, isometric environment. As Figure 1 shows, the Axoview Engine 1.0 is the result of my efforts. In this article, I'll describe how I designed the engine, and provide an overview of its inner workings.

Putting Things in Perspective

You need to keep two things in mind when designing an isometric layout:

  • Ease of visibility.
  • Ease of putting things together.
Of all the isometric possibilities, the axonometric perspective provides excellent vision clarity and allows for each cube to be easily broken down into unique components; see
Figure 2(a). The multilevel, tile-based world is constructed entirely from axonometric cubes called "cell blocks." As Figure 2(b) illustrates, each cell block is composed of separate tiles:

  • The base tile, which serves as a floor or ceiling.
  • X- and y-wall tiles, for walls, fences, and the like.
  • The prop tile, for stationary objects such as trees and chairs.

Getting Coordinated

Cell blocks are arranged side-by-side on a 2-D grid, bearing a strong resemblance to a top-down tile layout. The cell-block grid, however, is rotated at 45 degrees. Now the origin of the rotated grid lies at the top-most (farthest) corner, and the x- and y-map grid axes increase downward and to the right or left, respectively, as in Figure 3. Therefore, the x-wall of each cell block is the wall players come in contact with as they traverse the grid in the x-direction; likewise for the y-wall. This Cartesian gridmap of cell blocks is referred to as "map coordinates."

To locate various positions within each cell block, the map grid is further broken down into a uniform number of virtual partitions per cell block. For practical reasons, the number of partitions must be an integral power of 2. These virtual partitions are referenced using world coordinates. Consequently, each cell block has a local coordinate system (with the origin in the top corner of the cell block) created by the world-coordinate partitions. This "grid within a grid" is referenced using "cell coordinates" (see Figure 3).

Finally, a familiar coordinate system must be defined. The grid of pixels that makes up the screen display will be referred to as "screen coordinates."

Base Tile Design

The base tile is most important because it is directly related to the layout of the cell-block grid. Its design hinges on two crucial choices: edge slope and edge length. For standard 320x200 VGA displays, a slope of 0.5 pixels produces the best results. This particular slope creates a series of two-pixel steps which go down and right in the x-direction, and down and left in the y-direction. The "length" of each edge is simply the number of two-pixel steps per edge. This length must correspond to the number of virtual cell-block partitions chosen earlier.

By connecting the ends of four edges to form a diamond (the base tile), a graphic representation of the local cell-coordinate grid is formed; see Figure 4(a). Subsequently, by aligning adjacent base tiles so that the edges transfer smoothly in both the x- and y-directions (no disturbance in edge slope), a graphical representation of the rotated cell-block (map) grid is created. This also reveals an extra edge of "padding" along the bottom half of each base tile; see Figure 4(c). This padding completes the design of the base tile.

The alignment of base tiles also provides several important cell-block dimension constants defined in Listings One and Two. Figure 4(b) illustrates these dimensions.

Wall Tile Design

It should be noted that the base tile's edges of padding form slots between adjacent base tiles. This is a convenient place for some walls. The width of each wall tile is a function of the base tile width: wall width = (base tile width/2)+2. The height of the wall must be an even number; choose the height with user visibility in mind.

Prop Tile Design

Once the base and wall tiles are designed, the prop-tile dimensions are straightforward. The width of the prop tile is simply the same width as the base tile. The prop-tile height is equivalent to the height of the connected base and wall tiles decremented by 1.

Leveling Things Out

Multiple levels will be dealt with by stacking several identical cell-block grids directly on top of one another. The base tiles of one level should rest comfortably on the wall tiles of the level below. It should be noted that the base tile of a certain cell block will serve as the ceiling tile for the cell block directly below it.

Basic Engine Structures: The Tile Maps

With the abstract design of the engine out of the way, how do you represent the axonometric layout in memory?

Generally, the axonometric world consists of a fixed number of levels. Each level consists of a 2-D grid of cell blocks, and each cell block consists of four unique tile components. This layout can be represented by four 2-D arrays (one array for each component). The first dimension of each array is a level index and the second is a cell-block location index (for the 2-D grid map). These tile maps are arrays of bytes, where each byte holds a unique tile-identification number. Therefore, every cell block in the world has a corresponding value in the base_map, xwall_ map, ywall _map, and prop_map. See Listing One for the tile-map declarations.

The Tile Class

Now that a means of storing tile layouts has been established, the tiles must be capable of organizing themselves. The Tile class organizes tile bitmap data through four data elements. The most important of these data elements is a pointer to an array of tile bitmaps. Each tile bitmap is accessed through an identification number (which works out well because that is exactly what is stored in the tile maps). The Tile class keeps track of the width and height of each individual bitmap (all bitmaps for an instance of the Tile class must have identical dimensions), and also of the size in bytes of each bitmap. The Tile class is declared in Listing Three.

Not surprisingly, the Axoview Engine declares four instances of the tile class: base_tiles, xwall_tiles, ywall_tiles, and prop_tiles (see Listing One). To declare an instance of the Tile class, the class constructor requires three arguments: the number of tiles, the width of each tile, and the height of each tile. The constructor calculates the size of each bitmap (widthxheight), and, more importantly, allocates memory for each of the tile bitmaps (see Listing Four).

But how do the bitmaps make it to the big screen? The Tile class' member function blit() takes care of everything. You just need to be sure to pass it three things: the tile identification number, screen coordinates (where you would like the bitmap displayed), and a pointer to the off-screen buffer on which it should do the blitting. How exactly does the blit() function blit? The typical way of rendering a tile-based display would be to use the painter's algorithm. The painter's algorithm draws the tiles back to front, so that the tiles in the foreground cover the tiles beneath them (much like a painter would paint a picture). Standard bitmap transparency also would be applied, where each byte of the bitmap would be checked for transparency (usually a value of 0). If the byte were "transparent," it would not be drawn to the off-screen buffer. While this method works, it can be highly inefficient, drawing several levels of cell-block tiles on top of each other, even though many of the tiles would not be seen at all. It turns out that there is a way to draw only what can be seen. I refer to it as the "reverse painter's algorithm."

The Reverse Painter's Algorithm

The reverse painter's algorithm (RPA) eliminates the blitting of "covered over" portions of tile bitmaps--only the pixels that will be seen in the end are drawn. The algorithm can be broken into several steps. First, fill the entire off-screen buffer (destination viewport) with a string of transparent (zero) values as quickly as possible. Second, calculate the order in which the tiles would be drawn using the normal painter's algorithm and reverse it (draw tiles in the foreground first). The cell-block tile-display order becomes: prop, x-wall, y-wall, base. To draw from front to back according to the RPA, the top-most level of cell blocks should be drawn first. Therefore, draw each level from left to right across the screen, starting with the bottom-most row, and ending with the top-most row. A significant problem still remains to be addressed, however--can the blit() function keep the later-drawn background tiles from copying over the foreground tiles?

RPA-Style Drawing with Tile::blit()

To preserve speed, the blit() function performs all clipping outside of the main blitting loop. It first clips the edges of the tile bitmap to the viewport, adjusting the starting and ending coordinates appropriately, and calculates the necessary blitting offsets. It then loops through the unclipped portion of the tile bitmap and copies the "permissible" bytes to the off-screen buffer.

It is here that the "magic" takes place. Instead of checking each byte of the bitmap (the source byte) for transparency, the blit() function checks the corresponding byte in the off-screen buffer (the target byte) for transparency (recall that the off-screen buffer was initially filled with transparent values). If the target byte is transparent (nothing has been drawn there yet), the blit is deemed "permissible" and the function copies the source byte to the target byte, as in Figure 5. The only drawback to this method is that transparent source bytes may be needlessly copied over transparent target bytes. This drawback is negligible, however, when compared to the inefficiency of copying hundreds of tiles on top of one another. Listing Four presents the full definition of Tile::blit().

It Was a Time to be Rendered

The next step involves rendering the multilevel cell-block environment. The engine function draw_tilesRPA() assumes full responsibility for this task. The function draw_tilesRPA() requires five arguments:

  • The pair of world coordinates around which the display should be centered.
  • The level that will serve as the top-most level.
  • The level around which the display should be centered.
  • A pointer to the off-screen buffer.
However, before draw_tilesRPA() can begin the process of rendering the layout to the screen, we need a method for converting world coordinates to screen coordinates (and vice versa).

Going from World to Screen, and Back Again

To convert from world coordinates to screen coordinates, you need to compare the local cell coordinates of a base tile with its local screen coordinates; see Figure 6. This comparison shows that a change of +1 along the cell-coordinate x-axis is equivalent to the following screen-coordinate changes: +2 in the x-direction, and +1 in the y-direction. Table 1 lists similar comparisons.

These comparisons suggest the pair of equations in Example 1(a) for converting from a change in world (or cell) coordinates to a change in screen coordinates. Solving this pair of equations for the change in world (or cell) coordinates yields the pair of equations in Example 1(b), which convert from a change in screen coordinates to a change world coordinates. With these conversions at the ready, the rendering process may begin.

Rendering with Function draw_tilesRPA()

The pair of world coordinates (x,y) passed to draw_tilesRPA() corresponds to the center of the viewport. However, the rendering process does not start in the center, but instead in the lower-left corner (RPA requirement). Therefore, the screen-to-world conversion formulas are used to find the world coordinates for the lower-left corner of the viewport. This is done by plugging the change in screen coordinates (distances from center of viewport to lower-left corner) into the formulas, then adding the equivalent change in world coordinates to the original pair of world coordinates (center of viewport). The resulting pair of world coordinates correspond to the lower-left corner of the viewport. This world-coordinate location is called the "world intersection point;" see Figure 7.

Because the rendering process starts on the top-most level (not necessarily the current level), the world intersection point must be adjusted to compensate for the difference in screen height between levels. This change along the screen y-axis is equivalent to the distance (in pixels) from the top of a wall tile to the top of its connected base tile (there is no change along the x-axis). Using the conversion formulas again, the equivalent level change in world coordinates is added to the world intersection point for each level that needs to be drawn above the current level. This centers the display around the current level.

The Main Rendering Loop

Now for the main looping procedure. Starting with the top-most level, the function calculates the map coordinates of the cell block that contains the world-intersection point. This is done by a technique I call "binary masking," which uses the bitwise AND operator and a certain binary "mask" to retain (or clip) a wanted or unwanted power of 2 from a number (in this case, the world coordinates). Assuming that the coordinates are 16-bit integers, the binary mask is calculated as Map Coordinate Mask = 0xffff-- (# of cell partitions - 1). This technique works because the number of cell partitions was chosen to be an integral power of 2. The map location (cell block) that contains the world intersection point is called the "intersection block" (see Figure 7).

Next, the local cell coordinates of the world intersection point are calculated using the same binary-masking technique. However, in this case, a different power of 2 needs to be clipped. Therefore, the cell-coordinate mask is defined as Cell Coordinate Mask = ( # of cell partitions - 1 );. The world-to-screen conversion formulas are then used to find the screen offsets of the calculated cell coordinates. These screen offsets, once adjusted to the viewport, yield the screen coordinates of the intersection block.

The function next finds the map and screen coordinates for the starting block (the place where the rendering process will finally begin). The starting block is found by dropping down a certain number of cell blocks from the intersection block to account for the fact that walls and props of off-screen base tiles may still be visible. Hence, the higher you make the walls, the farther down you have to move the starting block; see Figure 7.

The time to render has finally come. Using the starting block as a starting point, the tiles for each cell block are drawn from left to right (according to the RPA). Each tile is drawn through a call to the appropriate Tile::blit(), after getting the tile-identification number from the appropriate tile map. After each row is drawn, the starting block is moved alternatively up and to the left and up and to the right, as in Figure 7. Once all the rows that fit into the viewport have been drawn, the process is done for the current level. The world intersection point is then adjusted for the next level down, and the main rendering process continues until all the levels have been drawn. That's all there is to it! The Axoview Engine is ready to display your worlds!

Conclusion

You can fill the tile maps with level layouts, fill the Tile class bitmaps with artwork, and create axonometric, multilevel environments (with applications beyond just game worlds). Moveable characters (and objects) could be implemented by incorporating an object map (with the same dimensions as the tile maps) that contains a linked list to all the objects currently in a cell block. These characters could then be moved around the world using standard, cell-oriented collision-detection techniques.

Although the Axoview Engine was written specifically for Mode 13h in DOS (assembly-language files are available electronically; see "Availability," page 3), it can easily be ported to any platform--in some cases with just a few changes to the Tile class' blitting function. Portability aside, however, a multitude of possible projects stem from the Axoview Engine as it now stands. Animated tile code could be thrown into the blitting algorithm, map rotation could be incorporated to allow the user to change the perspective, and so on. The possibilities are limited only by your imagination.

Figure 1: The Axoview Engine v1.0.

Figure 2: (a) The axonometric cube (cell block); (b) component breakdown of the cell block.

Figure 3: The axonometric map-coordinate grid.

Figure 4: (a) Building the base tile from equal edges; (b) base-tile dimensions to remember; (c) the finished base tile.

Figure 5: Reverse painter's algorithm tile-transparency blitting technique.

Figure 6: Base-tile coordinate-conversion analysis.

Figure 7: Main rendering process.

Example 1: (a) Equations for converting from a change in world (or cell) coordinates to a change in screen coordinates; (b) equations which convert a change in screen coordinates to a change in world coordinates.

(a)
     dX(screen) = 2*dX(world) - 2*dY(world)
     dY(screen) = dX(world) + dY(world)

(b)
     dX(world) = [ dX(screen) + 2*dY(screen) ] / 4
     dY(world) = [ 2*dY(screen) - dX(screen ) ] / 4

Table 1: Cell-coordinates-to-screen-coordinates conversion.

Change in Cell          Change in Screen
Coordinates          Coordinate



dX     dY     dX     dY

+1     0     +2     +1

0     +1     -2     +1

     

+1     +1     0     +2

+1     -1     +4     0

-1     +1     -4     0

-1     -1     0     -2

-1     0     -2     -1

0     -1     +2     -1

Listing One

/********************************************************************
 AXOVIEW.H
 Header file for the Axoview Engine v1.0
 Contains engine constants, global data structures, and function prototypes.
 Written by Nate Goudie  1996
********************************************************************/
// Defines for Tile Totality Constants
#define  BASE_COUNT        7        // Number of base tiles
#define  WALL_COUNT        5        // Number of wall tiles
#define  PROP_COUNT        3        // Number of prop tiles
// Defines for Tile Bitmap Dimension Constants
#define  BASE_WIDTH        30       // Width of base tile
#define  BASE_HEIGHT       16       // Height of base tile
#define  WALL_WIDTH        17       // Width of wall tile
#define  WALL_HEIGHT       28       // Height of wall tile
#define  PROP_WIDTH        30       // Width of prop tile
#define  PROP_HEIGHT       33       // Height of prop tile
// Defines for Viewport Dimension Constants
#define  FULL_SCREEN_SIZE  64000    // Size of a full screen buffer
#define  VIEWPORT_X_START  0        // Starting X coord of viewport
#define  VIEWPORT_X_END    319      // Ending X coord of viewport
#define  VIEWPORT_Y_START  0        // Starting Y coord of viewport
#define  VIEWPORT_Y_END    199      // Ending Y coord of viewport
#define  VIEWPORT_WIDTH    320      // Horizontal width of viewport
#define  VIEWPORT_HEIGHT   200      // Vertical height of viewport
// Defines for Map Dimension Constants
#define  LEVEL_MAX         4        // Maximum number of levels
#define  XMAP_MAX          50       // Maximum X dimension of map
#define  YMAP_MAX          50       // Maximum Y dimension of map
#define  SCROLL_STEP       2        // World coordinate scroll step
#define  MAX_CELL_UNITS    8        // No. of cell units per map unit
#define  CELL_COORD_MASK   0x0007   // Cell coordinate mask
#define  MAP_COORD_MASK    0xfff8   // Map coordinate mask
#define  X_VPTM_OFFSET     10       // Viewport-to-map X offset
#define  Y_VPTM_OFFSET     90       // Viewport-to-map Y offset
// Defines for Cell Block Dimension Constants
#define  CELL_START_X      14       // Init X for cell -> screen op
#define  CELL_START_Y      0        // Init Y for cell -> screen op
#define  CELL_FULL_WIDTH   32       // X of cellA - X of adj cellB
#define  CELL_HALF_WIDTH   16       // Half the full width
#define  CELL_FULL_HEIGHT  16       // Height of base tile
#define  CELL_HALF_HEIGHT  8        // Y of cellA - Y of next cellB
#define  XWALL_XOFFSET     -1       // X offset for the cell's X-Wall
#define  YWALL_XOFFSET     +14      // X offset for the cell's Y-Wall
#define  WALL_YOFFSET      -20      // Y offset for the cell's walls
#define  PROP_YOFFSET      -18      // Y offset for the prop tile
#define  LEVEL_ADJUST      10       // Offset to move up/down a level
// Defines for view rectification of walls
#define  START_YS_OFFSET   32       // Initial Y screen offset
#define  START_XS_OFFSET    0       // Initial X screen offset
#define  START_YM_OFFSET    2       // Initial Y map offset
#define  START_XM_OFFSET    2       // Initial X map offset
#define  START_XADD        16       // Initial Xadd value
// Declare global data structures
unsigned char palette[256*3];   // array for the color palette
// Declare arrays for the tile maps
unsigned char base_map[LEVEL_MAX][XMAP_MAX*YMAP_MAX];
unsigned char xwall_map[LEVEL_MAX][XMAP_MAX*YMAP_MAX];
unsigned char ywall_map[LEVEL_MAX][XMAP_MAX*YMAP_MAX];
unsigned char prop_map[LEVEL_MAX][XMAP_MAX*YMAP_MAX];
// Declare instances of the Tile class
Tile  base_tiles(BASE_COUNT, BASE_WIDTH, BASE_HEIGHT),
      xwall_tiles(WALL_COUNT, WALL_WIDTH, WALL_HEIGHT),
      ywall_tiles(WALL_COUNT, WALL_WIDTH, WALL_HEIGHT),
      prop_tiles(PROP_COUNT, PROP_WIDTH, PROP_HEIGHT);
// Function prototypes
void draw_tilesRPA(int x, int y, int top_level, int current_level,
           unsigned char far *screenbuf);
int  load_files();

Listing Two

/********************************************************************
 AXOVIEW.CPP
 The Axoview Engine v1.0. An engine which renders free scrolling tile-based 
 environments in axonometric perspective. Main program and engine functions.
 Written by Nate Goudie  1996
********************************************************************/
#include    <stdio.h>
#include    <dos.h>
#include        <conio.h>
#include    <alloc.h>
#include    <mem.h>
#include        <iostream.h>
#include        <process.h>
#include    "tile.h"
#include    "screen.h"
#include        "axoview.h"
void main()
    {
    clrscr();
    // Allocate memory for the offscreen buffer
    unsigned char far *screenbuf;
    screenbuf=new unsigned char[FULL_SCREEN_SIZE];
    // Get old video mode number
    int oldmode=*(int *)MK_FP(0x40,0x49);
    // Create pointer to video memory
    char far *screen=(char far *)MK_FP(0xa000,0);
    // Clear video memory ( set each byte to 0 )
    memset(screen, 0, FULL_SCREEN_SIZE);
    // Call function to load files into memory, exit on error
    if(!load_files()) exit(1);
    // Call assembly routines to set graphics mode and palette
    setgmode(0x13);          // Set mode to 13h
    setpalette(palette);     // Set VGA palette
    // Initialize starting variables
    int bye=0;                   // Exit program flag
    int x=0,y=0;                 // Initial world coord position
    int tlevel=(LEVEL_MAX-1);    // Top level to display
    int clevel=0;                // Current level position
    // Main rendering loop
    do
       {
       // Clear the offscreen buffer: 1st Step in RPA
       memset(screenbuf, 0, FULL_SCREEN_SIZE);
       // Render the axonometric tile display
       draw_tilesRPA(x, y, tlevel, clevel, screenbuf);
       // Copy offscreen buffer to the screen
       memmove(screen,screenbuf,FULL_SCREEN_SIZE);
       // Check to see if user hit a key, and process if so
       if ( kbhit() )
       {
       int xmap, ymap;
       char ch;
       ch=getch();
         switch (ch)
           {
           case 0:
         ch=getch();
         switch (ch)
           {
           // Left Arrow Key - shift map to the right
           case 'M':
             x+=SCROLL_STEP;
             xmap=int(x & MAP_COORD_MASK)/MAX_CELL_UNITS;
             if (xmap>(XMAP_MAX-1)) x-=SCROLL_STEP;
             break;
           // Right Arrow Key - shift map to the left
           case 'K':
             x-=SCROLL_STEP;
             xmap=int(x & MAP_COORD_MASK)/MAX_CELL_UNITS;
             if (xmap<0) x+=SCROLL_STEP;
             break;
           // Up Arrow Key - shift map downward
           case 'P':
             y+=SCROLL_STEP;
             ymap=int(y & MAP_COORD_MASK)/MAX_CELL_UNITS;
             if (ymap>(YMAP_MAX-1)) y-=SCROLL_STEP;
             break;
           // Down Arrow Key - shift map upward
           case 'H':
             y-=SCROLL_STEP;
             ymap=int(y & MAP_COORD_MASK)/MAX_CELL_UNITS;
             if (ymap<0) y+=SCROLL_STEP;
             break;
           }
         break;
           case  27:
         bye=1;
         break;
           }
       // Clear the keyboard buffer (eliminates backup)
       while(kbhit()) ch=getch();
       }
       }
    while(!bye);
    // Restore the old video mode and clear the screen
    setgmode(oldmode);
    clrscr();
    return;
    }
/*******************************************************************
 Function:  draw_tilesRPA();
 Purpose:   Renders the axonometric tile display using the
        "Reverse Painter's Algorithm"
 Arguments: x, y          - pair of world coordinates on which
                display should be centered
        top_level     - topmost level to display
        current_level - level on which display should be
                centered
        screenbuf     - pointer to offscreen buffer
 Comments:  The destination viewport (screenbuf) must be cleared
        (set to 0 values) before this function is called
*******************************************************************/
void draw_tilesRPA(int x, int y, int top_level, int current_level,
           unsigned char far *screenbuf)
    {
    // Add viewport-to-map offsets corresponding to the distances
    // from the center to the lower left corner of the viewport
    x+=X_VPTM_OFFSET;
    y+=Y_VPTM_OFFSET;
    // Adjust world position to compensate for levels which must
    // be drawn above the current_level position
    for ( int k=current_level; k<top_level; k++ )
      { x+=LEVEL_ADJUST; y+=LEVEL_ADJUST; }
    // Begin rendering process, starting with topmost level
    for ( int level=top_level; level>=0; level-- )
      {
      // Calculate map location (map coordinates)
      int mapx=int(x & MAP_COORD_MASK)/MAX_CELL_UNITS;
      int mapy=int(y & MAP_COORD_MASK)/MAX_CELL_UNITS;
      // Calculate location within cell (cell coordinates)
      int cellx=(x & CELL_COORD_MASK);
      int celly=(y & CELL_COORD_MASK);
      // Set up initial values for conversion from cell
      // coordinates to screen coordinates
      int xpos=CELL_START_X;
      int ypos=CELL_START_Y;
      // Calulate x and y screen coordinates using the
      // cell-to-screen conversion formulas
      xpos+=(2*cellx-2*celly);
      ypos+=cellx+celly;
      // Adjust screen coordinates to the viewport
      xpos=(VIEWPORT_X_START-xpos);
      ypos=(VIEWPORT_Y_END-ypos);
      // Make adustments to the map and screen coordinates
      // (go below the viewport) to ensure that the walls/objects
      // of offscreen base tiles may still be visible (drawn)
      mapy+=START_YM_OFFSET;
      mapx+=START_XM_OFFSET;
      xpos+=START_XS_OFFSET;
      ypos+=START_YS_OFFSET;
      // Set initial x increment value
      int xadd=START_XADD;
      // Hold on to starting values of map coordinates
      int mxhold=mapx, myhold=mapy;
      // Create temporary holding variables for the screen position
      int tempx=xpos, tempy=ypos;
      // Loop through and draw tiles: left->right, bottom->top
      do
    {
    mapx=mxhold; mapy=myhold;     // Set to starting map values
    tempx=xpos;                   // Get starting x value for run
    do
      {
      // Check if current map position is in bounds, if not, skip
      if ((mapx>=0)&&(mapx<XMAP_MAX)&&(mapy>=0)&&(mapy<YMAP_MAX))
        {
        // Calculate current map coordinate location
        int loc=(XMAP_MAX*mapy)+mapx;
        // Get tile index values from the tile maps
        int prop_index=prop_map[level][loc];
        int xwall_index=xwall_map[level][loc];
        int ywall_index=ywall_map[level][loc];
        int base_index=base_map[level][loc];
        // Blit cell block tiles to screen in "reverse" order
        // if not of index 0 ( indicates nothing there )
        // 1st=Prop, 2nd=Xwall, 3rd=Ywall, 4th=Base
        if(prop_index) prop_tiles.blit(prop_index-1,
               tempx, tempy+PROP_YOFFSET, screenbuf);
        if(xwall_index) xwall_tiles.blit(xwall_index,
                tempx+XWALL_XOFFSET, tempy+WALL_YOFFSET,
                screenbuf);
        if(ywall_index) ywall_tiles.blit(ywall_index,
                tempx+YWALL_XOFFSET, tempy+WALL_YOFFSET,
                screenbuf);
        if((base_index)||(level==0)) base_tiles.blit(base_index,
                     tempx, tempy, screenbuf);
        }
      tempx+=CELL_FULL_WIDTH;  // Incr x position for next x move
      mapx+=1; mapy-=1;        // Incr map values for next x move
      }
    while ( tempx <= (VIEWPORT_X_END+1) );
    tempy-=CELL_HALF_HEIGHT;   // Move y position up for next row
    xadd=-xadd;                // Flip xadd orientation
    xpos+=xadd;                // Add inc to x position
    if(xadd>0)                 // If xadd is positive,
        myhold-=1;             //   decrease starting y map pos
    else                       // If xadd is negative,
        mxhold-=1;             //   increase starting y map pos
    }
      while ( tempy >= (VIEWPORT_Y_START-CELL_FULL_HEIGHT) );
      // Adjust world coordinates in order to draw the next level
      x+=-LEVEL_ADJUST; y+=-LEVEL_ADJUST;
      }
    return;
    }
/*******************************************************************
 Function:  load_files();
 Purpose:   Reads files and loads data for engine into memory
 Arguments: none
 Comments:  returns 1 if successful
        returns 0 if error is encountered
*******************************************************************/
int load_files(void)
    {
    int j;
    // Load palette, and load in straight tile bitmaps
    FILE *in;
    if ((in = fopen("pics.wad", "rb")) == NULL)
    {
    fprintf(stderr, "Cannot find file: PICS.WAD \n");
    return(0);
    }
    fread(palette, sizeof(palette), 1, in);
    for (j=0; j<BASE_COUNT; j++)
    fread(base_tiles.image[j], base_tiles.size, 1, in);
    for (j=0; j<WALL_COUNT; j++)
    fread(xwall_tiles.image[j], xwall_tiles.size, 1, in);
    for (j=0; j<WALL_COUNT; j++)
    fread(ywall_tiles.image[j], ywall_tiles.size, 1, in);
    for (j=0; j<PROP_COUNT; j++)
    fread(prop_tiles.image[j], prop_tiles.size, 1, in);
    fclose(in);
    // Load in data for tile map arrays
    if ((in = fopen("map.wad", "rb")) == NULL)
    {
    fprintf(stderr, "Cannot find file: MAP.WAD \n");
    return(0);
    }
    fread(base_map, sizeof(base_map), 1, in);
    fread(xwall_map, sizeof(xwall_map), 1, in);
    fread(ywall_map, sizeof(ywall_map), 1, in);
    fread(prop_map, sizeof(prop_map), 1, in);
    fclose(in);
    return(1);
    }

Listing Three

/********************************************************************
 TILE.H
 Header file for the Tile class of the Axoview Engine v1.0
 Contains the Tile class declaration.
 Written by Nate Goudie  1996
********************************************************************/
class Tile
    {
    // Make everything public
    public:
    char far **image;       // Pointer to array of tile bitmaps
    int width,height;       // Width and height of tile bitmap
    int size;               // size of tile bitmap (in bytes)
    //Constructor for tile class:
    Tile(int num_tiles,int w,int h);
    // Function to draw tile into offscreen buffer
    void blit(int tile_num,int x,int y,
        unsigned char far *screen);
    };

Listing Four

/********************************************************************
 TILE.CPP
 The Tile class for the Axoview Engine v1.0
 - a class to handle the containment and drawing of tiles
 Contains the Tile class functions.
 Written by Nate Goudie  1996
********************************************************************/
#include    <stdio.h>
#include    <alloc.h>
#include    <mem.h>
#include    "tile.h"
// Declare constants for the viewport clipping
#define XMIN 0
#define XMAX 319
#define YMIN 0
#define YMAX 199
#define SCREENWIDTH 320
#define SCREENHEIGHT 200
/*******************************************************************
 Function:  Tile::Tile();
 Purpose:   The Tile class constructor
 Arguments: num_tiles     - number of tiles (bitmaps)
        w             - width of the tile in pixels (bytes)
        h             - height of the tile in pixels (bytes)
 Comments:  Will compute size of each tile in bytes, and will
        allocate memory for the tile bitmaps
*******************************************************************/
Tile::Tile(int num_tiles,int w,int h)
    {
    width=w;
    height=h;
    // Calculate size of tile bitmap
    size=w*h;
    // Allocate memory for tile bitmaps
    image=new char far *[num_tiles];
    for (int j=0; j<num_tiles; j++)
    image[j]=new char[size];
    }
/*******************************************************************
 Function:  Tile::blit();
 Purpose:   Tile class function which blits the indicated tile
         bitmap to an offscreen buffer using the
         "Reverse Painter's Algorithm".
         Handles clipping and tile transparency.
 Arguments: tile_num      - index of tile bitmap to display
         x, y          - offscreen coordinates where the tile
                 should be drawn
         screen        - pointer to offscreen buffer
 Comments:  The destination buffer (screen) must be 320x200.
*******************************************************************/
void Tile::blit(int tile_num, int x, int y, unsigned char far *screen)
    {
    int txstart=0, txend=width, tystart=0, tyend=height;
    // Perform clipping before main loop
    // Clip tile to viewport and set boundaries to blit through
    if (x<XMIN)
    txstart=XMIN-x;
    if (y<YMIN)
    tystart=YMIN-y;
    if ((x+width-1)>XMAX)
    txend=XMAX-x+1;
    if ((y+height-1)>YMAX)
    tyend=YMAX-y+1;
    // Calculate tile and buffer starting offsets
    int toffset = (tystart*width)+txstart;
    int poffset = (y+tystart)*SCREENWIDTH+(x+txstart);
    // Calculate next row increments
    int toffinc = ((width-txend)+txstart);
    int poffinc = (SCREENWIDTH-(txend)+txstart);
    // Dereference one of the pointers to the tile bitmap for speed
    char far *tileimage = image[tile_num];
    // Now loop through and copy the tile bitmap to the screen buffer
    for(int row=0; row<(tyend-tystart); row++)
    {
    for(int column=0; column<(txend-txstart); column++)
        {
        // Get pixel from the offscreen buffer
        int dest_pixel=screen[poffset];
        // Check if it is transparent (0), if so, copy
        // the tile bitmap's pixel over it
        if (!dest_pixel)
        screen[poffset] = tileimage[toffset];
        poffset++; toffset++;
        }
    // Jump to start of next row
    toffset+=toffinc;
    poffset+=poffinc;
    }
    return;
    }
End Listings


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video