Adding Obstacles

Congratulations. This is the first interactive 3D graphics application you've created. The effect of simulating car movement has been achieved. While you know that it's really the road that is moving below the car, it appears as if the car is speeding down the road. You've got half of the gameplay options implemented already. What you need now are the obstacles that you will be trying to avoid. Much like you added the car class, add a new class called "Obstacle". Make sure you add the using clauses for the Managed DirectX references for this code file as well.

Rather than using the same mesh for every obstacle, it would be nice to have a little variety in the obstacle list: in both type of obstacle as well as color. You can use the stock objects that can be created from the mesh object to vary the types of mesh, and you can use the materials to vary the colors. You should add the constants and variables needed for the obstacle class from Listing 6.10.

Listing 6.10 Obstacle Constants
 // Object constants private const int NumberMeshTypes = 5; private const float ObjectLength = 3.0f; private const float ObjectRadius = ObjectLength / 2.0f; private const int ObjectStacksSlices = 18; // obstacle colors private static readonly Color[] ObstacleColors = {         Color.Red, Color.Blue, Color.Green,         Color.Bisque, Color.Cyan, Color.DarkKhaki,         Color.OldLace, Color.PowderBlue, Color.DarkTurquoise,         Color.Azure, Color.Violet, Color.Tomato,         Color.Yellow, Color.Purple, Color.AliceBlue,         Color.Honeydew, Color.Crimson, Color.Firebrick }; // Mesh information private Mesh obstacleMesh = null; private Material obstacleMaterial; private Vector3 position; private bool isTeapot; 

As you can see from the first constant, there will be five different mesh types (sphere, cube, torus, cylinder, and teapot). The majority of these mesh types will have either a length parameter or a radius parameter. Since you will want the obstacles to be similarly sized, you should use constants for these parameters. Many of the mesh types also have extra parameters that control the number of triangles included in the mesh (stacks, slices, rings, and so forth). The last constant will be used for these parameters. Increase this value for more triangles (and a higher level of detail), decrease it for less.

The list of colors that is next is where you will pick your color for the mesh. I've randomly selected these colors, but you can feel free to add, remove, or change any of the colors in this list. You'll notice that you aren't storing an array of materials, nor are you storing textures for our meshes in this class. You know the default mesh types only contain one subset with no textures or materials, so this extra information isn't necessary.

Since the obstacles should be "lying" on the road as the player drives by, and you know in reality that it's the road that is moving (not the player), you will also need to ensure that the obstacles move with the world. You therefore maintain a position for the obstacle that will be updated every frame as the road moves. Finally, since you can't control the size of a teapot during creation, you need to know if we're using a teapot obstacle so that you can scale it to the correct size.

Replace the class constructor of the Obstacle class with the following new one:

 public Obstacle(Device device, float x, float y, float z) {     // Store our position     position = new Vector3(x, y, z);     // It's not a teapot     isTeapot = false;     // Create a new obstacle     switch (Utility.Rnd.Next(NumberMeshTypes))     {         case 0:             obstacleMesh = Mesh.Sphere(device, ObjectRadius, ObjectStacksSlices,                 ObjectStacksSlices);             break;         case 1:             obstacleMesh = Mesh.Box(device, ObjectLength, ObjectLength, ObjectLength);             break;         case 2:             obstacleMesh = Mesh.Teapot(device);             isTeapot = true;             break;         case 3:             obstacleMesh = Mesh.Cylinder(device, ObjectRadius, ObjectRadius, ObjectLength,                 ObjectStacksSlices, ObjectStacksSlices);             break;         case 4:             obstacleMesh = Mesh.Torus(device, ObjectRadius / 3.0f, ObjectRadius / 2.0f,                 ObjectStacksSlices, ObjectStacksSlices);             break;     }     // Set the obstacle color     obstacleMaterial = new Material();     Color objColor = ObstacleColors[Utility.Rnd.Next(ObstacleColors.Length)];     obstacleMaterial.Ambient = objColor;     obstacleMaterial.Diffuse = objColor; } 

You'll notice the use of the "Rnd" property on a utility class here. The source for this function is included on the CD, and is unimportant to the implementation of this function. Its goal is to simply return random numbers. The constructor for our obstacle stores the default position of the obstacle, and defaults to a non-teapot mesh (since there is only one case where it will be a teapot). It then randomly selects one of the mesh types and creates that mesh. Finally, it selects a random color from the list and uses that as the material color for the obstacles.

Before you're ready to get the obstacles into your game engine, though, there are a couple other things you will need to do. First, you need to have a function to update your objects' position to match the road. Add the following method:

 public void Update(float elapsedTime, float speed) {     position.Z += (speed * elapsedTime); } 

Again, you pass in the elapsed time to ensure that we maintain the same behavior on machines of different speeds. You also pass in the current speed of the road, so the objects will be moving as if they are lying on the road. With the obstacles being updated, you also need a method to do the rendering. Much like the car's draw method, you will need one for your obstacle as well. Add the method found in Listing 6.11 to your obstacle class.

Listing 6.11 Drawing Obstacles
 public void Draw(Device device) {     if (isTeapot)     {         device.Transform.World = Matrix.Scaling(ObjectRadius, ObjectRadius, ObjectRadius)             * Matrix.Translation(position);     }     else     {         device.Transform.World = Matrix.Translation(position);     }     device.Material = obstacleMaterial;     device.SetTexture(0, null);     obstacleMesh.DrawSubset(0); } 

Since the teapot mesh isn't correctly scaled after creation, if you are rendering one of those, you should first scale the teapot, and then move it into position. You then set the material for your color, set the texture to null, and draw the mesh.

Obviously, you will want to have more than one obstacle on the road at a time. What you need is an easy way to add and remove obstacles from the game engine. Using an array is a possibility, but not the greatest since it makes resizing the array a bit difficult. You should just make a collection class for storing your obstacles. Add the class found in Listing 6.12 to the end of your obstacle code file.

Listing 6.12 Obstacles Collection Class
 public class Obstacles : IEnumerable {     private ArrayList obstacleList = new ArrayList();     /// <summary>     /// Indexer for this class     /// </summary>     public Obstacle this[int index]     {         get         {             return (Obstacle)obstacleList[index];         }     }     // Get the enumerator from our arraylist     public IEnumerator GetEnumerator()     {         return obstacleList.GetEnumerator();     }     /// <summary>     /// Add an obstacle to our list     /// </summary>     /// <param name="obstacle">The obstacle to add</param>     public void Add(Obstacle obstacle)     {         obstacleList.Add(obstacle);     }     /// <summary>     /// Remove an obstacle from our list     /// </summary>     /// <param name="obstacle">The obstacle to remove</param>     public void Remove(Obstacle obstacle)     {         obstacleList.Remove(obstacle);     }     /// <summary>     /// Clear the obstacle list     /// </summary>     public void Clear()     {         obstacleList.Clear();     } } 

You will need to put a using clause for System.Collections at the top of your code file for this to compile correctly. This class contains an indexer for direct access to an obstacle, an enumerator so that the foreach construct works, and the three methods you will care about: add, remove, and clear. With this base functionality in your obstacles code file, you're ready to add obstacles to your engine.

First, you will need a variable that you can use to maintain the list of current obstacles in the scene. Add the following variable into the DodgerGame class:

 // Obstacle information private Obstacles obstacles; 

Now you need a function you can use to fill up the next road section with new obstacles. Use the following code for this:

 /// <summary> /// Add a series of obstacles onto a road section /// </summary> /// <param name="minDepth">Minimum depth of the obstacles</param> private void AddObstacles(float minDepth) {     // Add the right number of obstacles     int numberToAdd = (int)((RoadSize / car.Diameter - 1) / 2.0f);     // Get the minimum space between obstacles in this section     float minSize = ((RoadSize / numberToAdd) - car.Diameter) / 2.0f;     for (int i = 0; i < numberToAdd; i++)     {         // Get a random # in the min size range         float depth = minDepth - ((float)Utility.Rnd.NextDouble() * minSize);         // Make sure it's in the right range         depth -= (i * (car.Diameter * 2));         // Pick the left or right side of the road         float location = (Utility.Rnd.Next(50) > 25)?RoadLocationLeft:RoadLocationRight;         // Add this obstacle         obstacles.Add(new Obstacle(device, location, ObstacleHeight, depth));     } } 

This function will be the starting point for getting obstacles into the game. It first calculates the number of obstacles to add in this road section. It has to make sure that there will be enough room between obstacles for the car to fit; otherwise, the game wouldn't be very fair. After it calculates the number of obstacles needed for the road and the minimum space between obstacles, it adds them randomly to the road. It then adds them to the current list of obstacles. You'll notice the ObstacleHeight constant used when creating a new obstacle. The definition for this is

 private const float ObstacleHeight = Car.Height * 0.85f; 

Three things left before the obstacles are in the scene: You need to add a call into our obstacle addition method somewhere, you need to make sure you call the update function on each obstacle in the scene, and you need to render the obstacles. Since you will need a method to reset all the member variables back to defaults in order to start a new game, you should just create that function now, and use this method to be our initial call to AddObstacles. Add the method found in Listing 6.13.

Listing 6.13 Loading Default Game Options
 /// <summary> /// Here we will load all the default game options /// </summary> private void LoadDefaultGameOptions() {     // Road information     RoadDepth0 = 0.0f;     RoadDepth1 = -100.0f;     RoadSpeed = 30.0f;     // Car data information     car.Location = RoadLocationLeft;     car.Speed = 10.0f;     car.IsMovingLeft = false;     car.IsMovingRight = false;     // Remove any obstacles currently in the game     foreach(Obstacle o in obstacles)     {         // Dispose it first         o.Dispose();     }     obstacles.Clear();     // Add some obstacles     AddObstacles(RoadDepth1);     // Start our timer     Utility.Timer(DirectXTimer.Start); } 

This method takes the various member variables of the classes you might care about and resets them to the defaults. It also takes any existing obstacles in the list, disposes them, and clears the list before refilling the list with new obstacles. It finally starts the timer. You should add the call to this function after you've created the device in the InitializeGraphics method. Do not add this function into the OnDeviceReset method; you only want to call this function when a new game is being started.

 // Load the default game options LoadDefaultGameOptions(); 

Now you need to add the call to update the obstacles into the OnFrameUpdate method. You want to update every obstacle every frame, so you will need to enumerate them. Add the following into the OnFrameUpdate method before the car's update method:

 // Move our obstacles foreach(Obstacle o in obstacles) {     // Update the obstacle, check to see if it hits the car     o.Update(elapsedTime, RoadSpeed); } 

The last step before having the obstacles in the game engine would be to actually render them. In your OnPaint method, immediately after the car's draw method, you should add similar code for rendering your obstacles:

 // Draw any obstacles currently visible foreach(Obstacle o in obstacles) {     o.Draw(device); } 

Try running the game now! As you speed down the road, you pass a few obstacles, but then what's this? It seems that after you pass the first few obstacles, no more ever appear. If you remember correctly, the AddObstacles method only adds the obstacles to one section of road, and you are constantly moving the road sections, yet you never call the method again for these "new" road sections. Update the section of code you use to add new road sections as follows:

 // Check to see if we need to cycle the road if (RoadDepth0 > 75.0f) {     RoadDepth0 = RoadDepth1 - 100.0f;     AddObstacles(RoadDepth0); } if (RoadDepth1 > 75.0f) {     RoadDepth1 = RoadDepth0 - 100.0f;     AddObstacles(RoadDepth1); } 

Now things are looking more promising. You have the car speeding down the road, flying by obstacles (and at least currently, flying through obstacles). The obstacles themselves seem a little boring though. You should add a little movement to them. You can make them rotate around as you speed toward them! First, you'll need to add a few new member variables to the obstacle class to control the rotation:

 // Rotation information private float rotation = 0; private float rotationspeed = 0.0f; private Vector3 rotationVector; 

The speed they rotate and the axis they rotate on should be random so that they appear to all be moving differently. This can be easily accomplished by adding the following two lines at the end of the obstacle class constructor:

 rotationspeed = (float)Utility.Rnd.NextDouble() * (float)Math.PI; rotationVector = new Vector3((float)Utility.Rnd.NextDouble(),     (float)Utility.Rnd.NextDouble(),(float)Utility.Rnd.NextDouble()); 

Two things left to do before the obstacles will rotate correctly. First you need to include the rotation into the update function as follows:

 rotation += (rotationspeed * elapsedTime); 

Nothing unusual here; simply increase the rotation based on the elapsed time and the current (randomly selected) rotation speed. Finally, you will need to actually add a rotation into your world transform so that the obstacles are rendered rotating. Update the two world transform calls as follows:

 if (isTeapot) {     device.Transform.World = Matrix.RotationAxis(rotationVector, rotation)         * Matrix.Scaling(ObjectRadius, ObjectRadius, ObjectRadius)         * Matrix.Translation(position); } else {     device.Transform.World = Matrix.RotationAxis(rotationVector, rotation)         * Matrix.Translation(position); } 

Running the game now, you can see the obstacles spinning around randomly as you speed toward them. What's the next step? Well, it would be great if the car would actually collide with the obstacles, and you could keep a score to see how well you've done. Once you start adding the score, you'll also need to maintain game state. Add the following member variables to your main engine in the DodgerGame class:

 // Game information private bool isGameOver = true; private int gameOverTick = 0; private bool hasGameStarted = false; private int score = 0; 

All of the game's information is stored here. You know whether or not the game is over, whether the game has started for the first time, the last time the game was over, and the current score of the user playing the game. That's all well and good, but what do you need to actually do with all of this? You should start with the score, since that's what the player is going to care most about anyway. Every time the player passes an obstacle, you will want to increase the score. However, you also want to make the game harder as it progresses, so you should also increase the road's speed so that the obstacles come at the player even faster. First things first: Add a line to reset the score in the LoadDefaultGameOptions so that when a new game is started, the player won't start with points:

 car.IsMovingRight = false; score = 0; 

Now, in the OnFrameUpdate method, before you actually move the obstacles, add the following code:

 // Remove any obstacles that are past the car // Increase the score for each one, and also increase // the road speed to make the game harder. Obstacles removeObstacles = new Obstacles(); foreach(Obstacle o in obstacles) {     if (o.Depth > car.Diameter - (Car.Depth * 2))     {         // Add this obstacle to our list to remove         removeObstacles.Add(o);         // Increase roadspeed         RoadSpeed += RoadSpeedIncrement;         // Make sure the road speed stays below max         if (RoadSpeed >= MaximumRoadSpeed)         {             RoadSpeed = MaximumRoadSpeed;         }         // Increase the car speed as well         car.IncrementSpeed();         // Add the new score         score += (int)(RoadSpeed * (RoadSpeed / car.Speed));     } } // Remove the obstacles in the list foreach(Obstacle o in removeObstacles) {     obstacles.Remove(o);     // May as well dispose it as well     o.Dispose(); } removeObstacles.Clear(); 

What you want to do here is get a list of obstacles that you've already passed (this "list" should only contain one obstacle at a time). For each obstacle in this list, you will want to increase the score, as well as increase the road speed to raise difficulty, and increase the car's movement speed (although not as much as you increase the road's speed). After all that has been completed, you remove these obstacles from the real obstacle list. As you can see, a formula using the current road speed is used when calculating the score, so that as you get farther along, you will get more points. You'll also notice a method we use for the car that we haven't implemented yet. Nothing fancy here:

 /// <summary> /// Increment the movement speed of the car /// </summary> public void IncrementSpeed() {     carSpeed += SpeedIncrement; } 

Now you will need to add a new method to determine when the car has actually hit one of the obstacles. You should add this check into the obstacle class. Add the following method there:

 public bool IsHittingCar(float carLocation, float carDiameter) {     // In order for the obstacle to be hitting the car,     // it must be on the same side of the road and     // hitting the car     if (position.Z > (Car.Depth - (carDiameter / 2.0f)))     {         // are we on the right side of the car         if ((carLocation < 0) && (position.X < 0))             return true;         if ((carLocation > 0) && (position.X > 0))             return true;     }     return false; } 

Pretty simple stuff here; you check to see if the car is at the same depth as the obstacle and can hit it, and if it is, and it's on the same side of the road, return true. Otherwise, the car and obstacle haven't hit, so you can return false. With this code in now, you need to implement this into the game engine. Replace the obstacle update code with this new and improved code:

 // Move our obstacles foreach(Obstacle o in obstacles) {     // Update the obstacle, check to see if it hits the car     o.Update(elapsedTime, RoadSpeed);     if (o.IsHittingCar(car.Location, car.Diameter))     {         // If it does hit the car, the game is over.         isGameOver = true;         gameOverTick = System.Environment.TickCount;         // Stop our timer         Utility.Timer(DirectXTimer.Stop);     } } 

Now, after you update each obstacle, you check to see if it is hitting the car. If it is, the game is over. You set the game settings, and stop the timer.



Managed DirectX 9 Graphics and Game Programming, Kick Start
Managed DirectX 9 Kick Start: Graphics and Game Programming
ISBN: B003D7JUW6
EAN: N/A
Year: 2002
Pages: 180
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net