Microsoft XNA Game Studio 3.0 Unleashed
Creating a 3D Game
Creating the Game LogicAfter we successfully compile and run our game, we can start working on our game logic. Fortunately, we have the framework in place where we can easily put in our game code. We will first work on our game play by modifying the playing state code file (/GameStates/PlayingState.cs).We need to remove SpriteFont from our member fields and add in the following fields: private MissileManager missileManager; private EnemyManager enemyManager; private List<Level> Levels; private int currentLevel; private float totalCreatedEnemies; public int TotalCollisions; private BoundingSphere playerSphere;We are going to manage our enemies and our missiles, so we have set up some variables for those. We also created a list to store our levels. Before we dig into our playing state any further, we can build out these classes. The code for our Level class, which can be added as Level.cs to our main game folder, is as follows:
public class Level
{
public int Missiles;
public int Enemies;
public int Time;
public float EnemySpeed;
public Level(int missiles, int enemies, int time, float enemySpeed)
{
Missiles = missiles;
Enemies = enemies;
Time = time + 1;
EnemySpeed = enemySpeed;
}
}
The class is pretty straightforward—we are providing a way to store the number of missiles that we are allowing on the screen at one time. One of the challenges of the game is to not expend too many bullets at once. The next challenge is how many enemies this level needs to generate before the level is over. We also have a timer that we will be using to award bonus points if all the enemies are killed before it runs out. Finally, we store the speed at which the enemy is moving in this level.
The enemy and missile managers do as their names imply and manage the missiles and the enemies, respectively. These objects share a lot of the same properties. Therefore, we are going to introduce a SceneObject and then have a PhysicalObject that inherits from our SceneObject. Although everything in this game will really be a physical object, we could have a trigger that, if reached, kicks off some animation; this would need to be a scene object, not a physical object. You’ll see that this makes more sense as we look at the code, starting with the SceneObject:
public abstract class SceneObject
{
public Matrix World;
public BoundingSphere BoundingSphere;
}
This is a very simplistic abstract class that stores our world matrix for the object as well as a bounding sphere. This allows us to place the object in the world and assign it a sphere so that if something collides with it, we could kick off an event such as spawning enemies, opening doors, doing a cut scene, or anything else we wanted. In our game, we will not be using an object that is purely a scene object, but it is good to have it here for future projects. The Microsoft.Xna.Framework namespace will need to be included because this class uses BoundingSphere. Our physical object inherits from this and has more properties for an actual drawable object. Here is the code for the PhysicalObject class:
public abstract class PhysicalObject : SceneObject
{
public Vector3 Position;
public Vector3 Velocity;
public Vector3 Acceleration;
public float Mass;
public float Scale = 1.0f;
public float Radius = 1.0f;
public Color Color;
public Matrix Rotation = Matrix.Identity;
public virtual void Move(float elapsed)
{
//adjust velocity with our acceleration
Velocity += Acceleration;
//adjust position with our velocity
Position += elapsed * Velocity;
World = Matrix.CreateScale(Scale) * Rotation * Matrix.CreateTranslation(Position);
BoundingSphere = new BoundingSphere(Position, Radius);
}
}
We inherit from SceneObject so we get the bounding sphere and the world matrix, but we also add position, velocity, acceleration, mass, scale, radius, color, and rotation. This allows us to assign physical properties to our objects. We have a Move method that applies the physical forces we talked about in Chapter 16, “Physics Basics.” This method adds the acceleration to our velocity and adds our velocity (taking the time delta into account) to our position. It then uses the scale, rotation, and position to update the world matrix for the object. Finally, it recalculates the bounding sphere based on the position and radius of the object. We need to add the following using statements:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics;Now we are ready to look at our EnemyManager and MissileManager, which manage objects that inherit from our PhysicalObject. We will start with our MissileManager and our Missile class in particular:
class Missile : PhysicalObject
{
public DateTime StartTime;
public bool IsActive;
}
The missile object inherits all the properties from our physical object class and also includes the start time and a flag that states whether the missile is active. We will be treating this much like we did our particle system. We can jump right into our MissileManager to see how to manage these objects. Our MissileManager object will inherit from the DrawableGameComponent. As usual, we can start with our member fields and constructor:
public const int MISSILE_LIFE = 5; //5 seconds
private Model missile;
private Effect effect;
public Matrix View;
public Matrix Projection;
private Missile[] missiles;
private Texture2D missileTexture;
private int lastMissileIndex;
private float timer = 0;
public MissileManager(Game game) : base(game) { }
We have a constant that determines how long our missile should stay on the screen if it does not hit any enemies. One of the challenges we are presenting gamers is that only a certain number of missiles can be on the screen at any given time for each level. The more accurate the players are, the more frequently they can fire a missile. If they miss, the missile is active for 5 seconds, and if they have fired their allotment, players need to wait until the 5 seconds are up before they can fire another missile.
We also have fields that store the missile model and the effect we will be using for that model. We set our View and Projection properties (which our effect needs) inside of the game, so we set those fields with the public modifier. We have an array where we store and manage our missile objects and the texture we apply to those objects. Much like we did for our particle system, we are going to keep track of the last index in our array to which we added a missile so we know where to start when a new request is created to add a missile to our list. Our constructor is empty and only passes the game object to the base DrawableGameComponent class. Next, we can look at the Load method that our game will call every time we start a new level. Each time a new level is started, we reset our array of missiles to handle the maximum amount the level allows the player to fire at one time. The code for the Load method is as follows:
public void Load(int capacity)
{
missiles = new Missile[capacity];
lastMissileIndex = 0;
for (int i = 0; i < missiles.Length; i++)
missiles[i] = new Missile();
}
At the beginning of the level, we reallocate our array and reset our last missile index. Finally, we loop through all the missiles and store an instance of each. Our LoadContent and UnloadContent methods are next:
protected override void LoadContent()
{
missileTexture = Game.Content.Load<Texture2D>(@”Textures\FireGrade”);
missile = Game.Content.Load<Model>(@”Models\sphere”);
effect = Game.Content.Load<Effect>(@”Effects\VertexDisplacement”);
effect.Parameters[“ColorMap”].SetValue(missileTexture);
effect.Parameters[“AmbientColor”].SetValue(0.8f);
base.LoadContent();
}
protected override void UnloadContent()
{
missiles = null;
base.UnloadContent();
}
In our LoadContent method, we retrieve the texture from our TextureManager and the effect from our EffectManager. We then set the parameters on the effect. Our UnloadContent method sets our missiles array to null.
We need to add the FireGrade texture to our Content project. We also need to add the sphere model to our project. Both of these assets can be found on the accompanying CD in this chapter’s code folder. When the player fires a missile, we need to tell our MissileManager to add a missile to its array. Therefore, we provide a public method called AddMissile:
public bool AddMissile(Vector3 position, Vector3 direction, DateTime startTime)
{
int index = lastMissileIndex;
for (int i = 0; i < missiles.Length; i++)
{
if (!missiles[index].IsActive)
break;
else
{
index++;
if (index >= missiles.Length)
index = 0;
}
if (index == lastMissileIndex)
return (false);
}
//at this point index is the one we want ...
InitializeMissile(index, position, direction, startTime);
missiles[index].IsActive = true;
lastMissileIndex = index;
return (true);
}
The AddMissile method loops through the entire array, finding the first empty slot it can initialize a missile into. If there are no free slots, the user has fired all the missiles allowed and the method returns false. The details of the InitializeMissile method are as follows:
private void InitializeMissile(int index, Vector3 position, Vector3 direction, DateTime startTime)
{
missiles[index] = new Missile();
missiles[index].Position = position;
missiles[index].Acceleration = direction * 10f;
missiles[index].Velocity = Vector3.Zero;
missiles[index].StartTime = startTime;
}
The InitializeMissile method sets the values passed in from AddMissile, which gets the values from the game. Our MissileManager class also has a collision detection method. CheckCollision is used to determine if any of the missiles have collided with the bounding sphere passed in:
public bool CheckCollision(BoundingSphere check)
{
for (int i = 0; i < missiles.Length; i++)
{
if ((missiles[i].IsActive) && (missiles[i].BoundingSphere.Intersects(check)))
{
RemoveMissile(i);
return (true);
}
}
return (false);
}
If a collision is detected, we remove the missile from the list by setting its active flag to false. This is done in the RemoveMissile method:
private void RemoveMissile(int index)
{
missiles[index].IsActive = false;
}
This leaves just the Update and Draw methods. The Update method simply loops through all the active missiles and checks to see how long they have lived. If they have lived too long, they are removed from the array. If they are still alive, they are moved by calling the Move method on the Missile object, which ultimately calls the Move method on the PhysicalObject class:
public override void Update(GameTime gameTime)
{
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
timer += 0.655f;
for (int mi = 0; mi < missiles.Length; mi++)
{
//we do not want to update any inactive missiles
if (!missiles[mi].IsActive)
continue;
if ((DateTime.Now - missiles[mi].StartTime) > TimeSpan.FromSeconds(MissileManager.MISSILE_LIFE))
RemoveMissile(mi);
else
missiles[mi].Move(elapsed);
}
base.Update(gameTime);
}
The Update method also adds a fixed amount to a timer, which is used for the effect of the missile. The Draw method is shown here:
public override void Draw(GameTime gameTime)
{
GraphicsDevice.RenderState.DepthBufferEnable = true;
GraphicsDevice.RenderState.AlphaBlendEnable = true;
effect.Parameters[“Timer”].SetValue(timer);
effect.Parameters[“View”].SetValue(View);
effect.Parameters[“Projection”].SetValue(Projection);
for (int mi = 0; mi < missiles.Length; mi++)
{
if (!missiles[mi].IsActive)
continue;
effect.Parameters[“World”].SetValue(missiles[mi].World);
Matrix[] transforms = new Matrix[missile.Bones.Count];
missile.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in missile.Meshes)
{
for (int i = 0; i < mesh.MeshParts.Count; i++)
{
// Set this MeshParts effect to our RedWire effect
mesh.MeshParts[i].Effect = effect;
}
mesh.Draw();
missiles[mi].BoundingSphere = mesh.BoundingSphere;
missiles[mi].BoundingSphere.Center += missiles[mi].World.Translation;
}
}
base.Draw(gameTime);
}
The Draw method makes sure the render state is in the right state before drawing anything. It sets the appropriate effect parameters; then it loops through all the missiles and applies the effect to them. It also recalculates the bounding sphere of the missile.
Before we get back to the PlayingState, we need to add the enemy manager. The concept is the same as our missile manager: Only a certain number of enemies can be displayed at one time, determined by each level. The enemy object has tracking AI built in to it as well. We start by creating the Enemy class, which inherits from PhysicalObject:
public class Enemy : PhysicalObject
{
Random random = new Random(DateTime.Now.Millisecond);
public Vector3 Target = Vector3.Zero;
public Texture2D Texture;
private float moveSpeed;
private Vector3 Up, Forward;
public Enemy(Texture2D texture, float moveSpeed)
{
Texture = texture;
this.moveSpeed = moveSpeed;
Scale = 0.01f;
Radius = 5f;
Position = XELibrary.Utility.GetRandomVector3(
new Vector3(-300, -100, -100), new Vector3(300, 100, -100));
Up = Vector3.Up;
Forward = Vector3.Forward;
}
public override void Move(float elapsed)
{
Vector3 tv = Target - Position;
tv.Normalize();
Velocity = tv * moveSpeed;
Forward = tv;
Vector3 Right = Vector3.Normalize(Vector3.Cross(Forward, Vector3.Up));
Up = Vector3.Normalize(Vector3.Cross(Right, Forward));
Rotation = Matrix.Identity;
Rotation.Forward = Forward;
Rotation.Up = Up;
Rotation.Right = Right;
base.Move(elapsed);
}
}
moveSpeed is the speed at which the enemies are moving toward the player. The Up and Forward vectors are stored so we can calculate the rotation. The position in which an enemy is generated is random. The Move method handles the AI for tracking the player as well as setting the rotation so the enemy is always facing the player. We define two vectors: Up and Forward. Then we calculate our Right vector by normalizing the cross-product of the Forward and Up vectors. Then we recalculate the Up vector again to make sure the vectors are truly perpendicular. Once we have all three vectors perfectly perpendicular to each other, we set our rotation matrix’s vectors so we can use it inside our base class. After setting all the properties for our enemy, the base class is called to finish the move process.
Our EnemyManager class inherits from the DrawableGameComponent class, just like our MissileManager class. The member fields we need for this class are as follows: public const int MAX_ENEMIES = 10; private Texture2D[] enemyTextures; private Model enemy; private Effect effect; private Random rand = new Random(); public Matrix View; public Matrix Projection; public List<Enemy> Enemies = new List<Enemy>(MAX_ENEMIES);In our constructor, we need to store a reference to our game object:
public EnemyManager(Game game) : base(game) { }
Microsoft.Xna.Framework, Microsoft.Xna.Framework.Graphics, and System.Collections.Generic will need to be added to the EnemyManager.cs file. The LoadContent method of EnemyManager is as follows:
protected override void LoadContent()
{
enemyTextures = new Texture2D[3];
enemyTextures[0] = Game.Content.Load<Texture2D>(@”Textures\wedge_p2_diff_v1”);
enemyTextures[1] = Game.Content.Load<Texture2D>(@”Textures\wedge_p2_diff_v2”);
enemyTextures[2] = Game.Content.Load<Texture2D>(@”Textures\wedge_p2_diff_v3”);
enemy = Game.Content.Load<Model>(@”Models\p2_wedge”);
effect = Game.Content.Load<Effect>(
@”Effects\AmbientTexture”);
effect.Parameters[“AmbientColor”].SetValue(.8f);
base.LoadContent();
}
We are loading three different textures that the model we are loading can use. This model and these textures are taken from the Spacewar starter kit. They can also be found on the accompanying CD in this chapter’s code folder. The assets should be added to the appropriate folders in which the code is expecting to find them.
The UnloadContent method, which gets called by the XNA Framework whenever the graphics device is reset or the game exits, simply clears out the list of enemies:
protected override void UnloadContent()
{
Enemies.Clear();
Enemies = null;
base.UnloadContent();
}
The next method in our enemy manager class is the Draw method:
public override void Draw(GameTime gameTime)
{
GraphicsDevice.RenderState.AlphaTestEnable = false;
GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.RenderState.PointSpriteEnable = false;
GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
GraphicsDevice.RenderState.DepthBufferEnable = true;
effect.Parameters[“View”].SetValue(View);
effect.Parameters[“Projection”].SetValue(Projection);
for (int ei = 0; ei < Enemies.Count; ei++)
{
effect.Parameters[“World”].SetValue(Enemies[ei].World);
effect.Parameters[“ColorMap”].SetValue(Enemies[ei].Texture);
Matrix[] transforms = new Matrix[enemy.Bones.Count];
enemy.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in enemy.Meshes)
{
foreach (ModelMeshPart mp in mesh.MeshParts)
{
mp.Effect = effect;
}
mesh.Draw();
}
}
base.Draw(gameTime);
}
First, we set our render state properties to the values we require. Then we set our effect’s view and projection properties. Finally, we loop through all our enemies and draw each one with the appropriate texture and world matrix being passed into the effect.
The last method we need to add in our EnemyManager class is the AddEnemy method:
public void AddEnemy(float moveSpeed)
{
Enemies.Add(new Enemy(enemyTextures[rand.Next(0, 3)], moveSpeed));
}
This is a public method the playing state code will use to tell the manager to add another enemy to the list. The enemy will be assigned one of the three textures we have specified, along with its movement speed.
Reproduced from the book Microsoft XNA Game Studio 3.0 Unleashed. Copyright? 2009. Reproduced by permission of Pearson Education, Inc., 800 East 96th Street, Indianapolis, IN 46240. Written permission from Pearson Education, Inc. is required for all other uses
|
|