XNA 4-3D:Getting the battle-tanks into game world

Exclusive offer: get 50% off this eBook here
XNA 4 3D Game Development by Example: Beginner's Guide

XNA 4 3D Game Development by Example: Beginner's Guide — Save 50%

Create action-packed 3D games with the Microsoft XNA Framework with this book and ebook.

$29.99    $15.00
by Kurt Jaegers | September 2012 | Beginner's Guides Enterprise Articles Microsoft Open Source

In this article, the author of XNA 4 3D Game Development by Example Beginner's GuideKurt Jaegers will cover all that is necessary to get battle tanks into the game and placed in the game world. This can be accomplished by performing the following:

  • Adding models to our game's content project and loading them into the game
  • Drawing the tank model to the screen
  • Animating the various components of the tank model
  • Matching the elevation of the tank to its position on the generated terrain
  • Adding a second tank and positioning both tanks appropriately on the map

Adding the tank model

For tank battles, we will be using a 3D model available for download from the App Hub website (http://create.msdn.com) in the Simple Animation CODE SAMPLE available at http://xbox.create.msdn.com/en-US/education/catalog/sample/simple_animation. Our first step will be to add the model to our content project in order to bring it into the game.

Time for action – adding the tank model

We can add the tank model to our project by following these steps:

  1. Download the 7089_06_GRAPHICSPACK.ZIP file from the book's website and extract the contents to a temporary folder.
  2. Select the .fbx file and the two .tga files from the archive and copy them to the Windows clipboard.
  3. Switch to Visual Studio and expand the Tank BattlesContent (Content) project.
  4. Right-click on the Models folder and select Paste to copy the files on the clipboard into the folder.
  5. Right-click on engine_diff_tex.tga inside the Models folder and select Exclude From Project.
  6. Right click on turret_alt_diff_tex.tga inside the Models folder and select Exclude From Project.

What just happened?

Adding a model to our game is like adding any other type of content, though there are a couple of pitfalls to watch out for.

Our model includes two image files (the .tga files&emdash;an image format commonly associated with 3D graphics files because the format is not encumbered by patents) that will provide texture maps for the tank's surfaces. Unlike the other textures we have used, we do not want to include them as part of our content project. Why not?

The content processor for models will parse the .fbx file (an Autodesk file format used by several 3D modeling packages) at compile time and look for the textures it references in the directory the model is in. It will automatically process these into .xnb files that are placed in the output folder &endash; Models, for our game.

If we were to also include these textures in our content project, the standard texture processor would convert the image just like it does with the textures we normally use. When the model processor comes along and tries to convert the texture, an .xnb file with the same name will already exist in the Models folder, causing compile time errors.

Incidentally, even though the images associated with our model are not included in our content project directly, they still get built by the content pipeline and stored in the output directory as .xnb files. They can be loaded just like any other Texture2D object with the Content.Load() method.

Free 3D modeling software
There are a number of freely available 3D modeling packages downloadable on the Web that you can use to create your own 3D content. Some of these include:

  • Blender: A free, open source 3D modeling and animation package. Feature rich, and very powerful. Blender can be found at http://www.blender.org.
  • Wings 3D: Free, open source 3D modeling package. Does not support animation, but includes many useful modeling features. Wings 3D can be found at http://wings3d.com.
  • Softimage Mod Tool: A modeling and animation package from Autodesk. The Softimage Mod Tool is available freely for non-commercial use. A version with a commercial-friendly license is also available to XNA Creator's Club members at http://usa.autodesk.com/adsk/servlet/pc/item?id=13571257&siteID=123112.

 

Building tanks

Now that the model is part of our project, we need to create a class that will manage everything about a tank. While we could simply load the model in our TankBattlesGame class, we need more than one tank, and duplicating all of the items necessary to handle both tanks does not make sense.

Time for action – building the Tank class

We can build the Tank class using the following steps:

  1. Add a new class file called Tank.cs to the Tank Battles project.
  2. Add the following using directives to the top of the Tank.cs class file:

    using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics;

  3. Add the following fields to the Tank class:

    #region Fields private Model model; private GraphicsDevice device; private Vector3 position; private float tankRotation; private float turretRotation; private float gunElevation; private Matrix baseTurretTransform; private Matrix baseGunTransform; private Matrix[] boneTransforms; #endregion

  4. Add the following properties to the Tank class:

    #region Properties public Vector3 Position { get { return position; } set { position = value; } } public float TankRotation { get { return tankRotation; } set { tankRotation = MathHelper.WrapAngle(value); } } public float TurretRotation { get { return turretRotation; } set { turretRotation = MathHelper.WrapAngle(value); } } public float GunElevation { get { return gunElevation; } set { gunElevation = MathHelper.Clamp( value, MathHelper.ToRadians(-90), MathHelper.ToRadians(0)); } } #endregion

  5. Add the Draw() method to the Tank class, as follows:

    #region Draw public void Draw(ArcBallCamera camera) { model.Root.Transform = Matrix.Identity * Matrix.CreateScale(0.005f) * Matrix.CreateRotationY(TankRotation) * Matrix.CreateTranslation(Position); model.CopyAbsoluteBoneTransformsTo(boneTransforms); foreach (ModelMesh mesh in model.Meshes) { foreach (BasicEffect basicEffect in mesh.Effects) { basicEffect.World = boneTransforms[mesh.ParentBone. Index]; basicEffect.View = camera.View; basicEffect.Projection = camera.Projection; basicEffect.EnableDefaultLighting(); } mesh.Draw(); } } #endregion

  6. In the declarations area of the TankBattlesGame class, add a new List object to hold a list of Tank objects, as follows:

    List tanks = new List();

  7. Create a temporary tank so we can see it in action by adding the following to the end of the LoadContent() method of the TankBattlesGame class:

    tanks.Add( new Tank( GraphicsDevice, Content.Load(@"Models\tank"), new Vector3(61, 40, 61)));

  8. In the Draw() method of the TankBattlesGame class, add a loop to draw all of the Tank objects in the tank's list after the terrain has been drawn, as follows:

    foreach (Tank tank in tanks) { tank.Draw(camera); }

  9. Execute the game. Use your mouse to rotate and zoom in on the tank floating above the top of the central mountain in the scene, as shown in the following screenshot:

What just happened?

The Tank class stores the model that will be used to draw the tank in the model field. Just as with our terrain, we need a reference to the game's GraphicsDevice in order to draw our model when necessary.

In addition to this information, we have fields (and corresponding properties) to represent the position of the tank, and the rotation angle of three components of the model. The first, TankRotation, determines the angle at which the entire tank is rotated.

As the turret of the tank can rotate independently of the direction in which the tank itself is facing, we store the rotation angle of the turret in TurretRotation. Both TankRotation and TurretRotation contain code in their property setters to wrap their angles around if we go past a full circle in either direction.

The last angle we want to track is the elevation angle of the gun attached to the turret. This angle can range from 0 degrees (pointing straight out from the side of the turret) to -90 degrees (pointing straight up). This angle is stored in the GunElevation property.

The last field added in step 3 is called boneTransforms, and is an array of matrices. We further define this array while defining the Tank class' constructor by creating an empty array with a number of elements equal to the number of bones in the model.

But what exactly are bones? When a 3D artist creates a model, they can define joints that determine how the various pieces of the model are connected. This process is referred to as "rigging" the model, and a model that has been set up this way is sometimes referred to as "rigged for animation".

The bones in the model are defined with relationships to each other, so that when a bone higher up in the hierarchy moves, all of the lower bones are moved in relation to it. Think for a moment of one of your fingers. It is composed of three distinct bones separated by joints. If you move the bone nearest to your palm, the other two bones move as well – they have to if your finger bones are going to stay connected!

The same is true of the components in our tank. When the tank rotates, all of its pieces rotate as well. Rotating the turret moves the cannon, but has no effect on the body or the wheels. Moving the cannon has no effect on any other parts of the model, but it is hinged at its base, so that rotating the cannon joint makes the cannon appear to elevate up and down around one end instead of spinning around its center.

We will come back to these bones in just a moment, but let's first look at the current Draw() method before we expand it to account for bone-based animation.

Model.Root refers to the highest level bone in the model's hierarchy. Transforming this bone will transform the entire model, so our basic scaling, rotation, and positioning happen here. Notice that we are drastically scaling down the model of the tank, to a scale of 0.005f. The tank model is quite large in raw units, so we need to scale it to a size that is in line with the scale we used for our terrain.

Next, we use the boneTransforms array we created earlier by calling the model's CopyAbsoluteBoneTransformsTo() method. This method calculates the resultant transforms for each of the bones in the model, taking into account all of the parent bones above it, and copies these values into the specified array.

We then loop through each mesh in the model. A mesh is an independent piece of the model, representing a movable part. Each of these meshes can have multiple effects tied to it, so we loop through those as well, using an instance of BasicEffect created on the spot to render the meshes.

In order to render each mesh, we establish the mesh's world location by looking up the mesh's parent bone transformation and storing it in the World matrix. We apply our View and Projection matrices just like before, and enable default lighting on the effect. Finally, we draw the mesh, which sends the triangles making up this portion of the model out to the graphics card.

The tank model
The tank model we are using is from the Simple Animation sample for XNA 4.0, available on Microsoft's MSDN website at http://xbox.create.msdn.com/en-US/education/catalog/sample/simple_animation.

Bringing things down to earth

You might have noticed that our tank is not actually sitting on the ground. In fact, we have set our terrain scaling so that the highest point in the terrain is at 30 units, while the tank is positioned at 40 units above the X-Z plane.

Given a (X,Z) coordinate pair, we need to come up with a way to determine what height we should place our tank at, based on the terrain.

Time for action – terrain heights

To place our tank appropriately on the terrain, we first need to calculate, then place our tank there. This is done in the following steps:

  1. Add a helper method to the Terrain class to calculate the height based on a given coordinate as follows:

    #region Helper Methods public float GetHeight(float x, float z) { int xmin = (int)Math.Floor(x); int xmax = xmin + 1; int zmin = (int)Math.Floor(z); int zmax = zmin + 1; if ( (xmin < 0) || (zmin < 0) || (xmax > heights.GetUpperBound(0)) || (zmax > heights.GetUpperBound(1))) { return 0; } Vector3 p1 = new Vector3(xmin, heights[xmin, zmax], zmax); Vector3 p2 = new Vector3(xmax, heights[xmax, zmin], zmin); Vector3 p3; if ((x - xmin) + (z - zmin) <= 1) { p3 = new Vector3(xmin, heights[xmin, zmin], zmin); } else { p3 = new Vector3(xmax, heights[xmax, zmax], zmax); } Plane plane = new Plane(p1, p2, p3); Ray ray = new Ray(new Vector3(x, 0, z), Vector3.Up); float? height = ray.Intersects(plane); return height.HasValue ? height.Value : 0f; } #endregion

  2. In the LoadContent() method of the TankBattlesGame class, modify the statement that adds a tank to the battlefield to utilize the GetHeight() method as follows:

    tanks.Add( new Tank( GraphicsDevice, Content.Load(@"Models\tank"), new Vector3(61, terrain.GetHeight(61,61), 61)));

  3. Execute the game and view the tank, now placed on the terrain as shown in the following screenshot:

What just happened?

You might be tempted to simply grab the nearest (X, Z) coordinate from the heights[] array in the Terrain class and use that as the height for the tank. In fact, in many cases that might work. You could also average the four surrounding points and use that height, which would account for very steep slopes.

The drawbacks with those approaches will not be entirely evident in Tank Battles, as our tanks are stationary. If the tanks were mobile, you would see the elevation of the tank jump between heights jarringly as the tank moved across the terrain because each virtual square of terrain that the tank entered would have only one height.

In the GetHeight() method that we just saw, we take a different approach. Recall that the way our terrain is laid out, it grows along the positive X and Z axes. If we imagine looking down from a positive Y height onto our terrain with an orientation where the X axis grows to the right and the Z axis grows downward, we would have something like the following:

As we discussed when we created our index buffer, our terrain is divided up into squares whose corners are exactly 1 unit apart. Unfortunately, these squares do not help us in determining the exact height of any given point, because each of the four points of the square can theoretically have any height from 0 to 30 in the case of our terrain scale.

Remember though, that each square is divided into two triangles. The triangle is the basic unit of drawing for our 3D graphics. Each triangle is composed of three points, and we know that three points can be used to define a plane. We can use XNA's Plane class to represent the plane defined by an individual triangle on our terrain mesh.

To do so, we just need to know which triangle we want to use to create the plane. In order to determine this, we first get the (X, Z) coordinates (relative to the view in the preceding figure) of the upper-left corner of the square our point is located in. We determine this point by dropping any fractional part of the x and z coordinates and storing the values in xmin and zmin for later use.

We check to make sure that the values we will be looking up in the heights[] array are valid (greater than zero and less than or equal to the highest element in each direction in the array). This could happen if we ask for the height of a position that is outside the bounds of our map's height. Instead of crashing the game, we will simply return a zero. It should not happen in our code, but it is better to account for the possibility than be surprised later.

We define three points, represented as Vector3 values p1, p2, and p3. We can see right away that no matter which of the two triangles we pick, the (xmax, zmin) and (xmin, zmax) points will be included in our plane, so their values are set right away.

To decide which of the final two points to use, we need to determine which side of the central dividing line the point we are looking for lies in. This actually turns out to be fairly simple to do for the squares we are using. In the case of our triangle, if we eliminate the integer portion of our X and Z coordinates (leaving only the fractional part that tells us how far into the square we are), the sum of both of these values will be less than or equal to the size of one grid square (1 in our case) if we are in the upper left triangle. Otherwise our point is in the right triangle.

The code if ((x - xmin) + (z - zmin) <= 1) performs this check, and sets the value of p3 to either (xmin, zmin) or (xmax, zmax) depending on the result.

Once we have our three points, we ask XNA to construct a Plane using them, and then we construct another new type of object we have not yet used – an object of the Ray class. A Ray has a base point, represented by a Vector3, and a direction – also represented by a Vector3.

Think of a Ray as an infinitely long arrow that starts somewhere in our world and heads off in a given direction forever. In the case of the Ray we are using, the starting point is at the zero point on the Y axis, and the coordinates we passed into the method for X and Z. We specify Vector3.Up as the direction the Ray is pointing in. Remember from the FPS camera that Vector3.Up has an actual value of (0, 1, 0), or pointing up along the positive Y axis.

The Ray class has an Intersects() method that returns the distance from the origin point along the Ray where the Ray intersects a given Plane. We must assign the return value of this method to a float? instead of a normal float. You may not be familiar with this notation, but the question mark at the end of the type specifies that the value is nullable—that is, it might contain a value, but it could also just contain a null value. In the case of the Ray.Intersects() method, the method will return null if the object of Ray class does not intersect the object of the Plane class at any point. This should never happen with our terrain height code, but we need to account for the possibility.

When using a nullable float, we need to check to make sure that the variable actually has a value before trying to use it. In this case, we use the HasValue property of the variable. If it does have one, we return it. Otherwise we return a default value of zero.

XNA 4 3D Game Development by Example: Beginner's Guide Create action-packed 3D games with the Microsoft XNA Framework with this book and ebook.
Published: September 2012
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Animating the tank

Now that we have a tank in our game, let's look at how we can animate the bones defined in the model in order to aim the turret and the cannon. We will be adding some temporary code to our TankBattlesGame class in order to see our animations in action.

Time for action – tank animation

In order to animate our tank, we perform the following steps:

  1. In the constructor of the Tank class, add the following two lines to the end of the method:

    baseTurretTransform = model.Bones["turret_geo"].Transform; baseGunTransform = model.Bones["canon_geo"].Transform;

  2. In the Draw() method of the Tank class, add the following before the call to model.CopyAbsoluteBoneTransformsTo():

    model.Bones["turret_geo"].Transform = Matrix.CreateRotationY(TurretRotation) * baseTurretTransform; model.Bones["canon_geo"].Transform = Matrix.CreateRotationX(gunElevation) * baseGunTransform;

  3. In the Update() method of the TankBattlesGame class, add some temporary code to allow us to animate the tank with the keyboard. Place this code after the existing camera movement code, inside the if block that checks for (this.IsActive ) – directly after the current mouse position is stored in previousMouse :

    // Begin temporary code KeyboardState ks = Keyboard.GetState(); if (ks.IsKeyDown(Keys.A)) { tanks[0].TankRotation += 0.05f; } if (ks.IsKeyDown(Keys.Z)) { tanks[0].TankRotation -= 0.05f; } if (ks.IsKeyDown(Keys.S)) { tanks[0].TurretRotation += 0.05f; } if (ks.IsKeyDown(Keys.X)) { tanks[0].TurretRotation -= 0.05f; } if (ks.IsKeyDown(Keys.D)) { tanks[0].GunElevation += 0.05f; } if (ks.IsKeyDown(Keys.C)) { tanks[0].GunElevation -= 0.05f; } //End temporary code

  4. Launch the game, use the mouse to zoom in on the tank, and then use the keyboard to rotate the tank with keys A and Z , the turret with keys S and X, and the cannon with keys D and C. Our tank would look like the one in the following screenshot:

What just happened?

Each of the bones within the tank model we are using has a name assigned to it. In this case, the turret bone is named turret_geo, while the bone for the gun is named canon_geo. In step 1, we store the base transformations for these bones so that we have their baseline positions, which we will use to apply our modifications to later.

When drawing the model, recall that we can produce a matrix that includes all of the transforms we wish to apply by multiplying the component matrices together. This is done in step 2.

Finally, we modify the Update() method of the TankBattlesGame class to allow us to use the keyboard to modify the various rotation values associated with the parts of our tank. We will pull this code back out of our project later, so it is marked with start and end comments to make it easy to recognize.

The combatants

Now that we can render and animate tanks, we will add a second tank to our game and position the two tanks randomly within the game world.

Time for action – positioning tanks

To position tanks within our game, perform the following steps:

  1. Add the following fields to the declarations area of the TankBattlesGame class:

    ContentManager p2Content; Random rand = new Random();

  2. In the Initialze() method of the TankBattlesGame class, add the following lines right before the call to base.Initialize():

    p2Content = new ContentManager(this.Services); p2Content.RootDirectory = "Content";

  3. Add the StartNewRound() method to the TankBattlesGame class as follows:

    public void StartNewRound() { tanks.Clear(); Vector3 p1Position = new Vector3(rand.Next(8, 56), 0, rand.Next(8, 56)); Vector3 p2Position = new Vector3(rand.Next(8, 56), 0, rand.Next(8, 56)); int p1Quadrant = rand.Next(0, 4); switch (p1Quadrant) { case 0: p2Position += new Vector3(64, 0, 64); break; case 1: p1Position += new Vector3(64, 0, 0); p2Position += new Vector3(0, 0, 64); break; case 2: p1Position += new Vector3(0, 0, 64); p2Position += new Vector3(64, 0, 0); break; case 3: p1Position += new Vector3(64, 0, 64); break; } p1Position.Y = terrain.GetHeight(p1Position.X, p1Position.Z); p2Position.Y = terrain.GetHeight(p2Position.X, p2Position.Z); tanks.Add( new Tank( GraphicsDevice, Content.Load(@"Models\tank"), p1Position)); tanks.Add( new Tank( GraphicsDevice, p2Content.Load(@"Models\tank"), p2Position)); }

  4. In the LoadContent() method of the TankBattlesGame class, remove the current code that adds a tank to the Tanks list, and replace it with the following:

    StartNewRound();

  5. Execute the game. Verify that two tanks have been added to the battlefield in opposite quadrants of the map as shown in the following screenshot:

What just happened?

We logically divide our battlefield into four quadrants, numbered 0, 1, 2, and 3. As our battlefield is 128 units on a side, each quadrant is 64 by 64 units. Using this information we generate two positions within quadrant 0 (the upper left quadrant). We pad these positions a bit to keep the tanks from being too close to the outside edges of the map, or to the dividing lines between the quadrants.

Once we have two positions, we randomly select a quadrant for the first tank to occupy. In order to position it in the correct quadrant, we add 64 to the X, Z, both, or neither components of the position depending on the quadrant we selected. We similarly add offsets to the second tank based on the quadrant the first tank is located in, so that the two tanks are in diagonally opposite quadrants.

We calculate the height of each of the final points for the tanks and then generate and add both of them to the tanks list.

You might be wondering though, why we went through the trouble of creating a second instance of the ContentManager class to load the model for the second player's tank. This is because when we load a model (or any other resource) with ContentManager, it checks to see if it has already loaded that content. If it has, you simply get a pointer to the existing content object in memory. In most cases this is not a problem. If we are using the same texture in multiple classes in our game, there really is no need to have multiple copies of the same data in memory.

With our models, though, the transforms that make up the animations will be changing over time. This means that we have to have some way to separate the different instances of our tank models from each other. There are, of course, multiple ways to do this. You could write your own code to draw the model's meshes, taking each set of bone transforms into account and applying them separately.

The approach we have taken here is far simpler. By creating a second instance of ContentManager, it does not know that the first instance has already loaded, so it happily loads a new copy of it from the disk and supplies it for the second tank. Now both tanks can operate independently.

Summary

We have a pair of tanks on our battlefield now, and they are ready to fight! In this article, we covered the addition of 3D model content to our project along with the textures to support them, loading and displaying a 3D model, and animating a 3D model by applying bone transforms.

We have also seen how to precisely determine the elevation of a given point on our terrain using Ray/Plane intersection and how to lay the groundwork for our game flow by randomly positioning enemy tanks on the battlefield.

XNA 4 3D Game Development by Example: Beginner's Guide Create action-packed 3D games with the Microsoft XNA Framework with this book and ebook.
Published: September 2012
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Resources for Article :


Books From Packt


XNA 4.0 Game Development by Example Beginner's Guide
XNA 4.0 Game Development by Example Beginner's Guide

Microsoft XNA 4.0 Game Development Cookbook
Microsoft XNA 4.0 Game Development Cookbook

3D Graphics with XNA Game Studio 4.0
3D Graphics with XNA Game Studio 4.0

XNA 4.0 Game Development by Example Beginner's Guide – Visual Basic Edition
XNA 4.0 Game Development by Example Beginner's Guide – Visual Basic Edition

Windows Phone 7 XNA Cookbook
Windows Phone 7 XNA Cookbook

Unity 3D Game Development by Example Beginner's Guide
Unity 3D Game Development by Example Beginner's Guide

Unity 3.x Game Development by Example Beginner's Guide
Unity 3.x Game Development by Example Beginner's Guide

Unity 3D Game Development by Example Beginner’s Guide LITE
Unity 3D Game Development by Example Beginner’s Guide LITE


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software