Write-up is below.
Code is here.








Introduction


In this second project of SSE 643, Spring 2010, I will use the XNA documentation's tutorial on how to build an Asteroids® look-alike using Microsoft Visual C# and the XNA Framework to demonstrate some key concepts of game programming. This game will incorporate 3D graphics, user input, and sound effects. The tutorial contains all the models, textures, and audio files needed for the game. This document assumes the reader is familiar with the concepts covered in project 1, which can be found at the course's ftp site.

The game is pretty simple conceptually. The user controls a spaceship, which can move forward, turn, shoot its cannon, and warp to the center of the screen. THe player can additionally end the program by pressing the Back button of the controller. Asteroids float through space, and they will destroy the player's ship if they hit it. The player should move to avoid the asteroids, and shoot them with the cannon when possible. Points are accumulated by blowing up asteroids, and lost by dying, shooting, and warping. A counter on the top of the screen keeps track of the player's score. A sound will play continously while the player is engaging the engine to move the ship forward, and additional sounds play for shooting the cannon, destroying an asteroid, blowing up the ship, and warping to the starting location.

Graphics


Although the game is played just like a top-down 2D game, the graphics are 3D, with the exception of the background image. The graphics used in the game are the background image, the ship, the asteroids, and the bullets.

First, let's cover the background image. Even though we talked about 2D graphics in the first project, we'll go over the basics again here. Before you can use any resource in your game, you have to add it to the project via the Content Pipeline. For this project, we created a new folder within the project's Content folder called Textures. The background image has to be added to this folder. First make sure the file is actually located in this folder, then, in the Solution Explorer, right-click Content, click Add -> Existing Item, and browse to the file, select it, and click Okay. This is the method that will be used in the future to add content to the project. Now that the project has the image in its Content, you have to add it in code, as well. You will need a Texture2D variable to store it in, and then you will need to load it in the LoadContent method by calling the Content.Load<Texture2D> method. Assuming the asset name of the image is "B1_stars", the code would look like this:
Texture2D stars;
and within the LoadContent method,
stars = Content.Load<Texture2D>("Textures/B1_stars");
This is the basic technique used to add any resource to the project, excepting that Texture2D is replaced by the appropriate class, for instance Model or SoundEffect.
The last step, now that the image has been added to the project, is to actually draw the image. Since the image is 2D, it does not exist in the 3D space of the game, and will be drawn at a specified pixel location on the screen instead of at a certain point in 3D space. The code to draw the image will go in the Draw method.
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate,
                SaveStateMode.None);
spriteBatch.Draw(stars, new Rectangle(0, 0, 800, 600), Color.White);
spriteBatch.End();
It is important to draw this image first, meaning this code segment comes before those used to draw the other objects, because it will be drawn over anything that has already been drawn.

Now that the background image is taken care of, the next thing to draw is the ship. The ship, unlike the background image, is a 3D graphic, and is composed of a model and a texture. The model contains all the geometric information about the object, in other words its shape, while the texture is like a skin that is placed around the model to make it look like something (a space ship in this case). The model needs to be added to the Content Pipeline using the method described above, but the texture can just be moved into the Textures folder without being added via the Content Pipeline. Also, the model and texture are automatically tied together, so you don't have to do anything at all in the code with the texture. Drawing the model will automatically use the right texture (assuming the model and texture files are correct). Since there are lots of models that we will eventually need to draw for this program, we will add a helper method to draw models.
public static void DrawModel(Model model, Matrix modelTransform,
            Matrix[] absoluteBoneTransforms)
{
   //Draw the model, a model can have multiple meshes, so loop
   foreach (ModelMesh mesh in model.Meshes)
   {
      //This is where the mesh orientation is set
      foreach (BasicEffect effect in mesh.Effects)
      {
         effect.World =
            absoluteBoneTransforms[mesh.ParentBone.Index] * modelTransform;
      }
      //Draw the mesh, will use the effects set above.
      mesh.Draw();
   }
}
Now in the Draw method we can call this helper method on whatever model we want to draw.
Matrix shipTransformMatrix = ship.RotationMatrix
         * Matrix.CreateTranslation(ship.Position);
if (ship.isActive)
{
   DrawModel(ship.Model, shipTransformMatrix, ship.Transforms);
}
That's all you need to draw the ship. The rest is just keeping track of the location of the ship and its orientation. The ship.Position takes care of the position, and the ship.RotationMatrix takes care of the orientation.

Drawing the asteroids and bullets is done the same way as drawing the ship. The only difference is that, since there may be more than one of these objects, you keep a list of them and loop through the list each frame.
for (int i = 0; i < GameConstants.NumAsteroids; i++)
{
    Matrix asteroidTransform =
        Matrix.CreateTranslation(asteroidList[i].position);
    if (asteroidList[i].isActive)
    {
        DrawModel(asteroidModel, asteroidTransform, asteroidTransforms);
    }
}
 
for (int i = 0; i < GameConstants.NumBullets; i++)
{
    if (bulletList[i].isActive)
    {
        Matrix bulletTransform =
          Matrix.CreateTranslation(bulletList[i].position);
        DrawModel(bulletModel, bulletTransform, bulletTransforms);
    }
}
The bullets and asteroids do not have the rotation variable, and so they are always drawn in the same orientation. Since we already have this functionality for the ship, adding it for asteroids and bullets would be simple, however we will not do that here in the interest of brevity.

Now all of the graphics have been drawn into the world. One thing is still missing, however, and that is the camera. The camera represents the point of view from which everything is seen. Just like two people looking at the same scene from different locations will see different images, the same scene can be viewed from different places and angles in a 3D game. For this simple game, the camera is fixed in place and does not move or rotate, but in many games, where the world is larger and the player-controlled character can move around a lot, the camera might need to move. Since our camera is stationary, we can set it up when the game starts to run and not worry about it again.
projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.ToRadians(45.0f),
      GraphicsDevice.DisplayMode.AspectRatio,
      GameConstants.CameraHeight - 1000.0f,
      GameConstants.CameraHeight + 1000.0f);
viewMatrix = Matrix.CreateLookAt(cameraPosition,
      Vector3.Zero, Vector3.Up);
This code is located in the Initialize method and sets up the camera's field of view. Next, this information has to be added to all the models' Transforms arrays.
private Matrix[] SetupEffectDefaults(Model myModel)
{
    Matrix[] absoluteTransforms = new Matrix[myModel.Bones.Count];
    myModel.CopyAbsoluteBoneTransformsTo(absoluteTransforms);
 
    foreach (ModelMesh mesh in myModel.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.EnableDefaultLighting();
            effect.Projection = projectionMatrix;
            effect.View = viewMatrix;
        }
    }
    return absoluteTransforms;
}
Here we see every effect of every mesh in every model is given the camera information. If the camera moved, we would have to redo this for every frame to take into account the new projectionMatrix and viewMatrix. For our game, this SetupEffectDefaults method is only called once for each model in the LoadContent method.

That is the basics of drawing 3D objects. More complicated behaviors can be added without much more effort, as long as our models don't have any moving parts, like a model of a person that can move its limbs. These movements are called animations and we will not go into that for this project.

User Input


The XNA framework is set up to use the Xbox360 controller, and it is very very easy to use. You can also use the keyboard. One failing of the XNA framework is that it does not support the older DirectInput devices natively. For writing xbox360 games, this is not a problem, but for PC games, the player may want to use his own favored controller. The best way to handle this problem is to include a settings page in your game where the player can map the commands needed by the game to whatever buttons or keys on whatever input device he wants to use. Since this game is so simple, and since I only had one controller to use anyway, adding this functionality would have been as much work as the rest of the game combined, so I elected to just hardcode button configurations.

To get around the problem of not having DirectInput support, I used an open source library specifically designed for this problem. It can be found here.

The way the game takes input is pretty simple, but not as simple as it is in the tutorial, since I allow for using a DirectInput input device. The basic idea is that the game uses a GamePadState object to store the state of the input device. This class is specifically designed to work with the Xbox360 controller. First the game checks for an attached Xbox360 controller. I didn't have one, so I can only assume that the code will work with it attached. If it finds one, it initializes the GamePadState object with the state from the Sbox360 controller. This part is easy. It fails to find one, however, so it then checks for an attached DirectInput device. This part is a little clunky, but that's unavoidable as far as I can tell. What we do is check all the buttons and sticks of the DirectInput device and remap them to their corresponding Xbox360 controls, then construct the GamePadState object based on that. From that point on in the code, since either way the GamePadState object was initialized with the input from whichever device, we don't have to worry at all about what kind of input device is connected. Here is the code for the first part of the UpdateInput method.
// Get the game pad state.
GamePadState currentState;
 
if (GamePad.GetState(PlayerIndex.One).IsConnected)
{
    currentState = GamePad.GetState(PlayerIndex.One);
}
 
else if (DirectInputGamepad.Gamepads.Count >= 1)
{
    List<Buttons> buttonList = new List<Buttons>();
 
    if (DirectInputGamepad.Gamepads[0].Buttons.List[0] == ButtonState.Pressed) buttonList.Add(Buttons.Y);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[1] == ButtonState.Pressed) buttonList.Add(Buttons.B);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[2] == ButtonState.Pressed) buttonList.Add(Buttons.A);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[3] == ButtonState.Pressed) buttonList.Add(Buttons.X);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[6] == ButtonState.Pressed) buttonList.Add(Buttons.LeftShoulder);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[7] == ButtonState.Pressed) buttonList.Add(Buttons.RightShoulder);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[8] == ButtonState.Pressed) buttonList.Add(Buttons.Back);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[9] == ButtonState.Pressed) buttonList.Add(Buttons.Start);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[10] == ButtonState.Pressed) buttonList.Add(Buttons.LeftStick);
    if (DirectInputGamepad.Gamepads[0].Buttons.List[11] == ButtonState.Pressed) buttonList.Add(Buttons.RightStick);
    if (DirectInputGamepad.Gamepads[0].DPad.Down == ButtonState.Pressed)        buttonList.Add(Buttons.DPadDown);
    if (DirectInputGamepad.Gamepads[0].DPad.Up == ButtonState.Pressed)          buttonList.Add(Buttons.DPadUp);
    if (DirectInputGamepad.Gamepads[0].DPad.Left == ButtonState.Pressed)        buttonList.Add(Buttons.DPadLeft);
    if (DirectInputGamepad.Gamepads[0].DPad.Right == ButtonState.Pressed)       buttonList.Add(Buttons.DPadRight);
 
    Buttons[] buttons = new Buttons[buttonList.Count];
    for (int i = 0; i < buttonList.Count; i++)
    {
        buttons[i] = buttonList[i];
    }
 
    currentState = new GamePadState(DirectInputGamepad.Gamepads[0].ThumbSticks.Left,
                                                 DirectInputGamepad.Gamepads[0].ThumbSticks.Right,
                                                 (DirectInputGamepad.Gamepads[0].Buttons.List[4] == ButtonState.Pressed) ? 1.0f : 0.0f,
                                                 (DirectInputGamepad.Gamepads[0].Buttons.List[5] == ButtonState.Pressed) ? 1.0f : 0.0f,
                                                 buttons);
}
else currentState = new GamePadState();
Compare the length of the Xbox360 controller case with that of the DirectInput device case. Since XNA is set up to work smoothly with Xbox360 input, it only requires one line of code, whereas the DirectInput case requires a whole bunch of code. For reference, I used a Sony PlayStation DualShock2 controller, connected through a USB adapter. To use a DirectInput device, you have to know which button index corresponds to which button. You can find this out by going into the Control Panel -> Game Controllers, finding your gamepad, and clicking the Test tab. pressing buttons on the pad at this point should light up the indicators on the screen. For instance, if you press the O button, the 2 lights up, which means you should map the second element of Buttons.List to the B button, since the B button on the Xbox360 controller is in the same location as the O button in the PlayStation controller. One last thing to take note of is that a DirectInput gamepad may or may not have all the same functionality as the Xbox360 controller. In our case, the the triggers of the Xbox360 are analog inputs, but the PlayStation controller's triggers are digital. Also the big button in the center of the Xbox controller has no analog on the PlayStation controller. You have to take these differences into account when setting up your input. For this game, the lack of analog triggers is relevant, since the game is supposed to use the right trigger as the throttle. When using the PlayStation controller, you just have to go without that feature and use full on or full off.

Once you have the GamePadState object, it is smooth sailing.
if (currentState.Buttons.Back == ButtonState.Pressed)
   this.Exit();
Note that even though the code says currentState.Buttons.Back, when using the PlayStation controller this is controlled by the Select button, since their locations correspond to each other. currentState, our GamePadState object has the state of all the buttons stored as ButtonState objects, and the location of the thumbsticks and triggers. Triggers are a float value between 0.0f and 1.0f, and each thumbstick has a value for x and for y ranging from -1.0f to +1.0f. You can use them for whatever you need.

Sound


I talked about sound effects in project 1, so I won't go into much detail on them here. Sound-related resources are added to the Content Pipeline the same way other resources are. For this game I created a subfolder called Audio in the Content folder to store the audio files. The XNA Framework can use XACT to set up sound resources, which is what I did in the first project. For this project, I will skip on XACT and use the SoundEffect class instead. Unfortunately, XNA can only use wave files. Wav files are very large. For this reason, my first project ended up being like 300 megabytes, and the majority of it was the background music, since I had to store several minutes of music in wave format. This is a major shortcoming, and I would like to explore using other formats later. Specifically, I would like to be able to play MIDI music in my games, since it takes up so little space. If not, mp3 would be a nice compromise.

Using sound resources is very similar to using graphical resources. There are three steps: adding them to the project via the Content Pipeline, initializing them in the code, and then playing them when you want to.
//Audio Components
SoundEffect soundEngine;
SoundEffectInstance soundEngineInstance;
SoundEffect soundHyperspaceActivation;
SoundEffect soundExplosion2;
SoundEffect soundExplosion3;
SoundEffect soundWeaponsFire;
These are the variables we will need to setup sound in our game. They are engine noise, warp sound effect, explosions for blowing up ships and asteroids, and the sound effect for firing the cannon. We use a SoundEffectInstance also for the engine noises, since they play continuously while the engines are on (right trigger held down) and we need to check on their status. Next in the LoadContent method, we initialize these variables.
soundEngine = Content.Load<SoundEffect>("Audio/Waves/engine_2");
soundEngineInstance = soundEngine.CreateInstance();
soundHyperspaceActivation =
    Content.Load<SoundEffect>("Audio/Waves/hyperspace_activate");
soundExplosion2 =
    Content.Load<SoundEffect>("Audio/Waves/explosion2");
soundExplosion3 =
    Content.Load<SoundEffect>("Audio/Waves/explosion3");
soundWeaponsFire =
    Content.Load<SoundEffect>("Audio/Waves/tx0_fire1");
Now the only thing left to do is play them at the appropriate time.
//Play engine sound only when the engine is on.
if (currentState.Triggers.Right > 0)
{
 
    if (soundEngineInstance.State == SoundState.Stopped)
    {
        soundEngineInstance.Volume = 0.75f;
        soundEngineInstance.IsLooped = true;
        soundEngineInstance.Play();
    }
    else
        soundEngineInstance.Resume();
}
else if (currentState.Triggers.Right == 0)
{
    if (soundEngineInstance.State == SoundState.Playing)
        soundEngineInstance.Pause();
}
The code to use the engine noise is the most complicated, since we want the engine noise to repeat constantly as long as the right trigger is pressed. This block keeps it playing if the trigger is pressed and stops it if the trigger is not pressed. We use the Play method of the SoundEffectInstance object here, but for the more simple behavior of just playing a sound once, you can just use the Play method on a SoundEffect object. That is what we do for all the other sounds, for instance:
soundHyperspaceActivation.Play();
This is really simple, but should give you enough flexibility to do almost anything you would want to. The main reasons to use XACT instead are to help in organization and encapsulation of work (separating the work of the programmers and the sound effect people), so it would only be useful for much larger scale projects.

End


That summarizes most of the important parts of the prgramming for this game. You can find complete code here.
Please enjoy this screenshot of the game in progress.
asteroids_1.JPG
You can see the ship over a starry background amongst a few asteroids. The score of 191 indicates that some of the asteroids have already been blown up.