A C++ Ray-Casting Engine

Raycast, a ray-casting engine written entirely in C++, is the project that Al offers up this month.


November 01, 1995
URL:http://www.drdobbs.com/cpp/a-c-ray-casting-engine/184409670

NOV95: C PROGRAMMING

Last spring's Dr. Dobb's Sourcebook of Games Programming (May/June 1995) included the article "Ray Casting in C++," by Mark Seminatore. The article was timely for me, because at the time I was casting about (har, har) for a ray-casting algorithm to use in a C++ book about Windows games programming.

Ray casting is the technique that many 3-D game programs use to render and animate scenes that occur within a maze of perpendicular walls of a uniform height. The algorithm uses texture mapping to render walls, doors, props, and sprites. The ray-casting algorithm Mark implemented supports scenes similar to those in Wolfenstein, an early 3-D maze game. More-contemporary games use more-complex and better-optimized algorithms to support texture-mapped floors and ceilings, stairs, sloping and angular hallways, and player movement in three axes (pitch, yaw, and roll). My requirements are satisfied by the earlier technology, which is fortunate, since those techniques have been published and are widely understood.

Mark's article addressed the concepts behind ray casting in some detail. He also explained how to use assembly language to optimize the algorithm. Two books, Tricks of the Game Programming Gurus, by LaMothe, Ratcliff, Seminatore, and Tyler (Sams Publishing, 1994) and Gardens of Imagination, by Christopher Lampton (Waite Group Press, 1994) also provide explanations of ray casting and source code that implements the algorithm. Mark's ray caster and the one in the Gurus book are similar, although he did not write the chapter on ray casting.

I downloaded Mark's program and dissected it to see how adaptable it was to my needs. The algorithm is well implemented, but I wanted to change or add several things. Mark built his ray caster as a C library with functions that you can call from a C or C++ program. I wanted mine to be pure C++ because I wanted to make many modifications and I am more comfortable with C++. I also made changes to the following items:

I used Wolfenstein as a model for a 3-D maze engine to allow programmers to design game programs with all of its features. It's not there yet, but it's close, close enough to be the subject of a "C Programming" column project.

I used Mark's code only to determine how the algorithms work. My code is a complete rewrite in C++ rather than a wrapper around a C library. This decision was not meant to diminish his achievement, which is significant, but rather to produce an enhanced library, one with the features that I need in my simulation.

My first objective was to match the performance of the C version of the ray caster by implementing only the features that Mark supports. In addition to the C ray caster, he published an optimized version with the time-critical functions rewritten in assembly language. It's really fast, but his C version is plenty fast enough on my 4DX2-66V. That configuration, which was leading-edge technology not long ago, has sunk to the low end of mainstream hardware in these days of behemoth operating systems and compilers. I figure that with the old 486 as a target, I'll be okay. The project will eventually be ported to a Windows 95 program, and anyone with anything much slower than a 486/66 won't like Windows 95, anyway. For that reason, I decided to hand code only those optimizations that I can do in C++.

The first version of "Raycast," my unimaginative name for the project, matched the frames-per-second performance reported by Mark's C program when it terminates. By tweaking some loops, I was able to outrun the earlier program, but as I add features, the engine is becoming progressively slower. My objective is to achieve a rate of frames per second fast enough that a game has sufficient residual processing cycles between frames to do game-specific chores, such as keeping score, moving sprites around, playing sound effects, and so on.

The Raycast engine published here supports DOS programs. The final version, to be published in the book, will run under Windows, probably with the fast video drivers that come with the new Microsoft Game SDK. I'll discuss the engine's interface from a programmer-user's point of view. The code published with this column shows how to use the engine. The code for the engine itself can be downloaded or sent for.

The Maze

Listing One is maze.dat. Each character position represents a cell in the maze. The "1" characters are wall tiles. In this example I use only one wall tile, although there could be many. If you wanted tapestries, walls with graffiti, walls with pictures, and walls with textures other than the dull, prison-like concrete block walls in my example, you would build appropriate PCX files and add them to the game. The "D," "d," "E," and "e" characters in the maze represent doors. The "D" and "E" doors are those that you face in the y direction; the "d" and "e" doors are faced in the x direction. There is one "G" character in the maze, which represents a flower pot. The logic for displaying this flower pot and other props and characters with transparent qualities is not yet complete. I'll update the library when I finish that part.

The maze consists of 64 lines of text with 64 characters each. The blank lines in the maze have 64 space characters.

Modifying System Parameters

Listing Two is consts.h. This file contains constant values and some inline functions that the program uses. You can modify some of these values to change how the program operates. I've already discussed the LONGCORRIDORS global constant. The viewangle variable defines the width of the scene that you can see. It can be any value between 30 and 90, but 60 seems to provide a realistic rendering.

There are four values for specifying the default viewport, which I have set to a full-screen view. The values identify the screen coordinates for the upper-left corner of the viewport and its height and width. The program uses the videomode, screenheight, and screenwidth constants. To use a different mode and resolution, you must modify the VGA class to operate with those parameters.

Four constants specify the closest you can get to a wall when moving around and the furthest you can be from a door and still open it. There are constants that identify bitmap numbers associated with texture tiles. Those constants figure prominently in some inline functions later in the file.

The mazewidth and mazeheight constants specify the dimensions of the maze in tiles. The tilewidth and tileheight constants specify the dimensions of each texture-mapping tile in pixels. All those values are 64 in the existing implementation.

There are three constants each for the number of increments of movement for each step and the number of degrees of rotation for each turn when the game player moves around in the maze. There are Faster and Slower functions in the RayCaster class that use these values.

The algorithm uses the inline functions isWall, isDoor, isProp, isAutomaticDoor, and isTransparent to determine the properties of a tile. If you add texture-mapping tiles to the game beyond those in the example, you must modify not only the bitmap numbers mentioned earlier, but also the inline functions. A prop is a stationary tile with transparent parts. The image for a prop is cast as a flat surface positioned in the center of the tile space. A prop always faces the player. An automatic door is one that opens when you move close to it. Other doors must be opened and closed by a call to the RayCaster::OpenCloseDoor function. The isTransparent function returns True if the tile has transparent properties. Doors, props, and (later) sprites have transparent properties. The Maze class is sensitive to the bitmap numbers that you assign, so if you add texture tiles, you must look into that code as well. It should be obvious what code needs to be modified.

The Keyboard

The RayCaster class encapsulates only the video parts of a game program. To demonstrate the engine, you need to use the keyboard. Keyboard operations in a game program usually differ from those in other programs. The player can hold down several keys at once and the program must acknowledge them appropriately. To support that kind of keyboard operation, I built a Keyboard class. Listing Three is keyboard.h, which declares the class. The listing begins with a number of constants to define key values that are not in the ASCII character set. For example, there is no ASCII value for the Alt key. After declaring an object of type Keyboard, the program can call two functions to interrogate the keyboard.

The wasPressed function tests to see if the key specified in the argument is now being pressed. If so, the function waits until the key is no longer being pressed and then returns True; otherwise it returns False.

The isKeyDown function returns True immediately if the key is being held down at the time of the call. The function does not wait for the key to be released before returning.

The Ray Caster

Listing Four is raycast.h, the header file that declares the RayCaster class. It's a complicated class, and I'll describe its public interface. You construct a RayCaster object by passing the name of the maze file, a pointer to an array of char pointers that specify the PCX files of the texture-mapping tiles, the initial x- and y-coordinates, and viewing angle of the player. You can also specify a ViewPort object as the last argument to start out with other than the default full-screen ray caster. Many games use part of the screen to display other things.

The DrawFrame function displays one frame on the screen based on those values. The SetPosition function changes those values, and GetPosition retrieves the current settings. Four functions move the player one step through the maze in the four cardinal directions relative to the player's current view angle. Two functions rotate the player a specified number of degrees to the right or left. If the degrees argument is 0, the functions use the values from consts.h. The ToggleMap function displays and hides the maze map. The OpenCloseDoor function opens or closes a door if the player is close enough to one. The isInDoorway function returns True if the player is in the specified doorway. The Slower and Faster functions change the player's speed of motion. If you are debugging, the TraceFrame function enables trace and tracend macros to display values on the standard output device during the next execution of DisplayFrame.

The Game Program

Listing Five is main.cpp, which, together with maze.dat and a set of PCX files, constitutes an example game. An array of character pointers in main.cpp identifies the PCX files. The order of this array must correspond with the constants defined in consts.h. Therefore, WALL01.PCX represents bitmap tile number 1, and so on. The array of ViewPort objects represents a set of viewport sizes that this game allows the user to step through.

The program starts by declaring a RayCaster object from the free store. Then it loops until the player presses the Esc key. Each time through the loop, the program calls the DisplayFrame function. Then it tests to see if the player has pressed any important keys. The up- and down-arrow keys move the player forward and backward. The right- and left-arrow keys rotate the player one increment unless the Alt key is also being pressed, in which case the player moves sideways. The spacebar opens and closes doors, the Ins key toggles the map display, the F and S keys make the player move faster and slower, and the plus and minus keys increase and decrease the size of the viewport. Changing the viewport size involves a lot of recalculating, so the program simply deletes the current RayCaster object and constructs a new one with the new ViewPort object.

The try/catch mechanism is compiled only when you are debugging. Otherwise, exception handling is disabled by the makefile to avoid the overhead that exceptions impose on each function with automatic class objects.

The game program calls VGA::ResetVideo when the game is over to depart from graphics mode. The RayCaster destructor does not do this automatically: This prevents changes in viewport size from invoking video mode resets and the attendant flicker.

The PCX Files

The PCX files are built with a paint program. They have a resolution of 64x64 pixels and 256 colors. They should all use compatible palettes. Palette color 0 is reserved for transparent parts of the image.

Another Reason to Avoid gotos

One of the unavoidable aspects of teaching C involves the dreaded goto statement. For many years now, programmers have been conditioned to shrink in horror from any code that uses goto. Language gurus preach loudly that goto is unnecessary, unstructured, and unwise. Structured programming zealots assert with authority that any algorithm known to mankind and other species can be expressed using only the three constructs of structured programming: sequence, selection, and iteration. I and many others have fallen sway to that ideology, yet, most modern programming languages, including Pascal, which was designed to teach structured programming, support a goto statement, implying that there must be a reason for it. Therefore, when we teach those languages, we first teach the behavior of the goto statement, then we teach caution in its use.

Many books teach that goto in C can be used to jump out of nested loops, where a simple break statement would break out of only the innermost loop. To preserve structure, you can cause the outer loops to respond to the breaking condition, but that usually requires extra Boolean variables or tests that a simple goto can avoid. Example 1 illustrates this point. If there are situations where you want to break out of the j loop without breaking out of the i loop, Example 1 does not work. You need to contrive a Boolean variable and test it, as in Example 2.

There is nothing wrong with either of these programming idioms. They preserve structure, avoid gotos, and work. However, when optimization and performance are your goals, the propriety of structure can help to defeat your purpose. If the loops exist in a time-critical operation, such as the sweeps in a ray caster, such code can seriously degrade performance in the name of structure. The initialization and testing of that bool variable in Example 2 costs processor cycles. A peek at the assembly-language code generated by the compiler illustrates this point. Example 3 shows that using a goto produces tighter code, which might make the difference between a successful time-critical program and a turkey.

Last week, I taught an introductory C course to a class of 15 aerospace engineers. The course is the first in a series of two one-week sessions that teach C and C++ to people who already understand programming. Presumably, they will take the intervening three weeks to practice C and become familiar with its syntax so that they can tackle the C++ extensions comfortably.

When we got to the goto exercises, I discussed what I just presented in the three examples and then reinforced the structured-programming position by stating that in over ten years of publishing code, I had used the goto statement only in exercises that illustrate its behavior.

Then I added that my experiences with the ray caster might necessarily change that track record. I explained that I might finally have found a valid reason for using the goto statement in a C or C++ program, and pointed to one such statement on the projected screen. I said, however, that if the structured-programming gods could hear me say that, they would no doubt strike me down with a lightning bolt. At that very moment, the bulb burned out in the overhead projector.

You have been warned.

Source Code

The source-code files for the Raycast project are free. You can download them from the DDJ Forum on CompuServe, on the Internet by anonymous ftp, and from DDJ Online; see "Availability," page 3.

If you cannot get to one of the online sources, send a 3.5-inch diskette and an addressed, stamped mailer to me at Dr. Dobb's Journal, 411 Borel Avenue, San Mateo, CA 94402, and I'll send you the source code. Make sure to include a note specifying which project you want. The code is free, but if you care to support my Careware charity, include a dollar for the Brevard County Food Bank.

Example 1: Causing outer loops to respond to the breaking condition.

for (i = 0; i < MAXI; i++)   {
  for (j = 0; j < MAXJ; j++   {
    if ( breaking_condition )
// whatever it is
      break;
    // ...
  }
  if (j < MAXJ)  
// contrived condition
    break;
  // ...
}
Example 2: Testing a Boolean variable.
for (i = 0; i < MAXI; i++)   {
  bool breaking = false; 
// contrived variable
  for (j = 0; j < MAXJ; j++   {
    if ( breaking_condition )   {
      breaking = true;
      break;
    }
    // ...
  }
  if (breaking == true)
    break;
  // ...
}
Example 3: A goto can produce tight code for time-critical programs
  for (i = 0; i < MAXI; i++)   {
    for (j = 0; j < MAXJ; j++   {
      if ( breaking_condition }
         goto breakout;
      // ...
    }
    // ...
  }
breakout:
  // ...

Listing One

                                                                
   11111111111111111111111111E1111111111111111111111111111111   
   1                                                        e   
   111D111111111111111111111   111111111111111111111111111111   
   1     1                                            1     1   
   1     1                                            1     1   
   1     d   11111111111111     1111111111111111111   d     1   
   1     1               11                      11   1     1   
   1     1   11111  1111111     111111111111111  11   1     1   
   1111111   11     111111111D111                11   1111111   
   1     1   111111     111     1  1111111111111111   1     1   
   1     1   111111111 1111     1                11   1     1   
   e     d   11111111  111111   1111111111111 11111   d     1   
   1     1   1111                       1  1   1111   1     1   
   1     1   1111                       1  1   1111   1     1   
   1111111   1111        111D111        1  d   1111   1111111   
   1     1   1111        1     1        1  1    1     1     1   
   1     1   1111        1     1        1  1    1     1     1   
   1     d   1111        1     1        1  1    1     d     1   
   1     1   1111        111G111        d  1    1     1     1   
   1     1   1111           1           1  1    d     1     1   
   1111111   1111           1           1  1    1     1111111   
   1     1   1111111111111111111D111111111111 11111   1     1   
   1     1   1111  1                           1111   1     1   
   1     d   1111                              1111   d     e   
   1     1   1111111111111111111D111111111111111111   1     1   
   1     1   1111                1  1     1    1111   1     1   
   1111111   1111 1111111111111111  1  1 1   1 1111   1111111   
   1     1   1111      1            1  1 11  1 1111   1     1   
   1     1   111111111 1 111111111111  1 1  11 1111   1     1   
   1     d   1111      1               1   111 1111   d     1   
   1     1   111111 111111 11D111111111111 11111111   1     1   
   1     1   111111        11 1                   1   1     1   
   1111111   1111111111111111 11111111D111111111111   111D111   
   1111111                                                111   
   1111111                                            1111111   
   1111111111111111111111111111111E11111111111111111111111111   
                                                                
 11111111111                                                    
 1         1                                                    
 1         d                                                    
 1         1                                                    
 1         1                                                    
 11111111111                                                    
                                                                

Listing Two

// ------------ consts.h
#ifndef CONSTS_H
#define CONSTS_H
// data types
typedef short INT;              // 16 bits
typedef unsigned short UINT;    // 16 bits
typedef long LONG;              // 32 bits
typedef unsigned long DWORD;    // 32 bits
typedef unsigned short int bool;
const bool false = 0;
const bool true  = 1;
// these are values to change to modify the raycasting algorithm
#define LONGCORRIDORS
const INT viewangle      =    60;   // viewing angle
const INT defx           =     0;   // default x
const INT defy           =     0;   // default y
const INT defviewwidth   =   320;   // default viewing width
const INT defviewheight  =   200;   // default viewing height
const INT videomode   = 0x13;   // 320 x 200 x 256
const INT screenwidth =  320;   // horizontal screen resolution
const INT screenheight = 200;   // vertical screen resolution
const INT hitwallforward   =  64;    // closest you can get to a wall
const INT hitwallsideways  =   8;    // closest you can get to a wall
const INT hitwallbackwards =  64;    // closest you can get to a wall
const INT doordistance     = 128;    // furthest from door to open door
const INT maxoverlays =   20;   // maximum transparent tile overlays
const INT mindistance =   10;   // minimum distance cast to wall
const INT maxdistance = 2048;   // maximum distance cast to wall
const INT maxheight   = 1024;
const INT bitmapcount  =  30;   // maximum number of tile bitmaps
const INT doorjam1     =   3;   // bitmap # for doorjam
const INT doorjam2     =   5;   // bitmap # for doorjam
const INT door1        =   7;   // bitmap # for door1
const INT door2        =  16;   // bitmap # for door2
const INT flowerpot    =  25;   // bitmap # for flowerpot
const INT openinterval =   8;   // pixels per open interval
const INT autoclose    =  25;   // frames until auto door close
const INT transparencies = door1;   // this and higher can be transparent
const INT mazewidth      =   64;    // maze width
const INT mazeheight     =   64;    // maze height
const INT tilewidth      =   64;    // width of a bitmap tile
const INT tileheight     =   64;    // height of a bitmap tile
const INT fastspeed      =   10;    // increments per step
const INT mediumspeed    =   12;
const INT slowspeed      =   15;
const INT fastrotation   =    6;    // degrees of rotation per turn const INT mediumrotation =    4;
const INT slowrotation   =    1;
const INT ceilingcolor   =  90;
const INT floorcolor     = 139;
// self-adjusting values
const INT   mazexmax  = mazewidth * tilewidth;
const INT   mazeymax  = mazeheight * tilewidth;
const LONG  mazexmaxl = ((LONG)mazexmax << 16); 
const LONG  mazeymaxl = ((LONG)mazeymax << 16);
inline bool isWall(INT bmpno)
{
    // ---- modify this function if wall tiles are added
    return bmpno > 0 || bmpno < doorjam1;
}
inline bool isDoor(INT bmpno)
{
    // ---- modify this function if door tiles are added
    return bmpno >= door1 && bmpno <= door2 + 8;
}
inline bool isProp(INT bmpno)
{
    // ---- modify this function if prop tiles are added
    return bmpno >= flowerpot;
}
inline bool isAutomaticDoor(INT doorno)
{
   // ---- modify this function if door tiles are added
    return doorno >= door2 && doorno <= door2 + 8;
}
inline bool isTransparent(INT tileno)
{
    return tileno >= transparencies;
}
#ifdef NDEBUG
#define Assert(p)   ((void)0)
#else
void MyAssert(char* cond, char* file, int line);
#define Assert(p)   ((p) ? (void)0 : MyAssert(#p, __FILE__, __LINE__))
#endif
#endif

Listing Three

// -------- keyboard.h
#ifndef KEYBOARD_H
#define KEYBOARD_H
#include <dos.h>
#include "consts.h" 
const int homekey  = 71+128;
const int pgupkey  = 73+128;
const int endkey   = 79+128;
const int pgdnkey  = 81+128;
const int f1key    = 59+128;
const int f2key    = 60+128;
const int f3key    = 61+128;
const int f4key    = 62+128;
const int f5key    = 63+128;
const int f6key    = 64+128;
const int f7key    = 65+128;
const int f8key    = 66+128;
const int f9key    = 67+128;
const int f10key   = 68+128;
const int f11key   = 87+128;
const int f12key   = 88+128;
const int uparrow  = 72+128;
const int dnarrow  = 80+128;
const int rtarrow  = 77+128;
const int lfarrow  = 75+128;
const int inskey   = 82+128;
const int delkey   = 83+128;
const int altkey   = 56+128;
const int pluskey  = 78+128;
const int minuskey = 74+128;
const int esckey   = 27;
class Keyboard  {
    static void interrupt (*oldkbint)(...);
    static void interrupt newkbint(...);
    static bool kys[128];
    static int scancodes[256];
    static unsigned char scancode;
public:
    Keyboard();
    ~Keyboard();
    bool wasPressed(int ky);
    bool isKeyDown(int ky);
};
inline bool Keyboard::isKeyDown(int ky)
{
    return kys[scancodes[ky]];
}
#endif

Listing Four

// ------------- raycast.h
#ifndef RAYCAST_H
#define RAYCAST_H
#include "trigtabl.h" #include "tables.h"
#include "maze.h"
#include "pcx.h"
#include "vga.h"
#include "map.h"
#include "consts.h"
// ----- one vertical ray-cast slice
struct Slice {
    UINT bmptile;   // tile number
    UINT distance;  // distance from player
    UINT column;    // tile column to render
    INT mazeindex;  // index into maze of tile position
};
// ------ ray caster viewport
struct ViewPort {
    INT x, y;           // upper left origin
    INT viewwidth, viewheight;  // viewport dimensions
};
// ------ default viewport values (defined in consts.h)
const ViewPort fullscreenviewport = {
    defx,
    defy,
    defviewwidth,
    defviewheight
};
// ---- ray caster class
class RayCaster {
    VGA vga;                    // video object
    TrigTables trigtables;      // trig tables
    HeightTable heighttable;    // height table
    ScaleTable scaletable;      // vertical scale table
    Maze maze;                  // the game's maze
    Map map;                    // displayable map
    Slice slice;                // one ray-cast slice
    Slice* slices;              // -> an array of slices
    INT  overlaycount;          // nbr of transparent tiles hit
    PCXBitmap bmps[bitmapcount];
    INT  *ocounts;              
    char *screenbuffer;         // -> the screen buffer
    INT  x, y, angle;           // player's position
    INT viewwidth, viewheight;  // viewport dimensions
    LONG sinangle, cosangle;    // 
    INT  rayangle;              // angle of the ray
    INT  itilex, itiley;        // coordinates of tile
    LONG ltilex, ltiley;        // coordinates of tile
    DWORD raylength;            // length of ray
    bool mapon;                 // true if map is displaying
    INT xmazeindex;             // index into x maze
    INT ymazeindex;             // index into y maze

    INT hitwallmargin;          // margin for hit detection
    INT speed, rotation;        // movement speed controls
    // --- private functions
    INT  EastWest(INT angle);   // true if facing due east or west
    INT  NorthSouth(INT angle); // true if facing due north or south     bool CastSlice(INT i);      // cast one slice
    UINT CastXRay();            // cast an X ray
    UINT CastYRay();            // cast a Y ray
    enum direc { forward, backward, rightward, leftward };
    void Move(direc dir);       // move the player 1 step
    void CvtRayLength(DWORD& raylength);
    void SetAngles();
    bool SameCell(INT mazeindex1, INT mazeindex2);
    bool SameCoord(INT c1, INT c2);
    INT Degree(INT x);          // cvt degrees to viewport angle
    INT unDegree(INT d);        // cvt viewport angle to degrees
public:
    RayCaster(char* mazename, char* pcxs[],
        INT px,
        INT py,
        INT pangle,
        ViewPort vp = fullscreenviewport);
    ~RayCaster();
    // ---- draw one frame of the maze
    void DrawFrame();
    // ---- set the player's position and angle facing
    void SetPosition(INT xp, INT yp, INT angl);
    // ---- get the player's current position and angle facing
    void GetPosition(INT& xp, INT& yp, INT& angl);
    // ---- functions to move the player
    void MoveForward();
    void MoveBackward();
    void MoveLeftward();
    void MoveRightward();
    // --- functions to rotate the player
    void RotateRight(INT degrees = 0);
    void RotateLeft(INT degrees = 0);
    // ---- turn the map on and off
    void ToggleMap();
    // ----- open or close the door immediately in front of player
    void OpenCloseDoor();
    // ----- test if player is in the doorway of specified door
    bool isInDoorway(INT doorid);
    // ----- command player to change speed
    void Slower();
    void Faster();
#ifndef NDEBUG
    bool tracing;
    void TraceFrame();
#endif
};
inline INT RayCaster::Degree(INT x)
{
    return trigtables.Degree(x);
}
inline INT RayCaster::unDegree(INT d)
{
    return (INT)((LONG)d * viewangle / viewwidth);
}
inline INT RayCaster::EastWest(INT angle) {
    return (angle == Degree(90) ||
        angle == Degree(270));
}
inline INT RayCaster::NorthSouth(INT angle)
{
    return (angle == Degree(0) ||
        angle == Degree(180));
}
inline void RayCaster::MoveForward()
{
    Move(forward);
}
inline void RayCaster::MoveBackward()
{
    Move(backward);
}
inline void RayCaster::MoveLeftward()
{
    Move(leftward);
}
inline void RayCaster::MoveRightward()
{
    Move(rightward);
}
inline void RayCaster::RotateRight(INT degrees)
{
    if (degrees == 0)
        degrees = rotation;
    angle += Degree(degrees);
    if (angle >= Degree(360))
        angle -= Degree(360);
}
inline void RayCaster::RotateLeft(INT degrees)
{
    if (degrees == 0)
        degrees = rotation;
    angle -= Degree(degrees);
    if (angle < 0)
        angle += Degree(360);
}
inline void RayCaster::ToggleMap()
{
    mapon ^= true;
}
inline void RayCaster::SetAngles()
{
    sinangle = trigtables.SinAngle(angle);
    cosangle = trigtables.CosAngle(angle);
}
// ------- debugging slice tracing functions
#ifndef NDEBUG
    #define trace(x) if(tracing)cout<<(#x ": ")<<(x)<<' '
    #define tracend() if(tracing)cout<<endl
inline void RayCaster::TraceFrame() {
    tracing = true;
}
#else
    #define trace(x) ((void)0)
    #define tracend() ((void)0)
#endif
#endif

Listing Five

// ---------- main.cpp
#ifndef NDEBUG
#include <time.h>
#include <iostream.h>
#endif
#include "raycast.h"
#include "keyboard.h"
// ---- pcx files
// ---- the order of these entries must match consts in consts.h
static char *pcxs[] = {
    "wall01.pcx",
    "wall02.pcx",
    "doorjam1.pcx",
    "doorjam1.pcx",
    "doorjam2.pcx",
    "doorjam2.pcx",
    "door1.pcx",
    "door2.pcx",
    "door3.pcx",
    "door4.pcx",
    "door5.pcx",
    "door6.pcx",
    "door7.pcx",
    "door8.pcx",
    "door9.pcx",
    "door10.pcx",
    "door11.pcx",
    "door12.pcx",
    "door13.pcx",
    "door14.pcx",
    "door15.pcx",
    "door16.pcx",
    "door17.pcx",
    "door18.pcx",
    "flpot1.pcx",
    "flpot2.pcx",
};
// ---- view ports: changed by pressing + and -
static ViewPort vps[] = { //      x   y   ht   wd   (position and size)
//    ---  --  ---  ---   
    { 120, 75,  80,  50 },
    { 110, 69, 100,  62 },
    { 100, 62, 120,  74 },
    {  90, 57, 140,  86 },
    {  80, 50, 160, 100 },
    {  70, 52, 180, 112 },
    {  60, 45, 200, 124 },
    {  50, 37, 220, 136 },
    {  40, 30, 240, 150 },
    {  30, 23, 260, 162 },
    {  20, 15, 280, 174 },
    {   0,  0, 320, 200 },
};
const int nbrvps = sizeof vps / sizeof(ViewPort);
int vpctr = nbrvps - 1;  // viewport subscript
int main()
{
    RayCaster* rp = 0;
    INT x = 360, y = 950, angle = 0;
#ifndef NDEBUG
    char* errcatch = 0;
    // ---- for computing frames/per/second
    long framect = 0;
    clock_t start = clock();
    try {
#endif
       // ----- ray caster object
        rp = new RayCaster("maze.dat", pcxs, x, y, angle, vps[vpctr]);
        // ---- keyboard object
        Keyboard kb;
        while (!kb.wasPressed(esckey))  {
            // ----- draw a frame
            rp->DrawFrame();
#ifndef NDEBUG
            framect++;
#endif
            // ----- test for player movement commands
            if (kb.isKeyDown(uparrow))
                rp->MoveForward();
            if (kb.isKeyDown(dnarrow))
                rp->MoveBackward();
            if (kb.isKeyDown(rtarrow))  {
                if (kb.isKeyDown(altkey))
                    rp->MoveRightward();
                else
                    rp->RotateRight();
            }
            if (kb.isKeyDown(lfarrow))  {
                if (kb.isKeyDown(altkey))
                    rp->MoveLeftward();
                else
                    rp->RotateLeft();             }
            // -------- open and close door commands
            if (kb.wasPressed(' '))
                rp->OpenCloseDoor();
            // ----- command to turn the map on and off
            if (kb.wasPressed(inskey))
                rp->ToggleMap();
            // ----- commands to change player movement speed
            if (kb.wasPressed('f'))
                rp->Faster();
            if (kb.wasPressed('s'))
                rp->Slower();
            // ----- commands to change the size of the viewport
            if (kb.wasPressed(pluskey)) {
                if (vpctr < nbrvps-1)   {
                    rp->GetPosition(x, y, angle);
                    delete rp;
                    rp = new RayCaster("maze.dat",
                            pcxs, x, y, angle, vps[++vpctr]);
                }
            }
            if (kb.wasPressed(minuskey))    {
                if (vpctr > 0)  {
                    rp->GetPosition(x, y, angle);
                    delete rp;
                    rp = new RayCaster("maze.dat",
                            pcxs, x, y, angle, vps[--vpctr]);
                }
           }
#ifndef NDEBUG
            if (kb.wasPressed(delkey))
                // ------- turn on tracing for one frame
                rp->TraceFrame();
#endif
        }
#ifndef NDEBUG
    }
    catch (char* errmsg)    {
        errcatch = errmsg;
    }
    // ---- get current position to report for testing
    if (rp)
        rp->GetPosition(x, y, angle);
    clock_t stop = clock();
#endif
    delete rp;
    VGA::ResetVideo();
#ifndef NDEBUG
    cout << "Frames/sec: "
         <<  (int) ((CLK_TCK*framect) / (stop-start)) << endl;
    cout << "Position (x, y, angle) :"
         << x << ' ' << y << ' ' << angle << endl;
    if (errcatch)
        cerr << errcatch << endl;
#endif     return 0;
}


Copyright © 1995, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.