Marvelous Maze

Applies to: Windows Phone 7

Published: August 2011

Author: Rod Stephens

WROX Tech Editors for Windows Phone 7 Articles

Wrox Windows Phone 7 Books

This topic contains the following sections.

Summary: This article explains the Marvelous Maze game that brings together many of the techniques you have learned in this series of articles. It explains how the game loads a model created with MilkShape 3D, displays different avatar points of view, tracks the avatar’s position, allows the avatar to climb stairs and fall, and lets the player collect gems and remove obstacles.

At this point, you have a lot of 3-D game programming tools at your disposal. You can now do the following:

  • Display basic shapes

  • Implement the game’s Update and Draw methods

  • Draw shaded or textured objects

  • Draw two-dimensional objects in front of or behind a three-dimensional scene

  • Respond to the player’s screen taps

  • Load models created in other tools such as MilkShape 3D and Blender

  • Detect collisions to know when two objects touch

  • Simulate ballistic

  • Play simple sound effects and songs

The Marvelous Maze example program combines these techniques to build a simple maze search game. This article briefly describes these techniques and how the game uses them. It also explains a couple other features of the game and how they are implemented.

Unfortunately, there is not enough space to display all (or even the majority) of the code and walk through it a line at a time. The sections that follow describe the key pieces of code and new techniques that have not been covered in the previous articles. Download the example to see the complete source code.

The goal in the Marvelous Maze game is for the player to find the spinning globe shown in Figure 1. To find the globe, the player must navigate through a small maze. (The white ball at the bottom center of the screen is the back of the avatar’s head. A more elaborate version of the game could give the avatar hair or a hat.)

Figure 1. To win, the player must find the spinning globe.

Referenced Image

At several points, obstacles, such as the blue one shown in Figure 2, block the player. To get past an obstacle, the player must have the corresponding gem.

Figure 2. Obstacles such as this one block the player’s path.

Referenced Image

Figure 3 shows the player about to collect the green gem, having already collected the red one. When the player collects a gem, a small icon appears in the screen’s lower-right corner, as shown in Figures 1 and 3.

Figure 3. The player must collect gems to get past the obstacles.

Referenced Image

To make the game more interesting, the maze has two levels. Figure 4 shows the maze in MilkShape 3D. The front wall and ceiling are not shown because they are modeled as planes and the view is looking at their back surfaces, so they are culled. In Figure 4, you can see that the front room is two stories tall with doors leading back to rooms at both levels. Stairs on the left let the player climb from the first level to the second. Figure 5 shows the stairs as seen by the player in the game.

Figure 4. The game’s front room is two stories tall.

Referenced Image

Figure 5. To get to the second level, the player climbs the stairs.

Referenced Image

To get from the second level back to the first, the player can go back down the stairs or can move off the landing shown in Figure 4. If the player steps off the landing or the side of the stairs, the avatar falls to the ground floor below using ballistic motion. The player moves by touching the screen somewhere around the ship’s wheel displayed on the back of the avatar’s head. If the player touches to the left or right of the wheel, the avatar turns left or right. If the player touches above or below the wheel, the avatar moves forward or backward. If the player touches at an angle, the avatar turns and moves at the same time.

(I am not completely thrilled with this movement scheme. It lets the player turn and move at the same time, but it can be hard to control that kind of movement. Four arrow buttons to turn left/right and move forward/backward also works, but that feels a bit cumbersome. This is an area where the program can use further study.)

I created most of the maze in MilkShape 3D, exported it in Wavefront .obj format, imported that into Blender, and then exported the result in the Alias .fbx format. The program then loads the model.

In addition to the stationary maze loaded from the model, the program uses code to create several movable objects, including the rotating globe, the gems (which also rotate), the obstacles, and the player’s avatar. Each of these objects is represented by an instance of a class. The avatar is represented by the DrawableSkeleton class. The globe and gems are represented by Globe and Gem classes.

The Globe and Gem classes provide methods that help the program move the objects. Their Update methods rotate the objects, and their Draw methods draw them appropriately. Each of the class’s constructors takes parameters that describe the object to create. They store the object’s position in a Vector3 object named Position.

Both of the classes’ Update methods use the following code to update a variable Theta that determines the object’s angle of rotation:

public float Theta = 0;
...
public void Update(GameTime gameTime)
{
    // Rotate dtheta radians per second.
    const float dtheta = MathHelper.TwoPi / 6;
    Theta += (float)(dtheta * gameTime.ElapsedGameTime.TotalSeconds);
}

Both of the classes’ Draw methods use the following code to draw the objects:

public void Draw(Matrix worldMatrix, Matrix viewMatrix,
    Matrix projectionMatrix, FillMode fillMode, CullMode cullMode)
{
    if (!Visible) return;
            
    // Create the transformation including the
    // current rotation and translation to (Cx, Cy, Cz).
    Matrix currentWorld =
        Matrix.CreateRotationY(Theta) *         // Rotate.
        Matrix.CreateTranslation(Position) *    // Translate.
        worldMatrix;                            // Other world 
                                                // transformation.
    Drawable.Draw(currentWorld, viewMatrix, projectionMatrix, fillMode, cullMode);
}

This code creates a rotation matrix representing the object’s current angle of rotation and a transformation matrix to move the object to its desired position. It adds those matrices to the current world transformation and draws the object.

There are only a few small differences between the Globe and Gem classes. Globe displays a textured object, while Gem displays a colored object. The Gem class has a bounding box and an Intersects method that the main program uses to determine whether the player has collected a gem. The Gem class also plays a sound effect when the player collects its object.

The last new type of object used by the program is a barrier represented by the Barrier class. The Globe and Gem classes represent objects that rotate. The Barrier class represents an object that can slide down into the floor and disappear.

Most of the time, a Barrier object just sits there, but if the player’s avatar touches it and the player has collected the corresponding gem, the main program calls the Barrier object’s Remove method, shown in the following code:

// Remove the barrier.
private bool IsRemoved = false;
public void Remove()
{
    // Use IsRemoved so that we do this only the first time Remove is called.
    // For example, do not play the sound again if the player
    // bumps the partly removed barrier.
    if (IsRemoved) return;

    IsRemoved = true;
    RemovedSound.Play();
    Vy = -20;
}

This code does nothing if IsRemoved is true, meaning that the Barrier object is already gone. If IsRemoved is false, the code sets IsRemoved to true, plays a sound effect, and sets a velocity variable Vy to -20. The Barrier class’s Update method, shown in the following code, uses Vy to move the barrier downwards:

public void Update(GameTime gameTime)
{
    if (Vy < 0)
    {
        float dy = (float)(Vy * gameTime.ElapsedGameTime.TotalSeconds);
        YOffset += dy;

        // See whether we should stop moving.
        if (YOffset < MinY)
        {
            Visible = false;
            Vy = 0;
        }
    }
}

If Vy is less than 0, the Update method multiplies Vy by the elapsed time and adds it to the object’s YOffset variable. If the new YOffset value is less than a predetermined MinY value, the barrier sets Visible = false to hide itself (it has disappeared into the floor) and sets Vy = 0 so that it stops moving.

The Barrier class’s Draw method, shown in the following code, displays the object:

public void Draw(Matrix worldMatrix, Matrix viewMatrix,
    Matrix projectionMatrix, FillMode fillMode, CullMode cullMode)
{
    if (!Visible) return;

    // Offset for the current Y coordinate.
    Matrix currentWorld = worldMatrix * Matrix.CreateTranslation(0, YOffset, 0);

    // Draw.
    Drawable.Draw(currentWorld, viewMatrix, projectionMatrix, fillMode, cullMode);
}

This code creates a translation matrix to move the object distance YOffset in the Y direction. It adds this translation to the current world transformation and draws the object.

To summarize, when the avatar touches a barrier, it sets its Vy variable to -20. The Update method then moves the barrier downward by the distance Vy per second until it disappears into the floor. Meanwhile, the Draw method draws the barrier translated downward appropriately.

Three-dimensional games typically use one of two points of view: first-person or third-person.

In a first-person point of view, shown on the left in Figure 6, the camera is positioned where the avatar’s eyes would be. Sometimes parts of the avatar are visible. For example, the player might be able to see the avatar’s arms, a weapon, or the front part of a vehicle sticking out in front. The first-person point of view is particularly useful when the player must aim a weapon or other object because the player’s eyes are aligned with the avatar’s.

Figure 6. Three possible points of view include first-person (left), third-person (middle), and over-the-shoulder (right).

Referenced Image

In a third-person point of view, shown in the middle of Figure 6, the player can see the avatar. This point of view is useful when the player needs fine control over the avatar. For example, if the player must maneuver the avatar through tightly spaced obstacles, a third-person point of view can be useful because the player can see the avatar’s shoulders, legs, and other body parts that might not be visible in a first-person point of view. The third-person view is also useful if the avatar’s appearance is important. For example, the avatar might show equipment such as armor, clothes, or tools that would not be visible in a first-person view.

One drawback to the third-person point of view is that the camera must be positioned farther from the avatar. That means the program must take some extra care when displaying the scene with the avatar backed up against obstacles. For example, if the avatar is backed up against a wall, the camera’s position will be inside or behind the wall. If the wall is modeled as a solid object, the camera’s view will be blocked.

Often, in a third-person view, the camera is also positioned slightly above the avatar with a slight downward angle looking at a point a bit in front of the avatar. That downward angle allows the player to see objects directly in front of the avatar but also limits the player’s ability to see objects higher up.

The Marvelous Maze game uses a modified version of the third-person point of view where the camera is positioned to show just the avatar’s head as shown on the right in Figure 6. This point of view, which I call over-the-shoulder, provides a bit more context than a first-person view. It also has less trouble with positioning the camera inside walls and other objects as a third person view because the camera is much closer to the avatar.

Selecting the point of view in the Marvelous Maze game is surprisingly simple. The avatarEyeOffset variable determines where the camera is positioned relative to the center of the avatar’s head. The program uses the following code to select a point of view:

// Points of view.
Vector3 avatarEyeOffset = new Vector3(0, 3, 6);       // Over the shoulder.
//Vector3 avatarEyeOffset = new Vector3(0, 9, 25);      // Third person.
//Vector3 avatarEyeOffset = new Vector3(0, 0, 0);       // First person.
//Vector3 avatarEyeOffset = new Vector3(0, 200, 6);     // Bird's eye view.

For example, an offset of (0, 9, 25) places the camera 3 units above and 6 units behind the avatar in the Z direction.

If avatarEyeOffset is (0, 0, 0), the camera is placed inside the head to give a first-person view. Because the head’s triangles are oriented so that they are clipped when seen in a counterclockwise orientation, the head is essentially transparent when viewed from the inside. (You could also simply not draw the avatar in this viewpoint.)

If the offset is (0, 9, 25), the camera is relatively far behind and above the avatar and gives a third-person view.

If the offset is (0, 3, 6), the camera is only a little bit behind and above the avatar and gives an over-the-shoulder view.

The program also contains code to display a bird’s-eye view from far above. This point of view, which is another kind of third-person view, is not really useful for game play but can be handy for debugging.

To control the projection transformation, the program uses a “look at” offset in addition to the camera’s offset. The “look at” offset determines the point where the camera is pointed relative to the avatar. This program uses the following code to make the camera look at a point 10 units in front of the avatar:

Vector3 avatarLookAtOffset = new Vector3(0, 0, -10);

Whenever the player turns the avatar, the program also rotates the camera and “look at” offset vectors to keep the camera in the correct position relative to the avatar and pointing in the right direction. The following code shows how the program updates the avatar’s angle of rotation and these vectors:

avatarAngle += dtheta;
Matrix rotateMatrix = Matrix.CreateRotationY(dtheta);
avatarEyeOffset = Vector3.Transform(avatarEyeOffset, rotateMatrix);
avatarLookAtOffset = Vector3.Transform(avatarLookAtOffset, rotateMatrix);

When the player moves the avatar, the following code updates the avatar’s position by moving it a desired distance in the direction of the “look at” vector:

Matrix moveMatrix = Matrix.CreateTranslation(avatarLookAtOffset * distToMove);
newAvatarPosition = Vector3.Transform(newAvatarPosition, moveMatrix);

When it’s finally time to draw, the program uses the current camera and “look at” offsets to build a viewing transformation, as shown in the following code:

private void SetUpCamera()
{
    // Position and orient the avatar.
    avatar.CurrentTransformation =
        Matrix.CreateRotationY(avatarAngle) *
        Matrix.CreateTranslation(avatarPosition);

    eyeVector = avatarPosition + avatarEyeOffset;
    lookAtVector = avatarPosition + avatarLookAtOffset;

    viewMatrix = Matrix.CreateLookAt(eyeVector, lookAtVector, upVector);
}

The code updates the avatar’s current transformation so that it can properly draw itself. It creates vectors representing the camera’s position and a “look at” position. It then uses those values to create the viewing projection.

One of the trickiest pieces of this program is the code that handles the stairs and falling. Whenever the player moves the avatar, the program calculates a new Y position for the avatar. If the avatar’s X and Z position place it over the stairs, the code makes the Y position a linear function of how far up the stairs the avatar is. If the avatar is near the bottom of the stairs, the Y position is small. When the avatar is at the top of the stairs, the Y position is the same as that of the second floor landing.

If the avatar is not over the stairs, either it is standing on one of the two floors or it should be falling.

If the avatar is over the open area in the front room and is above the floor level, it should be falling. Also, if the avatar is above the ground floor and below the upper floor, it is falling and should continue to do so.

In either of these cases, the following code executes to make the avatar fall:

// Falling over the open area.
float gameSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
avatarVelocity += avatarAcceleration * gameSeconds;
newAvatarPosition += avatarVelocity * gameSeconds;
if (newAvatarPosition.Y <= avatarDown)
{
    // The avatar hit the ground.
    newAvatarPosition.Y = avatarDown;
    avatarVelocity = new Vector3(0, 0, 0);
}

This code should look somewhat familiar from the discussion of ballistic motion. The code calculates the elapsed time. It adds the avatar’s acceleration multiplied by the elapsed time to the current velocity. It then adds the avatar’s velocity multiplied by the elapsed time to the current position. This accelerates the avatar towards the ground.

If the avatar’s Y position is less than the avatar’s height (stored in the avatarDown variable), its feet have touched the ground floor. In that case, the program sets the Y position so that the avatar is exactly on the ground floor and sets the avatar’s velocity to 0 so that it stops falling.

This series of articles explained fundamental 3-D game programming techniques such as building and displaying basic shapes, loading models, creating textured objects, responding to player taps, detecting collisions, and more. Using those techniques, you can build a 3-D game such as Marvelous Maze. You can also build just about any other kind of game you can imagine that uses similar elements where the player moves through a 3-D scene.

Even after you have mastered all of these techniques, there is still plenty left for the motivated game developer to learn about 3-D XNA game programming, such as the following:

  • Gestures

  • Mirrors

  • Shadows

  • Custom Effects

  • Advanced lighting

  • Billboarding (keeping a surface always facing the game’s camera)

  • Pixel and vertex shaders

  • Terrain modeling

  • Skeletons

  • Vertex and index buffers (for faster performance)

  • Particle systems (for effects such as smoke and fire)

  • Viewports (to show multiple points of view)

Experiment with the techniques you have learned, and take a look at some of these more advanced topics. Then see what kinds of games you can build. If you come up with something cool, let me know at RodStephens@CSharpHelper.com. I would love to see what you have built, and I am sure others would too!

Previous Article: Game Physics

Show: