3D Maze Immersion

Introduction

The program described here uses OpenGL to render a three-dimensional maze, through which the user must maneuver (walk or run) using the mouse and/or arrow keys. A first-person perspective is used, with collision detection implemented at the maze walls. Each maze is randomized, and moreover each maze has only a single best (i.e., detour-free) solution path. The size and difficulty of the maze are controlled by compile-time constants. A relatively easy maze is provided by default, but very large and circuitous mazes have been simulated during testing, with good results.

The demonstration code is less than 650 lines long, even with liberal use of comments. The code provided is therefore architecturally minimalist in nature; like much of OpenGL itself, the demo is largely an unadorned C program. This makes for a clear presentation, which is still adaptable to more involved designs.

The simulation developed here is somewhat similar in appearance to Wolfenstein 3D, one of the earliest widely used 3D simulations. However, the use of OpenGL enables some effects not seen in Wolfenstein 3D.

At the start of each game session, for example, the user drops into the game world from slightly above, as if by parachute, near the entrance to the maze. This affords the user the opportunity to view the maze solution briefly before play, and adds another skill dimension to the game. An example of this airborne-style view is shown below:

3D Maze Immersion - Airborne

Background

To put together such a simulation requires a complex, multidisciplinary development process. Questions of design, physics, etc., must be addressed, along with the computer science issues inherent to 3D programming, and to a randomized maze game. So, much of the design work evident here, and much of the code provided here, is discussed in two previous articles by the same author. Readers who are particularly interested in certain aspects of the implementation may derive greater benefit from one of these predecessor articles.

The article titled Fast Generation of Single-Solution Mazes gives an algorithm for the generation of abstract square-based mazes of arbitrary size, and having just one solution. In the commentary beneath this article, another user actually suggests a 3D implementation using OpenGL. A runtime image from that article is shown below. Note that the maze is an abstract construct intended for expansion into a full simulation; there is no gameplay per se in this earlier article, only a well-documented generator of mazes that could ultimately be rectangular, cubic, or even prismatic in some non-cubic way, in a full application.

2D Maze

This implementation, and the next, are both basically MS-DOS (more recently "Command Prompt") applications. However, a very different 3D implementation of the algorithm used here is evident in the next picture. As in this OpenGL implementation, one dimension of the abstract maze is mapped to the "Z" dimension of the 3D system, and the "Y" dimension of the 3D system is used to add vertical depth to the maze. The code for this second, 3D MS-DOS implementation is not currently available on The Code Project, but is available from the author upon request.

Early 3D Maze

In both of the maze programs shown above, note that the maze is composed of regular rectangular units, and that the randomization algorithm operates in terms of these discrete units. Logically, the maze definitions used are thus built around two-dimensional arrays. This technique is carried forward into the OpenGL implementation described here, where (as in the MS-DOS 3D implementation) array elements represent whole cubes.

A more recent article, called OpenGL Programming with MinGW, describes the 3D development setup which is reused by this latest article, along with many other low-level architectural details. In particular, OpenGL Programming with MinGW goes over the coding and design work necessary to render a textured cube using OpenGL. In building the random mazes used here, this cube-building code is simply repeated as necessary to build closed-off sections of the maze, with just a few additions and optimizations. A runtime image from this second predecessor article is shown below:

OpenGL Cube

The article at hand thus focuses on the additions necessary to combine these prior works into a basic first-person maze game. Collision detection and collision handling (e.g., with the maze walls) are new topics which are discussed in depth in the text below. Another key topic is the mapping of the abstract mazes described in Fast Generation of Single-Solution Mazes into a three-dimensional OpenGL coordinate space. Finally, real game-style user input is a topic that is new in this third article, and this issue is dealt with in some depth in the text below.

Using the Demo

The demo archive provided at the top of this article contains a single folder in Zip format. This Zip should be extracted to a single Windows folder, and the single ".exe" file contained therein should be executed, in order to run the demo application. The demo application will query the screen resolution, and then size itself to use about 90% of the display.

During the user's parachute-style descent, no input of any sort is processed. After that, either the mouse or the arrow keys, or even both of these input devices in tandem, can be used to play the game.

The user is allowed to move throughout an open-ended game world in a fashion which, it is hoped, will prove to be intuitive. In keyboard mode, the left and right arrow keys rotate the user about the "Y" (or "yaw") axis, i.e. about the player character's own height. This is done without actually changing position. The up arrow key moves forward in the current direction and the down arrow key moves backward, at a slower rate. It should be noted that running into one of the gray brick maze walls will cause the user to be bounced backward slightly.

Mouse control operates in a fashion very similar to keyboard control, but with the introduction of variable speed based on control position. Pressing the mouse sufficiently forward will effect forward motion just like the motion effected by the up arrow. Left and right mouse movements are used to rotate the player's view, just like the left and right arrow keys. Backward mouse movements (i.e., movements of the mouse back towards the user) are analogous to down-arrow key presses, that is, such movements result in backward movement in the direction currently being faced by the player.

Keyboard and mouse control can be used alternately in the same game session, or even used at the same time. Pressing the up arrow and moving forward simultaneously, for example, offers a sort of "run" feature, in which forward movement faster than what is attainable using either input device alone can be achieved.

The player should note that it is possible to wander off into the wasteland around the maze, and to continue indefinitely. No change in display will happen while travelling in the wasteland; a generic horizon display will be seen during this time. If the user wanders too far in such a direction, the maze may seem to be lost. If this happens the user should press ESC to exit the simulation, and then restart it.

By default, the maze will be 8 cubes by 8 cubes in size, not including the two solid walls on the sides without an entrance/exit. There is no specific indication when the maze is solved; however the program does reliably enforce the solidity of the walls.

Building the Code

To build the code, it is best to first extract both archives provided with this article to a single folder. This will allow "openmaze.exe" to find the images and the library it needs at runtime after it is built.

An example build command (e.g., for execution in the Windows "Command Prompt") is shown below. In this example, the folder "c:\test\" is assumed to contain the source code. This is a MinGW build command, but the code provided can be built using a wide variety of compilers. MinGW is a version of GCC, for example, so building under GNU/Linux should be very similar.

c:\mingw\bin\c++.exe c:\test\openmaze.cpp c:\test\glew.c 
       c:\test\glut32.lib -lglu32 -lopengl32 -oc:\test\openmaze.exe

Coordinate System

The development of a coordinate system is the main step necessary to combine the conceptual maze described in Fast Generation of Single-Solution Mazes with the OpenGL techniques from OpenGL Programming with MinGW. The simple array-based representation generated in the first of these two articles must be translated into a three-dimensional representation suitable for OpenGL.

Four constants in particular play a key role in this mapping: MAZE_EXTREME_LEFT, MAZE_EXTREME_TOP, HALF_CUBE, and FULL_CUBE. Similarly, a pointer returned by the function maze_innards() plays a key role in this coordinate mapping; specifically, it points to an array holding the definition of the variable portion of the maze, i.e., the maze other than the two solid walls in which there is no entrance or exit. Finally, the constants XSIZE and YSIZE often enter into coordinate calculations. These define the size of the abstract maze in its X and Y dimensions, e.g., 8 by 8 in the default build configuration. The role played by each of these values is shown in the diagram shown below:

Early 3D Maze

In this diagram, first note that the squares shown above actually translate to cubes in the program display. The OpenGL "Y" dimension, which is the vertical dimension in the simulation, is omitted from the diagram. (The "Y" dimension of the abstract maze translates to the "Z" dimension of the OpenGL rendering, as indicated in the diagram shown above.)

All the shaded squares in the diagram above (both light and dark gray) are solid cubes in the OpenGL rendering, textured to represent wall segments. The use of cubes in this way introduces some extra faces to the maze as supplied to the OpenGL rendering pipeline, in the form of invisible, internal cube boundaries. This has not proven to be an issue in actual practice. As shown, MAZE_EXTREME_LEFT and MAZE_EXTREME_TOP define a starting corner for the maze, and all the coordinate calculations in the code (e.g., for collision detection) use these constants in conjunction with size constants HALF_CUBE and FULL_CUBE. These constants all have type GLfloat, and together they describe the nominal values used in the OpenGL coordinate system, as well as how each abstract square from Fast Generation of Single-Solution Mazes is located in 3D space.

Furthermore, in this last diagram shown, note that the darker squares represent the contents of the array pointed to by maze_innards(). As shown, this pointer is indexed from maze_innards()[0][0] to maze_innards()[XSIZE-1][YSIZE-1] (the 7,7 shown above). Again, the solid walls are not counted. As in Fast Generation of Single-Solution Mazes, constants are defined such that each element can either be 0 (closed, i.e., a cube or wall), 1 (a false path), or 2 (on the optimal solution path).

New Abstractions

OpenGL Programming with MinGW rendered a single cube, about the origin, having a volume of 2 x 2 x 2. This is not too difficult in an OpenGL program; OpenGL is a state machine, and it can be placed into rectangle-drawing mode. Then, the program must simply pass vertex coordinates using glVertex3f() to draw the cube. Calls to glTexCoord2d() connect the designated texture to the solid.

The program described in this article takes the code used to do this drawing in OpenGL Programming with MinGW and extends it to:

  1. use some size constants to determine the cube's volume, and
  2. allow translation of the cube to form different wall segments for the maze.

The beginning of the cube() function, which draws a cube of the designated size centered around coordinate (x,y,z), is shown below:

void cube(GLfloat x, GLfloat y, GLfloat z) //Draws a cube centered at (x,y,z)
{
 // Top of cube
 glTexCoord2d(1.0,1.0);
 glVertex3f(x+HALF_CUBE, HALF_CUBE,z-HALF_CUBE); // Top Right Of The Quad (Top)
 glTexCoord2d(0.0,1.0);
 glVertex3f(x-HALF_CUBE, HALF_CUBE,z-HALF_CUBE); // Top Left Of The Quad (Top)
 glTexCoord2d(0.0,0.0);
 glVertex3f(x-HALF_CUBE, HALF_CUBE, z+HALF_CUBE); // Bottom Left Of The Quad (Top)
 glTexCoord2d(1.0,0.0);
 glVertex3f(x+HALF_CUBE, HALF_CUBE, z+HALF_CUBE); // Bottom Right Of The Quad (Top)
 //...

Compared to the analogous code from OpenGL Programming with MinGW, the code shown above exhibits two basic differences. First, +/-HALF_CUBE replaces the previous use of +/-1.0. Second, translation factors x, y, and z are respected, by simply adding them into each vertex coordinate.

A few other, similar abstractions are created in this latest program. For example, a general texture load function is employed. The program adds a static background image, which is a rectangle having its own texture distinct from the maze, so this function proved necessary. Its prototype is shown below:

GLuint maketex(const char* tfile,GLint xSize,GLint ySize);

The value returned by this function is a number assigned to the texture by OpenGL. This acts as a runtime handle for the texture resource.

Maze Rendering

The rendering of the maze display is handled by print_maze(). This function, shown below, is entirely based around calls to cube(). It begins by drawing the solid walls, whose centers are located at Y=MAZE_EXTREME_TOP+HALF_CUBE and Y=MAZE_EXTREME_TOP+HALF_CUBE+FULL_CUBE+(YSIZE*(FULL_CUBE))). (The first FULL_CUBE term in this last expression accounts for the top solid wall, which is not included in YSIZE). For both walls, the drawing extends across the full breadth of the maze, i.e., from the center of the leftmost column of cubes at MAZE_EXTREME_LEFT+HALF_CUBE to the center of the rightmost column at MAZE_EXTREME_LEFT+HALF_CUBE+((GLfloat)(XSIZE-1)*FULL_CUBE). After these walls are drawn, a nested loop is used to draw a cube for each element of maze_innards() having the value NO_PATH. Finally, note that each cube in both of these sections is centered around 0.0 in the "Y" dimension, which is also where the camera is located (after landing).

void print_maze() //Renders the necessary OpenGL cubes
{
 int x,y; 
 for(x=0; x<XSIZE ; ++x ) //Print a border
 {
  cube(MAZE_EXTREME_LEFT+HALF_CUBE+((GLfloat)x*FULL_CUBE),
  0.0,
  MAZE_EXTREME_TOP+HALF_CUBE);

  cube(MAZE_EXTREME_LEFT+HALF_CUBE+((GLfloat)x*FULL_CUBE),
  0.0,
  MAZE_EXTREME_TOP+HALF_CUBE+FULL_CUBE+(YSIZE*(FULL_CUBE)) );
 } 
 for(y=0; y<YSIZE ; ++y ) //Maze proper
 {
  for(x=0; x<XSIZE ; ++x )
  {
   if((maze_innards())[x][y]==NO_PATH)
   {
    cube(LEFTMOST_CUBE_CENTER+((GLfloat)x*FULL_CUBE),
    0.0,
    MAZE_EXTREME_TOP+HALF_CUBE+FULL_CUBE+((GLfloat)y*FULL_CUBE)); 
   }
  }
 }
}

Fast Sky / Ground

The sky and ground are handled using a common, flat display. This is just a background image (defined by "sky.bmp"), with a horizon drawn at its approximate midpoint. This image is drawn into 3D space using texturing and glVertex3f(), just like a cube face. (In fact, it is based on the cube's front.) The function sky() takes care of the necessary drawing:

void sky(GLuint haze)
{
 //Modeled after cube front
 glBindTexture(GL_TEXTURE_2D,haze); //Texture "haze" holds the sky image
 glBegin(GL_QUADS); 
 glTexCoord2d(1.0,1.0);
 glVertex3f( (windowwidth()/SKY_SCALE), (windowheight()/SKY_SCALE),-SKY_DISTANCE); 
 glTexCoord2d(0.0,1.0);
 glVertex3f( -(windowwidth()/SKY_SCALE), (windowheight()/SKY_SCALE),-SKY_DISTANCE); 
 glTexCoord2d(0.0,0.0);
 glVertex3f( -(windowwidth()/SKY_SCALE), -(windowheight()/SKY_SCALE),-SKY_DISTANCE); 
 glTexCoord2d(1.0,0.0);
 glVertex3f( (windowwidth()/SKY_SCALE), -(windowheight()/SKY_SCALE),-SKY_DISTANCE); 
 glEnd();
}

This function is called near the beginning of each frame, before gluLookAt() is used to position the camera. This means that the sky quadrangle is always placed directly in front of the camera; it is simply drawn in the back of the scene, off in the distance (specifically in the plane defined by Z=-SKY_DISTANCE), regardless of camera position. The gradients used in the texture give a cheap illusion of lighting (or, at least, add interest to the scene).

Finally, in the code above, note that windowwidth() and windowheight() allow the program to obtain the screen dimensions, and SKY_SCALE is used to reduce the sky surface down to dimensions that are not needlessly large. This constant was set to 6.0f based on ad hoc tuning during testing.

Surface Culling

"Culling" is a process by which many hidden OpenGL surfaces can be excluded from the rendering process early in its evolution, resulting in a performance boost. It relies on a property of point orderings (such as surface definitions) as they are rotated in 3D space. Consider what happens when a polygon face is drawn, for example, using four successive points that happen to be in clockwise order when the face is at the front of the solid. If rotated to the back of the solid, without altering camera position/orientation, these same points will be in counter-clockwise order. OpenGL can use this property to determine which surfaces of a solid are facing the viewer, and render these faces only.

In the surface-drawing code given above, comments around each call to glVertex3f() detail how each face of the cube (as well as the single face of the sky/ground object) is rendered in counter-clockwise order when seen with the surface rotated to face the viewer (top right, top left, bottom left, and finally bottom right). This is because the OpenGL culling mode selected, GL_CCW, expects this. The code used to set up surface culling is located in the function initgl(), and is shown below:

glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glShadeModel(GL_SMOOTH);

User Input

Under GLUT, it is very typical for application code to pass handler function pointers into event-specific GLUT library functions at startup. Then, GLUT calls into the functions whose addresses were passed as it becomes necessary during the simulation process. This is a form of inversion of control which is very typical of APIs, especially those declared primarily in C or assembly language.

In the demonstration program provided here, this function wire-up is handled by the code shown below:

glutSpecialFunc(&arrows);      //"Special" key presses
glutKeyboardFunc(&keypress);   //Regular key presses
glutPassiveMotionFunc(&mouse); //Mouse move

The first statement wires up event handler arrows(), for arrow key presses. (The arrow keys are "special" keys in OpenGL's nomenclature.) Then, a handler is established for other key presses (keypress()). Finally, a handler is set up for the mouse motion event.

The OpenGL signatures for these functions are very intuitive. This allowed the author to focus on the algorithms used to simulate the physics of the random maze world. The function arrows() demonstrates how many of the issues associated with control and motion are addressed in the simulation code:

void arrows(GLint key, GLint x, GLint y) 
{
 if(key == GLUT_KEY_UP) 
  move(WALK_KEY_SENSE);
 if(key == GLUT_KEY_DOWN) 
  move(-WALK_KEY_REVERSE_SENSE);  
 if(key == GLUT_KEY_RIGHT)
  rot+=ROTATE_KEY_SENSE;
 if(key == GLUT_KEY_LEFT)
  rot-=ROTATE_KEY_SENSE;
}

This function accepts as its parameters a key code, followed by two parameters that convey the position of the mouse. These two parameters are not used here, since there is no special interaction between keyboard and mouse. Each of the four arrow keys is checked for. If the left or right arrow is pressed, the variable rot, which holds the viewer's angular position about his/her own "Y" axis, is adjusted using a compile-time sensitivity constant (ROTATE_KEY_SENSE).

If the up arrow (forward) or the down arrow (back) is pressed, a bit more work must be done. In these cases, the viewer is actually changing position, not just orientation. Collision detection thus comes into play (as abstracted over by the move() function), and this is discussed in the next section.

Mouse-based motion is somewhat more complex than keyboard-based motion. One reason for this is the fact that movement performed using the mouse is proportional to the mouse position; this is fundamentally more complex than the system of keyboard control shown above, which is based on movements of fixed size. Ultimately, though, supporting such proportional control is a simple matter of performing an additional subtraction, involving control position and the midpoint of the control's range (which is windowwidth()/2.0f or windowheight()/2.0f here). This subtraction establishes the magnitude of the user's command, and is multiplied by the appropriate sensitivity constant. These tasks are performed by the statements shown below:

if(yin<CONTROLLER_PLAY) 
   move((yin-windowheight()/2.0f)*-WALK_MOUSE_SENSE);
   
if(yin>(windowheight()-CONTROLLER_PLAY))
   move(((windowheight()/2.0f)-yin)*WALK_MOUSE_REVERSE_SENSE);
   
if(xin<CONTROLLER_PLAY || xin>(windowwidth()-CONTROLLER_PLAY))
   rot+=(xin-(windowwidth()/2.0f))*ROTATE_MOUSE_SENSE;

In the code shown above, the constant CONTROLLER_PLAY is used to establish a "dead spot" around the center of the mouse's range, which is necessary for stability. As in arrows(), user mouse actions ultimately lead to either a call to move() or an adjustment to rot. The variables xin and yin, which are discussed in detail below, hold the current mouse position.

Another complexity associated with mouse control is the fact that motion is not necessarily a result of an OpenGL event. For example, if the user pushes the mouse forward, an event is generated. But if the user leaves the mouse in this position, no further events are generated, and yet the user interface description given above for this simulation specifies that movement does continue to occur.

To support this mode of operation, the code provided here keeps track of the current mouse position in file-level variables xin and yin. These are edited by the handler mouse(); but it is drawscene(), which is called on a recurring basis, that actually inspects these position variables. Also, a static flag variable is used within mouse() to ignore the first mouse move event; this is triggered automatically at startup by OpenGL, and it should not be interpreted as a user action.

Collision Detection

The detection of player impact with a maze wall is made easier by the use of right angles in constructing the maze. The "Coordinate System" section above already gives the basic information necessary to determine which entry in maze_innards() corresponds to the user's current position in the maze. If this array element contains NO_PATH, then it has been collided with.

After any move, such calculations are used in the code developed here to determine whether the user has impacted a wall. A slight threshold value is included into these calculations, which results in collisions happening somewhat before they would otherwise. This keeps the viewer from getting too close to the wall surface, which helps eliminate aliasing and glitches. The value of COLLIDE_MARGIN was set empirically during testing; however, the value selected does seem to translate well to disparate hardware.

The function collide() takes care of performing these calculations and determining if a collision has occurred, in which case it returns true. It begins as shown below:

bool collide() //Is player in a state of collision?
{
 int x,y;

 //Walls...
 if(x_at>=MAZE_EXTREME_LEFT-COLLIDE_MARGIN && 
   x_at<=MAZE_EXTREME_LEFT+XSIZE*FULL_CUBE+COLLIDE_MARGIN)
 {
  if( y_at<=(MAZE_EXTREME_TOP+FULL_CUBE)+COLLIDE_MARGIN && 
    y_at>=MAZE_EXTREME_TOP-COLLIDE_MARGIN)
  {
   return 1; 
  }

  if(y_at<=(MAZE_EXTREME_TOP+FULL_CUBE)+FULL_CUBE+
      (YSIZE*FULL_CUBE)+COLLIDE_MARGIN && y_at>= MAZE_EXTREME_TOP+
      FULL_CUBE+(YSIZE*FULL_CUBE)-COLLIDE_MARGIN)
  {
   return 1;
  }
}

This first section checks for collisions between the viewer and either of the two solid walls that surround maze_innards(). The first if determines whether x_at, the current position of the viewer in the OpenGL "X" dimension, is within the horizontal expanse of these walls. These walls both span the whole width of the maze, from MAZE_EXTREME_LEFT to MAZE_EXTREME_LEFT+XSIZE*FULL_CUBE. Note that COLLIDE_MARGIN is incorporated into each comparison, with the sign necessary to make the comparison more likely to return true.

The next if after that checks for intersection in the OpenGL "Z" dimension between the viewer and the upper of these two solid walls. Here, the variable y_at is inspected. Despite its name, this variable does indeed describe the position of the viewer in the OpenGL "Z" dimension; the "y" in the variable's name refers to the original "Y" dimension in the abstract, two-dimensional maze that underlies the simulation. The upper wall extends from MAZE_EXTREME_TOP to MAZE_EXTREME_TOP+FULL_CUBE, and these terms are present in the comparisons.

Finally, the third if shown above performs a similar check for intersection with the lower of the two solid walls. In this case, the comparison terms for the boundary both include an additional YSIZE*FULL_CUBE+FULL_CUBE in compared to the analogous terms in the top wall's if. This accounts for the vertical expanse of the maze proper, i.e. of the array pointed to by maze_innards(), along with the top solid wall.

The remainder of collide checks for the intersection between the viewer and those parts of the maze proper where walls have been placed. At a high level, this code operates by checking each element of the abstract maze (i.e., maze_innards()) for a wall, using nested loops. For each wall found, the code checks to see if the viewer position is within the boundaries of the wall cube. The necessary comparisons are similar to the last comparison shown, except that instead of adding YSIZE*FULL_CUBE to MAZE_EXTREME_TOP to get the vertical collision zone, we must add FULL_CUBE multiplied by the wall's loop index to MAZE_EXTREME_TOP. A similar calculation must be made to verify the collision in the "X" dimension:

//Maze proper
for(y=0; y<YSIZE ; ++y )
{
  for(x=0; x<XSIZE ; ++x )
  {
   if((maze_innards())[x][y]==NO_PATH)
   {
    if( x_at>=MAZE_EXTREME_LEFT+x*FULL_CUBE-COLLIDE_MARGIN && 
      x_at<=MAZE_EXTREME_LEFT+FULL_CUBE+x*FULL_CUBE+COLLIDE_MARGIN && 
      y_at>=MAZE_EXTREME_TOP+(y+1)*FULL_CUBE-COLLIDE_MARGIN && 
      y_at<=MAZE_EXTREME_TOP+(y+2)*FULL_CUBE+COLLIDE_MARGIN )
    {
     return 1;
    }
   }
  }
}

If the code reaches this point and has not returned true, then no collision has occurred, and 0 is returned to the called:

    return 0;
}

Collision Handling

The detection of collisions is actually more straightforward, in many ways, than the handling of collisions. In the code described here, collision handling comes into play during a call to move(). That function calls collide() to check for a forward or reverse move resulting in a collision, and must take action of some sort whenever true is returned.

Simply reversing the move that led to the collision will result in a simulation where the camera freezes when it hits the wall. Rotation can still occur when this happens, since rotation does not involve a call to move(). This is how the user can escape the collision.

A more exciting and playable design has the viewer bounce backward (i.e., in the opposite direction from the move) by some amount greater than the move itself. The problem with this technique is that it can result in a second collision, or in fact in any number of cascading collisions. The algorithm ultimately decided upon here allows for some constant-driven bounce-back to occur, but also for this effect to be cancelled in cases where it leads to a subsequent collision. This cancellation is observed infrequently, but its existence prevents a host of infrequent-but-unacceptable issues involving collision detection, e.g., situations where the user can pass through walls.

The move() function, which is built around the algorithm just described, is shown below:

void move(GLfloat amt) //Move, incorporating collision and bounceback
{
  x_at+=cos(rot)*amt;
  y_at+=sin(rot)*amt; 
  if(collide()) //Don't let player walk through walls
  {
   x_at-=BOUNCEBACK*cos(rot)*amt;
   y_at-=BOUNCEBACK*sin(rot)*amt;
  } 
  if(collide()) //Bounced into another wall... just reverse original move
  {
   x_at+=BOUNCEBACK*cos(rot)*amt;
   y_at+=BOUNCEBACK*sin(rot)*amt;
   x_at-=cos(rot)*amt;
   y_at-=sin(rot)*amt; 
  } 
}

In the function shown above, the movement is first executed. The sine and cosine functions are used to effect motion in the direction indicated by rot. (The role played by these functions is similar to their role in constructing the Unit Circle). Then, if a collision occurs, bounce-back is applied. Finally, if this leads to yet another collision, the bounce-back movement is cancelled, and then the original move itself is cancelled as well. This results in the viewer's position simply freezing at the wall, as in a simplistic implementation.

Future Expansion

The supplied code handles the basics of first-person perspective simulation in OpenGL. One obvious direction for further work is the addition of more competition into the program, e.g., formal timing or score-keeping. The addition of some sort of enemy or non-player character into the simulation would also add interest.

The development of various levels, with different textures, is another potential avenue for expansion. In fact, much greater use of OpenGL features like lighting is possible.

Finally, it would not be that difficult to add more vertical motion to the game, e.g., some sort of jumping feature. Whether this is a cosmetic gimmick, a really useful maze-viewing feature, or even a system that allows the user to actually walk on and over the maze walls depends on the complexity of the implementation; but all of these variations are envisioned by the work presented here, which aims to be a fast, stable basis for many such applications.

History

This is the second version of the article. Compared to the first version, the explanatory text has been improved.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"