Implementing the Ammunition Class


Although tanks firing huge missiles with a lot of explosions everywhere is probably what you're used to seeing in a game (or movie), you can make this implementation simpler. Your bullets can be small bouncing pellets because, after all, I'm sure you don't want to promote violence! What you need now is an implementation.

Continuing with the theme in Tankers, you create a new code file for the "bullet" class. The included CD calls the file bullet.cs, and the initial implementation appears in Listing 15.1.

Listing 15.1. The Bullet Class
 using System; using System.Collections; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; using Microsoft.Samples.DirectX.UtilityToolkit; namespace Tankers {     /// <summary>     /// This class will maintain the bullets used by the game     /// </summary>     public class Bullet : IMoveableObject     {         public const float BulletMass = 8.0f; // The size of the bullets         private const float GravityForce = -9.8f * BulletMass * 5;         // The force of gravity         private const int SlicesStacks = 4; // How complex the bullet's mesh is         private const float LifeTime = 20.0f;         // Amount of time the bullet will stay alive         public const float BulletStrength = 1800.0f;         // Strength of the bullets being fired         private static Mesh bulletMesh = null;         private Vector3 pos; // Current position of the bullet         private Vector3 vel; // Velocity of the bullet         private Vector3 dir; // Current direction of the bullet         private float aliveTime = 0.0f;         // Amount of time the bullet has been alive         private Player owner = null; // The tank that fired this bullet         private Material alphaMaterial; // Current material of the bullet     } } 

As you can see, the Bullet class is also implementing the IMoveableObject interface, which means you need to implement the properties and methods from that interface soon, but first, look at the constants and variables that are currently defined. The "mass" of the bullet is used during creation to define its size. The force of gravity on the bullet is defined (with some added gravity to make the bullets fall faster). The bullet's mesh is defined by a sphere, and the complexity of the vertices in the sphere is defined by the SlicesStacks member. Lower numbers make less complex spheres, but higher numbers make spheres that look more realistic. In this case, you don't want a very realistic sphere, so it is easy to decide to use the lower polygon version.

The lifetime of the bullet is defined in seconds, in this case 20. From the moment the bullet is fired, it is in play for another 20 seconds after that. The strength of the bullet is the initial velocity at which the bullet is fired. This velocity changes over time due to gravity, but the starting velocity should have decent enough power to behave realistically.

You might notice that the mesh variable is listed as static, rather than just another instance variable to be used for the class. The reason is the same reason that the cube meshes in Blockers were static. There isn't any good reason to keep separate meshes for the bullets because each and every bullet is rendered with the same mesh.

Rounding out the variables is the information to track the actual bullet. You need to know the current position of the bullet and its velocity (which, as you learned back in Chapter 10, "A Quick 3D-Math Primer," includes both the direction and magnitude of that velocity). You're storing the direction here separately for ease of use. You also need to ensure that you know how long the bullet has been alive so you can decide whether that bullet is still in play.

The last two variables are used to control certain design features that you might or might not want in your version of the game. You use the owner variable so that during checks it is impossible for the player to hit himself with his own bullet. You use the new material object to slowly fade the bullets (make them render transparently) as they get closer and closer to death (when alive time has exceeded the maximum allowed). These features have no real effect on game play; they are more of a matter of choice.

You should look at the constructor for the Bullet class now, in Listing 15.2.

Listing 15.2. Creating a Bullet
 public Bullet(Device device, Vector3 initialPos, Vector3 initialVel,               Player parent) {     // First create the mesh if it doesn't exist yet     if (bulletMesh == null)     {         bulletMesh = Mesh.Sphere(device, BulletMass, SlicesStacks, SlicesStacks);     }     // Create the material for the bullets     alphaMaterial = new Material();     alphaMaterial.AmbientColor = new ColorValue(1.0f, 1.0f, 1.0f, 1.0f);     alphaMaterial.DiffuseColor = new ColorValue(1.0f, 1.0f, 1.0f, 1.0f -                                                 (aliveTime /LifeTime) );     // Store the position and velocity of the bullet     pos = initialPos;     vel = initialVel;     // Store the parent     owner = parent; } 

Aside from storing the data (which you do at the end of the constructor), you need to accomplish only two things here. First you need to see whether the mesh used to render the bullets is created yet, and if it isn't, well, then create it. The bullets here are represented by low-resolution spheres that can bounce around the levels. The material that is created by default is a fully white material, as was defined by the static material class used by the game engine. I break it down here to show you the formula that is used for marking the items as transparent. (I discuss that in just a few moments during the Update method.)

Construction Cue

You might be asking yourself, "Why did we have a static constructor for the game engine to initialize its static data, but for the Bullet class, we initialize its static data in a normal constructor?" If so, good job in noticing. The reasoning here is that the static constructor does not (and cannot) take parameters. When creating the mesh for the Bullet class, you need the rendering device already created and ready for use, which cannot be guaranteed in the static constructor.


Did you notice that the Bullet class didn't derive from IDisposable? Because the only disposable data it has is the mesh (which is static), it didn't make sense to have each bullet implement this interface. You do still need a way to clean up the data, though, so instead, you should add a static Cleanup method to the class to handle it

 public static void Cleanup() {     if (bulletMesh != null)     {         bulletMesh.Dispose();         bulletMesh = null;     } } 

What about the interface you did implement? You definitely need to get the implementations for the IMoveableObject interface done before you can compile, so see Listing 15.3 for those (except for Update, which I discuss separately).

Listing 15.3. IMoveableObject Implementation of Bullets
 /// <summary> /// The bullet's radius /// </summary> public float Radius {     get { return BulletMass; } } /// <summary> /// The bullet's position /// </summary> public Vector3 Position {     get { return pos; } } /// <summary> /// The direction the bullet is currently traveling /// </summary> public Vector3 Direction {     get { return dir; } } /// <summary> /// Render the bullet /// </summary> public void Draw(GameEngine engine, Camera c) {     // Set the world transform     engine.SetWorldTransform(Matrix.Translation(pos));     // Render the bullet mesh if it's viewable     if (c.ObjectInFrustum(this))     { #if (DEBUG)         engine.numberFaces += bulletMesh.NumberFaces;         engine.numberVerts += bulletMesh.NumberVertices; #endif         engine.RenderingDevice.SetTexture(0, null);         engine.RenderingDevice.Material = alphaMaterial;         bulletMesh.DrawSubset(0);     } } 

The radius of the sphere mesh that was created was also the mass of the bullet from the constant you defined earlier this chapter. Rather than actually calculate the radius of each bullet, you can simply use this constant. For the remaining two read-only properties, you simply return the stored values back to the caller. The drawing method sets the world transform to the current bullet's position, checks whether the bullet is within the camera's view frustum, and draws it. Did you notice in debug mode that the number of vertices and faces are stored for each bullet as well? Also notice that for the first time, you are setting a material to the device, namely the alpha material you defined earlier.

You might be wondering what alpha is. In short, it's the component of a color that determines how opaque or transparent an item is. For example, if you look over at the wall closest to you, unless you're Superman and you have x-ray vision, odds are that you cannot see through this wall. It has an alpha value of 1.0f, or fully opaque. A newly cleaned window could have an alpha value of 0.0f, or completely transparent. There can be varying degrees of alpha in between, such as sheer stockings, which are only partially transparent.

Because of the extra calculations required to render something using alpha transparency, this feature is disabled by default in Direct3D. You want a way to turn it on while you are rendering the bullets but then off again when you finish. Because you should avoid setting render state values more often than necessary for performance reasons, the best solution is to have two static functions, one that turns on the blending and one that turns it off. (See Listing 15.4.)

Listing 15.4. Rendering Objects with Alpha
 /// <summary> /// Sets the device up for rendering the bullets /// </summary> public static void SetBulletRenderStates(GameEngine engine) {     // Turn on alpha blending     engine.RenderingDevice.RenderState.AlphaBlendEnable = true;     engine.RenderingDevice.RenderState.SourceBlend = Blend.SourceAlpha;     engine.RenderingDevice.RenderState.DestinationBlend = Blend.InvSourceAlpha; } /// <summary> /// Resets the device back to defaults /// </summary> public static void EndBulletRenderStates(GameEngine engine) {     // Turn off alpha blending     engine.RenderingDevice.RenderState.AlphaBlendEnable = false; } 

The main component you are setting here is the AlphaBlendEnable render state. You do set the source blend and destination blend options as well, but they don't have any effect unless the alpha state is turned on (which is why they are not turned off when the alpha is). Before you can render any bullets, you want to call the SetBulletRenderStates method, and when you're done rendering the bullets, you then call the EndBulletRenderStates method.

The Update method is the largest method in this class because it has the most to do. See Listing 15.5 for the implementation of this method.

Listing 15.5. Updating a Bullet
 /// <summary> /// Update the bullet based on the time since the last frame /// </summary> public void Update(float elapsedTime, Level currentLevel) {     // Update the bullet's lifetime     aliveTime += elapsedTime;     // And alpha mode     alphaMaterial.DiffuseColor = new ColorValue(1.0f, 1.0f, 1.0f, 1.0f -                                                (aliveTime /LifeTime) );     Vector3 beginPos = pos;     // Velocity needs to scale by gravity first, gravity only affects the y axis     vel.Y += GravityForce * elapsedTime;     // Update the bullet's position     pos += Vector3.Scale(vel, elapsedTime);     // Now calculate the direction the bullet is currently traveling     dir = Vector3.Normalize(beginPos - pos);     // We may need to reflect our direction if there's a collision,     // store the points     Plane reflectionPlane;     if (currentLevel.HitTest(this, out reflectionPlane))     {         Matrix reflectionMatrix = Matrix.Identity;         reflectionMatrix.Reflect(reflectionPlane);         // Get the current velocity         float velocity = vel.Length();         Vector3 newPos = Vector3.TransformCoordinate(beginPos, reflectionMatrix);         vel = Vector3.Normalize(pos - newPos);         vel.Scale(velocity);         pos = newPos;     } } 

First, you update the amount of time the bullet has been alive so you know when they are no longer alive. Then, you calculate the alpha color. As discussed earlier, a value of 1.0f is fully opaque, and 0.0f is fully transparent. In this formula, when the alive time is 0, you see that the alpha value is 1.0f (fully opaque). When the alive time gets to the halfway point of the maximum life time, the value is 0.5f, partially transparent.

Next, you store the initial position of the bullet (because it will be modified in a moment) and update the velocity to be affected by gravity. Notice that only the y axis is modified for the gravity because gravity only pulls downward (at least on Earth). Next, you modify the position by adding the velocity to the current position (with the velocity scaled down by the elapsed time, of course). The direction is calculated by taking the difference vector from the start position and the ending position and normalizing it.

Here's where you actually use the reflection plane that you calculated in the hit testing on the level. If the bullet hits an obstacle, you want it to bounce off that obstacle in a realistic fashion, so what you do is grab the plane that the bullet has hit and build a "reflection matrix" based on that plane. The reflection matrix is used to transform the starting coordinate to a new, reflected coordinate. You then calculate the updated velocity by subtracting the reflected position from the position that was updated earlier in this method and then scaling it back to its original magnitude.

One more thing you need to add to the bullet class is another hit detection method, this time for when the bullet is going to hit an opposing tank. This task is slightly less complicated than the level detection because you only check whether the bounding spheres of the objects collide. Add the code from Listing 15.6 to your class.

Listing 15.6. Bullets Collision Detection
 /// <summary> /// Does a hit test on the object /// </summary> public bool HitTest(IMoveableObject obj) {     // Don't do hit testing if you fired the bullet     if (obj == owner)     {         return false;     }     // The hit testing for this will be simpler, simply compare the distance     Vector3 diff = this.Position - obj.Position;     return diff.Length() < (this.Radius + obj.Radius); } 

As you see, you check the owner first. If the owner is the object (tank) that fired the bullet, simply skip the test and return false. (They didn't collide.) This allows a player to fire a bullet and not hit herself. If this tank isn't the owner, though, find the difference in the two objects' positions. If the length of this vector is greater than the combined radius of the two bounding spheres, the objects must have hit.



Beginning 3D Game Programming
Beginning 3D Game Programming
ISBN: 0672326612
EAN: 2147483647
Year: 2003
Pages: 191
Authors: Tom Miller

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