Labyrinth 3D Maze Game

Download as pdf or txt
Download as pdf or txt
You are on page 1of 11

Labyrinth 3D Maze Game

2008-2009

4BA6 Computer Graphics

Jeff Warren 05531675

Maze rotation: Change camera angle: Toggle wireframe: Toggle music: Restart: Quit:

arrow keys e r m f ESC

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

Animated hierarchical enemies


Due to model importing issues (and 3DS max refusing to work at home) I decided to try drawing my own simple animated models. My enemies consist of a trapezoid textured and manually shaded shape (using quads, again) with 60 spikes (cones) coming out of the bottom, in an 8x8 formation with the corners cut off. The enemies rise up and down, at random offsets from each other, while the spikes on the bottom spin around. They are textured with a Thwomp image from the 16bit SNES game Super Mario World, for nostalgias sake. Enemies are inserted into cells via setting the 5th bit in the cells integer mask (this will be clear following maze generation).

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.

Additional camera angle


A first person camera angle was added, to help me learn more about the camera system. It is centred just above the ball, and looks in the direction that the ball is travelling. It looks parallel to the floor of the maze, hence maze rotation cannot be noticed when in this mode. The camera look-at point is calculated via rotations around the camera location manually.

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.

Good collision detection


This being another area of personal interest, I implemented a very computationally cheap technique. Collision detection is bounded. This greatly limits the need to test between the ball and every wall in the maze only potential walls in the current maze cell need to be checked for. Enemy collision needs only to be checked if there is indeed an enemy in the cell, another small saving. Given the quantity of computation needed to model and render everything, small optimisations such as this seem reasonable. Sphere collision detection against a single axis aligned wall is simply a subtraction. Upon the ball impacting an enemy, the player obviously dies and must restart. If the ball collides with a wall, the appropriate physics collision routine is invoked.

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.

Dual resolution texturing


In big mazes, we have a conflict with texture resolution between the two views. Textures which appeared to be too stretched in first person would appear in too high a resolution for pleasant rasterisation with each degree of rotation, every pixel would change from black to grey, giving a television static effect. To combat this, a scaling parameter was introduced, which loads textures at a different resolution depending on the viewpoint. This achieves a more attractive maze model in both views, and does not get noticed by players unless pointed out. Textures also scale according to the maze size, because a bigger maze will obviously be further away from the camera, causing the same issue again.

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.

A 10x10 generated 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

Maze completion screen

You might also like