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

15 min read

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(@"Modelstank"), 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(@"Modelstank"), 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.


Please enter your comment!
Please enter your name here