Labyrinth 3D Maze Game
Labyrinth 3D Maze Game
Labyrinth 3D Maze Game
2008-2009
Maze rotation: Change camera angle: Toggle wireframe: Toggle music: Restart: Quit:
Introduction
Initially, I intended to build a conventional maze game, with characters and enemies roaming around a flat plane. However, given that there were a number of potential bonus features to be implemented, this changed. I am interested in physics simulation, and given that I have previously studied the area of applied mathematics, I realised I should be able to implement some real world forces/acceleration/collisions, etc. Switching the game to be a pivoted maze with a non-direct player object control allowed me to realise this. At the early stages of learning OpenGL for the first time, it was also useful for beginning to understand exactly how rotation, translation and camera systems worked.
The Basics
Architecture
Starting off, it instantly became very obvious that to attain the final product with graphics, collisions, enemies etc, a strict data structure model would be needed. This could then be used to draw the appropriate graphical entities, and also be used by the physics system to model correct behaviour. Initially a quad set of 1D Boolean arrays was used for the maze model. The corresponding block of the maze could be tested at MAZE_DIMENSION*yCoord + xCoord (NB: the maze was always modelled as square). At this position in each of the four arrays, a Boolean for each of the top, bottom, left and right walls determined if that wall segment was present, or not. This became overly complicated as I tried to model thick walls, and was discarded in favour of an easier approach: a 2D Integer array. The required square could be tested at maze[yCoord][xCoord], each location containing a bit-mapped set of options. Initially the lower 4 bits were chosen for setting whether or not the segments of those walls were present or not. The 5th lowest bit was used for enemies, and maze generation (see later). Rather than concentrating on hacking together code as quickly as possible for the task at hand, I wished to architect a properly specified class model. Obviously at this point I would need a class to represent the game model the structure of the maze, current game state, maze rotation parameters, enemies, etc. I then created a rendering class, which would render according to the game model. A UI class came next, and other less important ones also followed (audio, enemies, etc). SDL was used for OpenGL libraries, and for texture loading, as the TCD lab .tga loader meant I needed to export everything to tga. Instead jpegs and pngs were usable, and the OpenGL library include and link procedure was simplified. SDL was also used for keyboard control.
The Maze
The maze is rendered using OpenGL quads. Every frame, the renderer class accesses the maze model and draws the floor and walls appropriately. A hierarchy of drawing helper functions achieves this in an elegant way, allowing for easier tweaking later on without having to dig through huge functions. A RenderSquare function was defined, obviously rendering a square. A texture scaling parameter is also passed in here, along with a GLuint specifying which texture to bind. RenderWalls will draw, for each square, the appropriate walls. The top and right squares will draw a wall cap if required, which bridges the walls thickness gap between it and the adjacent square. I decided to leave out the thickness of the outer wall of the maze purely as a visual decision. Due to each maze being a perfect maze, every square cells corner will be connected to at LEAST one wall. They can thus be drawn unconditionally. They consist of a frustum, with a hemisphere cap on top of each. Other functions render the ball, enemies, and pop up messages.
Camera angle
There is a God style camera angle as required, top down over the maze. This camera angle is static, and the rotation of the maze is obviously visible due to this. There is an extra camera angle, documented later on.
God view
As the ball rolls, it obviously rotates. Originally I wished to texture with a beachball or other colour map texture, but due to the time issue I never resolved the problem with this. Hence rolling cannot be seen. Rather than concentrate on adding more visual flair I spent extra time on the extras.
Gameplay
The player starts off in the bottom left corner of the maze. The objective is to get to the star teleporter, on the top right corner of the maze, to finish the game, as quickly as possible. They must do so by rotating the maze, fighting with the forces of gravity, friction, to work their way under any Thwomp enemies without being crushed on the way. I insert MAZE_DIMENSION + MAZE_DIMENSION/2 Thwomps, which gives an acceptable level of difficulty for any given maze size.
The RenderableEntity class takes care of the Thwomps, as it made little sense to split these between the GameModel and the Renderer, as would have been required by the code model otherwise. Location parameters are stored inside them, and a render function can be called upon them to display them. An updateModel function is called with every game tick to update their behaviour/position state. This RenderableEntity class is only used currently for Thwomps, but it can generalise to any moving non trivial object in the game.
The Extras
Framerate scheduling
When starting out with lab code, I noticed that it ran with a busy wait before drawing each frame. Since the 100% CPU usage was very wasteful, I added some framerate calculation code. It allows each frame to be perfectly scheduled, giving a constant framerate and suspending the process accordingly. (Given sufficient resources, otherwise obviously the framerate will simply drop)
Lookup tables
Since I was doing a lot of manual non matrix transformation trigonometric calculation, it seemed interesting to add a lookup table. It was used for physics calculation (see later), and was limited to reasonable accuracy. Calls to values between intervals are rounded to the closest one. The table is propagated during game loading, and calls can be made from anywhere via the GameModel class to this extra Math class.
Backface culling
A popular and easy way to speed up OpenGL programs is to perform manual culling by not calling OpenGL functions to display anything which the programmer knows will be occluded by a closer item, 1) the overhead of the call is saved, and 2) the overhead of OpenGLs own culling can be avoided. In God mode, I will take the Y axis for example. If the top is tilted away from the player, we know that maze cell bottoms cannot be seen on cells from the middle upwards. Similarly, cell tops of maze cells from the middle downwards cannot ever be seen, due to the rotational restriction of 20 degrees off flat that the player is restricted to. In first person mode, another simple approach can be taken. Depth first searching techniques could have been used, but again there was a time issue. I simply only draw cells on the players current X and Y rows, as well as a square of 3 in every direction. This ensures that the player cannot see any non-rendered entities around corners.
Audio
Audio effects were added to extend the game play experience. Sound samples are played upon winning, losing, and upon bouncing off a wall. An mp3 sound track is also played. FMod (http://www.fmod.org) was used for audio, since it was not a crucial part of the project. FMod is a commercial, multiplatform library used regularly for professional game development, but it is free for use in non-profit applications. It allows for easy management of sound channels, samples, and streams, plays many formats, and is arguably the easiest and most cost effective solution, for profit or non-profit development.
Maze generation
A random maze generation algorithm was added, giving almost infinite combinations of level. Another algorithm was initially used, but Prims algorithm was then used as it is the fastest. Using a stack, a fully filled in maze is randomly walked, knocking down walls between cells on the walk. The algorithm advances until all adjoining cells have been visited, then retreats back a step. The maze is regenerated every time the player starts a new game. It is very unlikely that two players will ever play the same level with same combination of enemy positioning. Coupled with the ability to make the maze assume any size, there is very good variation of gameplay.
Resizable model
All the way through the development process, I aligned, sized, and placed every item in the game according to a few parameters (ball radius, wall length, wall width, maze size, and so on). By simply editing one of these parameters (in globals.h, SDL_config was not able to be added due to time constraints) the game will alter perfectly, resizing everything as required. The camera system also accommodates this, zooming to fill the screen with the entire maze.
Physics
As mentioned, real-world physics were implemented in the game. A gravitational constant, g, is defined (in real life this would be 9.8 metres per second squared). The ball sitting on the inclined plane of the maze will have its i and j forces resolved, and will accelerate down the plane as one would expect. Obviously, the more inclined the plane, the greater the rate of acceleration. By definition acceleration refers to a change in velocity, of course this will cause the ball to slow down if it is already moving in direction i, uphill. These forces are calculated during every game tick, in the game model. They give the game a far more real world feel than linear or simple proportional-to-angle-of-inclination change in velocity.
Resolved forces.
This can simply be applied in the X and Y planes independently to yield 3D behaviour.
Accurate collisions are also modelled. A ball sliding across a smooth surface into a wall, moving perpendicularly to the wall, will bounce back in the direction from which it came, at a reflected velocity directly proportional to its velocity before impact. We call this factor the coefficient of restitution. Changing it will affect how energetic collisions are i.e. how hard you bounce back off the wall.
Obviously the coefficient of friction lies between 0 and 1. Setting it outside of these bounds would cause the collision to create energy, and the ball will speed up. Again, applying this model to the ball in the X and Y directions allows for 3D collisions and bouncing.
Friction is accounted for as an opposing force proportional to the current velocity in a given direction. Rotational friction and inertia was not considered given the scope of the project.
Conclusion
This system was well architected, and the decision to spend extra time building a modular class structure paid off later on during the project. As more elements needed to be added to the game, what would have been reduced to hack-on code with variables stored in arbitrary places was instead very approachable. The opportunity to learn how to use prebuilt libraries was very useful, and I will certainly be using FMod in future C++ projects which require audio support. While it is not the most stunning game visually, I am very happy with the technical system behind the scenes.
A 3x3 maze
Failure screen