Export (0) Print
Expand All

Tutorial 4: Make a Game in 60 Minutes

This tutorial helps you learn about the process of game construction, while guiding you through writing a relatively complete game.
Bb975644.note(en-us,XNAGameStudio.31).gifNote
This tutorial builds on code you have written during the previous tutorial: Tutorial 3: Making Sounds with XNA Game Studio. Follow the steps in the previous tutorial before starting this tutorial.

The Complete Sample

The code in this tutorial illustrates the technique described in the text. A complete code sample for this tutorial is available for you to download, including full source code and any additional supporting files required by the sample.

Introduction

Understanding the basics of game coding is the single most-difficult step for a beginning game programmer. While it is easy to find samples that show completed games, tips and tricks, or tutorials that show you how to do specific techniques, there are very few that help you through the process of game construction. The objective of this tutorial is to help you learn about the process of game construction, while guiding you through writing a relatively complete game. In addition, this tutorial will use only those assets found in the complete sample file (GoingBeyond4_Tutorial_Sample.zip), eliminating the need to install additional content. Download the sample file now and extract its contents to a directory on your local drive.

The game you implement will be a simple clone of the popular Asteroids® game by Atari®. The place of Asteroids in video game history is well known, and you are encouraged to read the interesting history of the game on Wikipedia. This tutorial assumes you have a general idea of how the Asteroids game works.

A lot of initial work in this tutorial is already done for you. In fact, this tutorial picks up at the end of the Tutorial 3: Making Sounds with XNA Game Studio tutorial. Once you have completed the first three tutorials in Going Beyond: XNA Game Studio in 3D, you will have a moveable spaceship with sounds and rendering in 3D space. In another 60 to 90 minutes of coding time, you will have a relatively complete Asteroids-style game.

Before You Begin: Getting the Project Ready

Begin this tutorial by completing the first three tutorials in the Going Beyond: XNA Game Studio in 3D series, or by downloading the completed code for the third tutorial (Video Tutorial 3: Making Sounds with XNA Game Studio and XACT) from the XNA Creators Club Online Web site.

Step 1: Ship Shape

The first three tutorials in the Going Beyond: XNA Game Studio in 3D series explained the basics of a single interactive object, rendered in 3D. A true game, however, needs more than just one object. The first step toward making this tutorial into a game is to prepare the game to track and render several objects.

Think of the idea of your ship on the screen. It is drawn using a Model class, it has a position tracked by a Vector3, and still another Vector3 tracks velocity. A float tracks the rotation angle. Each of these data types is modified or checked in different places along the code path, and while the end result looks good to the user, the drawback comes when you try to extend the gameplay to include another object that needs similar data.

If, for instance, you wanted to add a second ship that would also draw on the screen, and had the ability to move and turn, you would have to create a copy of each of the variables you were using for the first ship. You would have to duplicate the code you wrote that checked and modified each variable. Each copied line would be nearly identical to the original line, except that it was acting on a new variable.

For a game that will ultimately have more than a dozen objects all drawing and moving around, this is unworkable. The duplicated code would make your code unreadable and painful to modify. However, there is a better way. If you create a code object that holds the common variables that allow you to draw and move a 3D object, then maintain a list of these objects, you can draw and move them all together using the same code. This process is called encapsulation, and is the beginning of object-oriented programming, which becomes more and more important the larger your game becomes.

Start by right-clicking on your project in Solution Explorer, and select Add, then Class. Type Ship.cs into the Name box, then click Add.

When you add the new file, it will open up in the code window. This new file represents a class, or code object. This particular class is named Ship. You will notice it is very minimal now; modify it so it looks like the following:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace GoingBeyond4
{
    class Ship
    {
        public Model Model;
        public Matrix[] Transforms;

        //Position of the model in world space
        public Vector3 Position = Vector3.Zero;

        //Velocity of the model, applied each frame to the model's position
        public Vector3 Velocity = Vector3.Zero;

        public Matrix RotationMatrix = Matrix.Identity;
        private float rotation;
        public float Rotation
        {
            get { return rotation; }
            set
            {
                float newVal = value;
                while (newVal >= MathHelper.TwoPi)
                {
                    newVal -= MathHelper.TwoPi;
                }
                while (newVal < 0)
                {
                    newVal += MathHelper.TwoPi;
                }

                if (rotation != newVal)
                {
                    rotation = newVal;
                    RotationMatrix = Matrix.CreateRotationY(rotation);
                }

            }
        }

        public void Update(GamePadState controllerState)
        {
            // Rotate the model using the left thumbstick, and scale it down.
            Rotation -= controllerState.ThumbSticks.Left.X * 0.10f;

            // Finally, add this vector to our velocity.
            Velocity += RotationMatrix.Forward * 1.0f * 
                controllerState.Triggers.Right;
        }
    }
}

You can see that the Ship class now does a lot—it holds onto the ship's position, velocity, rotation, and 3D model, and has its own Update method that will move the ship around.

Now that you have created the Ship class, you need to change the code in the Game1.cs code file to take advantage of this new, encapsulated data. Double-click on Game1.cs in your Solution Explorer.

Start with drawing the ship's model. Your original drawing code was inside the Draw method, but that will not scale up to multiple objects very well. You will be drawing Model objects on the screen, so create a method that will draw a chosen Model. Below the Draw method, add a new method called DrawModel, like so:

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();
    }
}

This DrawModel method takes your model-drawing algorithm and applies it to any Model object passed into it, drawing the Model on the screen. Next, modify the Draw call so that it calls this new method:

protected override void Draw(GameTime gameTime)
{
    graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

    Matrix shipTransformMatrix = ship.RotationMatrix
            * Matrix.CreateTranslation(ship.Position);
    DrawModel(ship.Model, shipTransformMatrix, ship.Transforms);
    base.Draw(gameTime);
}

The code from the previous tutorial contained declarations for modelPosition and modelRotation values above the Draw call. Delete those—you will not need them anymore. Also delete the cameraPosition variable—you will recreate this later.

Next, modify the Update and UpdateInput methods to use the values in the new Ship class as follows:

protected override void Update(GameTime gameTime)
{
    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
        ButtonState.Pressed)
        this.Exit();

    // Get some input.
    UpdateInput();

    // Add velocity to the current position.
    ship.Position += ship.Velocity;

    // Bleed off velocity over time.
    ship.Velocity *= 0.95f;

    base.Update(gameTime);
}

protected void UpdateInput()
{
    // Get the game pad state.
    GamePadState currentState = GamePad.GetState(PlayerIndex.One);
    if (currentState.IsConnected)
    {
        ship.Update(currentState);

        //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();
        }


        // In case you get lost, press A to warp back to the center.
        if (currentState.Buttons.A == ButtonState.Pressed)
        {
            ship.Position = Vector3.Zero;
            ship.Velocity = Vector3.Zero;
            ship.Rotation = 0.0f;
            soundHyperspaceActivation.Play();
        }
    }
}

Above the UpdateInput method, remove the modelVelocity variable above Update—it is no longer needed.

Finally, you need to make a change to the way your initialization and content loading are handled. Starting from the top of the Game class and continuing down to just above the call to Update, modify the code as follows:

GraphicsDeviceManager graphics;

//Camera/View information
Vector3 cameraPosition = new Vector3(0.0f, 0.0f, -5000.0f);
Matrix projectionMatrix;
Matrix viewMatrix;

//Audio Components
SoundEffect soundEngine;
SoundEffectInstance soundEngineInstance;
SoundEffect soundHyperspaceActivation;

//Visual components
Ship ship = new Ship();

public Game1()
{
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";
}

/// <summary>
/// Allows the game to perform any initialization it needs to before 
/// starting to run. This is where it can query for any required 
/// services and load any non-graphic related content.  
/// Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
        MathHelper.ToRadians(45.0f),
        GraphicsDevice.DisplayMode.AspectRatio, 
        1.0f, 10000.0f);
    viewMatrix = Matrix.CreateLookAt(cameraPosition, 
        Vector3.Zero, Vector3.Up);

    base.Initialize();
}

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;
}

protected override void LoadContent()
{
    ship.Model = Content.Load<Model>("Models/p1_wedge");
    ship.Transforms = SetupEffectDefaults(ship.Model);
    soundEngine = Content.Load<SoundEffect>("Audio/Waves/engine_2");
    soundEngineInstance = soundEngine.CreateInstance();
    soundHyperspaceActivation = 
        Content.Load<SoundEffect>("Audio/Waves/hyperspace_activate");
}

/// <summary>
/// UnloadContent will be called once per game and is the place to unload
/// all content.
/// </summary>
protected override void UnloadContent()
{
}

While it may seem like a lot of work, the modified code is a good example of encapsulation, and will come in handy as you develop your game.

Step 2: Camera Work

Now that you have a ship object ready, the next step is to get the ship flying around the screen from a top-down point of view. You will accomplish this by simply changing the camera's angle and distance. Finally, you will adjust the rotation mechanics on the user input, so that it matches the behavior you want.

Reverse the camera position along the z-axis by simply altering the value from negative 5000 to positive 25000. The cameraPosition member is declared near the start of the Game1 class. Now your cameraPosition declaration will look like this:

Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 25000.0f);

Unfortunately, if you run the tutorial with only that change, the ship does not show up. This is because the "projection matrix" of the camera is not correct. The formal term that describes the problem is "bounding frustum culling" (also called "viewing frustum culling"). Look in the XNA Game Studio documentation for the BoundingFrustum class, which includes a key diagram to help you learn more about frustums and how they relate to the camera. A camera's near and far plane is set in a specific way to (usually) address performance concerns. In this case, the camera's original near plane is 1 and the far plane is at 10,000. When the camera was set at 5,000 units, like in Figure 1, the ship was in the camera's view space.

Bb975644.OrigCamSetting(en-us,XNAGameStudio.31).png

Figure 1.  Original camera setting and view space

That's perfectly fine when the spaceship was located 5,000 units away from the camera. But when you moved the camera starting point to 25,000, the camera's view space was in the wrong place, as in Figure 2, leaving the ship too far away to be seen.

Bb975644.NewCamSetting(en-us,XNAGameStudio.31).png

Figure 2.  New camera position with incorrect view space

Correct the viewing space problem now. Inside the Initialize method of the Game1 class, you will see the method that creates the projectionMatrix:

projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
    MathHelper.ToRadians(45.0f),
    GraphicsDevice.DisplayMode.AspectRatio, 
    1.0f, 10000.0f);

You need to change the near and far clipping planes of the frustum so that the ship is back in the viewing space. You determine the near and far clipping planes by simple math. The camera is 25,000 units away from the ship, so you set the near plane 5,000 units "closer" to the camera, and the far plane 5,000 units "farther away," like this:

projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
    MathHelper.ToRadians(45.0f),
    GraphicsDevice.DisplayMode.AspectRatio, 
    20000.0f, 30000.0f);

This corrects the viewing space so that the ship is inside it, as in Figure 3.

Bb975644.CorrectedCamSetting(en-us,XNAGameStudio.31).png

Figure 3.  Corrected viewing space

Running the program now will show you the view facing the back end of the ship, rather than facing the nose end. If you fly the ship toward or away from you, you will see the ship disappear as it moves outside the frustum after a few seconds. Now alter the orientation of the ship and how it responds to your input.

In the Ship class, you will change the default orientation of the ship so that it starts from a "top-down" perspective. Double-click on Ship.cs in Solution Explorer.

If the ship is initially facing away from you, a 90-degree rotation along the x-axis will give you the top-down view. Don't forget, you are looking at the ship down the z-axis, so from your perspective, changes in x are "left/right" and changes in y are "up/down." Thus, rotating the ship on the x-axis flips the ship around as if it were spinning on the wings. In the XNA Framework, angular measurements are given in radians, which means you are rotating the ship Pi/2 radians.

Replace the existing declaration of RotationMatrix with the following:

public Matrix RotationMatrix = 
    Matrix.CreateRotationX(MathHelper.PiOver2);

Now, every time you change the ship's rotation (in the ship's Rotation property "set" method), you alter the rotation matrix to include this default rotation, plus the rotation amount along the z-axis supplied by the player's controller. You could just as well rotate along any other axis, provided you:

  • Position the camera properly.
  • Perform your translation and rotation calculations in relation to the correct axis.

Failing to properly calculate translation and rotation movement can yield some surprising, if not frustrating, results. Avoid this by modifying the set method of the Rotation property. Change the existing if clause to match the following:

if (rotation != value)
{
    rotation = value;
    RotationMatrix = 
        Matrix.CreateRotationX(MathHelper.PiOver2) *
        Matrix.CreateRotationZ(rotation);
}

You should notice that your ship appears to be flying slowly now. That is because your view is much farther away than it used to be. Just under the declaration of Velocity, add a floating-point constant that you can use to adjust the ship's velocity:

//amplifies controller speed input
private const float VelocityScale = 5.0f;

At the end of the ship's Update method, change the current Velocity computation to use the VelocityScale value to give the ship a little extra speed (more accurately, it increases the number of units per frame in the game):

Velocity += RotationMatrix.Forward * VelocityScale * 
    controllerState.Triggers.Right;

Running with these changes will now give you a top-down view of the ship, which you can fly around on the screen. If you fly off the screen, press the warp button. It's a good idea to change the original use of the A button to another button, as you will be using the A button to fire in a later step.

Step 3: You Need Rocks. Lots of Them.

You have a ship in the game, so now add asteroids to it. For the sake of simplicity, you are only going to track each asteroid's position, direction, and speed. Create a simple class that has only those three members. Right-click on the GoingBeyond4Windows project in Solution Explorer, click Add, and then click Class. Name it Asteroid.cs. (Don't forget to add a using statement for Microsoft.Xna.Framework). Because this class is "lightweight," you will change it from a class to a structure. (Literally, change the word "class" to "struct" in the file.) There are many nuances about when to use and not use a structure (called a "value type" in C# parlance), which are beyond the scope of this document. Many of the issues relate to performance and garbage collection (GC). In a blog post by the Compact Framework team (http://blogs.msdn.com/netcfteam/archive/2006/12/22/managed-code-performance-on-xbox-360-for-xna-part-2-gc-and-tools.aspx) they say this about value types:

"Games typically have lots of small objects that represent game state. The obvious optimization here is to reduce live object count. You can do that by defining those data structures as structs which are value types (to use more general terminology). Value types stay off the GC heap... of course that assumes that your structs don't get boxed in to objects, which can often happen unknowingly in your code."

In this case, you will use a value type for the Asteroid (and later for bullets) to reduce garbage collection events, as well as to keep the implementation simple.

Add these three members to the structure:

public Vector3 position;
public Vector3 direction;
public float speed;

Double-click on your Game1.cs file. Inside your Game1 class, you will create a simple array that contains asteroids. Add some additional members to your Game1 class to render the asteroids. After the declaration for the ship (Ship ship = new Ship();), add the following:

Model asteroidModel;
Matrix[] asteroidTransforms;
Asteroid[] asteroidList = new Asteroid[GameConstants.NumAsteroids];
Random random = new Random();

There is something new in each of these four lines, so look at each one. The first line is an object that holds on to a lot of information that describes the actual asteroid model loaded by the Content Pipeline processor. You will do that shortly. The second line retains state information related to specific lighting and effect transformations on the asteroid. Because you are not adding any special lighting effects, you will set up a default effect on the model and leave it. The third line is a simple array of asteroids, but you will notice the introduction of the GameConstants class, which will generally hold values that you might want to change as you develop and test the game. There will be more about that shortly. The final line creates a random number generator, which you will use for a few purposes in the game.

Look at this new GameConstants class briefly. One nice design trick for simple games like this is to gather game parameters, which you might want to customize, into a single location. Create that class now. Click Add, and then click Class. Name it GameConstants.cs. Once the file opens, add these constants to the class (you will use the PlayfieldSize constants later):

//camera constants
public const float CameraHeight = 25000.0f;
public const float PlayfieldSizeX = 16000f;
public const float PlayfieldSizeY = 12500f;
//asteroid constants
public const int NumAsteroids = 10;

As you might guess from the addition of the camera constants, you will want to modify the CameraPosition declaration in the Game1 class to look like this now:

Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 
    GameConstants.CameraHeight);

And the initialization of the projectionMatrix (located in the Initialize method) to look like this:

projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
  MathHelper.ToRadians(45.0f), 
  GraphicsDevice.DisplayMode.AspectRatio,
  GameConstants.CameraHeight - 1000.0f,
  GameConstants.CameraHeight + 1000.0f);

Turn your attention back to the Asteroid structure again. To render the asteroid, you need to add an asteroid model to the Content Pipeline. You already have a Content/Models directory in your game, since it is storing your ship model. Add the "asteroid1.x" model to that directory by right-clicking on the directory, clicking Add, and then clicking Existing Item. Then navigate back to the path to which you extracted the contents of the sample file. (Remember, you had to do this when you did "Tutorial 1: Displaying a 3D Model on the Screen" from the "Going Beyond: XNA Game Studio in 3D" series). Select the asteroid1.x file from your Content/Models directory (you might need to select files of type "Content Pipeline Files" to see it), and add it to your Models directory. In addition to adding this model, you will also need to manually copy the asteroid's texture file, "asteroid1.tga," from the Content/Textures directory of the sample to the Content/Textures subfolder in your game project folder. Just manually copy it. Do not use the Add, and then Existing Item approach. Also, be very careful about the copying process. A common beginner's mistake is to copy a Texture file into a Model directory. This is not a good idea.

Now you will visit the LoadContent method in the Game1 class. This is where you will load the mesh model for your asteroid that you just added. Just below the line where you added the p1_wedge model, load the asteroid model and transforms:

asteroidModel = Content.Load<Model>("Models/asteroid1");
asteroidTransforms = SetupEffectDefaults(asteroidModel);

Next, you'll need a method to populate the asteroidList with several asteroids. It will be called at the end of the Initialize method in the Game1 class (before the base.Initialize() call). When you create an asteroid, you will give it a starting speed and random direction. For now, start the asteroids from the center of the screen.

Create a separate method called ResetAsteroids, which will populate the list of asteroids.


private void ResetAsteroids()
{
  for (int i = 0; i < GameConstants.NumAsteroids; i++)
  {
    asteroidList[i].position = Vector3.Zero;
    double angle = random.NextDouble() * 2 * Math.PI;
    asteroidList[i].direction.X = -(float)Math.Sin(angle);
    asteroidList[i].direction.Y = (float)Math.Cos(angle);
    asteroidList[i].speed = GameConstants.AsteroidMinSpeed +
      (float)random.NextDouble() * GameConstants.AsteroidMaxSpeed;
  }
}

Bb975644.note(en-us,XNAGameStudio.31).gifNote
You will need to add two floating-point constants (code given below), AsteroidMinSpeed and AsteroidMaxSpeed, to the GameConstants class yourself. In this example, 100.0 is the minimum speed, and 300.0 is the maximum.
public const float AsteroidMinSpeed = 100.0f;
public const float AsteroidMaxSpeed = 300.0f;

Then add a call to ResetAsteroids() just before the call to base.Initialize() in the Initialize method.

The direction values of the asteroids are using a basic trigonometric function to determine the x and y components of the direction, based on the starting angle. Do not modify the z value because the game only plays in two dimensions.

Now that you have created the asteroids, you need to render them. You should recognize that this should go in the Draw() method. Indeed, you will simply look through the asteroidList and render each asteroid in the same manner as the ship. So, add this code after the completion of the rendering of the ship in the Draw() method.

for (int i = 0; i < GameConstants.NumAsteroids; i++)
{
    Matrix asteroidTransform =
        Matrix.CreateTranslation(asteroidList[i].position);
    DrawModel(asteroidModel, asteroidTransform, asteroidTransforms);
}

If you run this code as-is right now, you will see the ship and single asteroid rendered in the center. There are actually 10 asteroids there, but they are stacked one on top of the other.

The next step is to give the asteroids some motion. This is accomplished in the Update() method by simply iterating over the list and updating their position. Do that just after you update the ship's velocity:

for (int i = 0; i < GameConstants.NumAsteroids; i++)
{
    asteroidList[i].Update(timeDelta);
}

One thing you added is a time delta. This is a small efficiency trick. Calculate the timeDelta value once per update, rather than repeatedly calling the property to check for the total seconds passed. This will be the first line of the Update() method (in the Game1 class):

float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;

Notice that you are calling each asteroid's Update() method in this loop, so you will need to add that method to the Asteroid structure (inside the structures braces, not outside it). Thanks to the expressiveness of the XNA Framework Math library, this can be written in a very simple manner:


public void Update(float delta)
{
  position += direction * speed * GameConstants.AsteroidSpeedAdjustment * delta;
}
Bb975644.note(en-us,XNAGameStudio.31).gifNote
You will need to add the floating-point constant, AsteroidSpeedAdjustment, to the GameConstants class. In this case, use a default value of 5.0.

If everything went well, you will see the asteroids all flying away from the ship in random directions until they all disappear from the screen.

What is wrong with this picture?

Keep the asteroids in the game by wrapping the asteroid around the screen. This is accomplished by allowing the asteroids to drift off the screen, then shifting them to the other side once they have disappeared. The values of playfield size constants were made from some rough approximations based on the actual viewing space. A properly designed game will carefully calculate the field of view area and determine the limits based on asteroid model sizes, and other parameters. In this case, use the PlayfieldSize constants in a simple fashion to determine the "wraparound" trigger areas. After you update the asteroid's position in the Asteroid class's Update method, you then determine if you need to move the asteroid around:

if (position.X > GameConstants.PlayfieldSizeX)
    position.X -= 2 * GameConstants.PlayfieldSizeX;
if (position.X < -GameConstants.PlayfieldSizeX)
    position.X += 2 * GameConstants.PlayfieldSizeX;
if (position.Y > GameConstants.PlayfieldSizeY)
    position.Y -= 2 * GameConstants.PlayfieldSizeY;
if (position.Y < -GameConstants.PlayfieldSizeY)
    position.Y += 2 * GameConstants.PlayfieldSizeY;

Now you should see your asteroids calmly wrapping around the screen as they drift through space. Perfect! Well, almost. This game will not be very interesting if you start all the asteroids in the center, since that would result in a collision with the ship. You need to add some code to start the asteroids on the left or right edge of the screen.

Choosing where to start the asteroid is a little tricky. For the x value of the asteroid's position, you must first choose to start on the left or right side of the screen. Use the random number generator to pick either a 0 or 1. If it's 0, you will start on the left. If it's 1, you will start on the right. To do this, call random.Next(2), which generates a number between 0 and up to, but not including, the passed value (so it only returns a 0 or 1). For the y value of the asteroid's position, simply choose a random number that is within the playfield's y range. This means you will modify the line that assigns the asteroid position a value of Vector3.Zero so that the final method looks like this:

private void ResetAsteroids()
{
    float xStart;
    float yStart;
    for (int i = 0; i < GameConstants.NumAsteroids; i++)
    {
        if (random.Next(2) == 0)
        {
            xStart = (float)-GameConstants.PlayfieldSizeX;
        }
        else
        {
            xStart = (float)GameConstants.PlayfieldSizeX;
        }
        yStart = 
            (float)random.NextDouble() * GameConstants.PlayfieldSizeY;
        asteroidList[i].position = new Vector3(xStart, yStart, 0.0f);
        double angle = random.NextDouble() * 2 * Math.PI;
        asteroidList[i].direction.X = -(float)Math.Sin(angle);
        asteroidList[i].direction.Y = (float)Math.Cos(angle);
        asteroidList[i].speed = GameConstants.AsteroidMinSpeed +
           (float)random.NextDouble() * GameConstants.AsteroidMaxSpeed;
    }
}
Bb975644.note(en-us,XNAGameStudio.31).gifNote
Do not forget to declare the two floating-point values, xStart and yStart, just before the for loop.

It might look a little confusing, but run through the math calculations a couple times to get comfortable with what is going on. At this point, you have a ship in the center of the screen, with several asteroids starting on the sides, moving in random directions and speeds.

Step 4: When Ships and Asteroids Collide

Now, add a few more content items to your game, which you will use in Steps 3 and 4. You will add one model and three sounds to the game:

  1. Add "pea_proj.x" (the bullet model) to the Models section in your project. To do this, right-click Models, click Add, and then click Existing Item. Do not forget you might need to change the Files of Type drop-down to Content Pipeline Files.

    The model is located in the downloaded samples directory under Content/Models —in the same place the asteroid model was lurking. You will also need to copy the "pea_proj.tga" file from the Content/Texture location to your Content/Textures location. Again, do not use Add and then Existing Item here.

  2. As in Tutorial 3 ("Making Sounds with XNA Game Studio"), navigate to the Content/Audio/Waves directory of the downloaded sample directory and copy weapons/explosion3.wav, explosions/explosion2.wav, and weapons/tx0_fire1.wav into your Content/Audio/Waves directory.

Right after the existing sound effect variables, add three new ones to store the sound effects you just added:

SoundEffect soundExplosion2;
SoundEffect soundExplosion3;
SoundEffect soundWeaponsFire;

Now, modify the LoadContent method to load your new sound effects:

soundExplosion2 = 
    Content.Load<SoundEffect>("Audio/Waves/explosion2");
soundExplosion3 = 
    Content.Load<SoundEffect>("Audio/Waves/explosion3");
soundWeaponsFire = 
    Content.Load<SoundEffect>("Audio/Waves/tx0_fire1");

The new explosion and weapons fire effects are now ready to be used when needed during game play.

You have something visually interesting now. You have a ship, with sound effects, that you can move around. You also have asteroids happily flying around on the screen. Unfortunately, you cannot shoot the asteroids. On the other hand, the asteroids also cannot hurt you—yet. Add some collision detection between the ship and the asteroids. In a later step, you will get even by shooting back.

With the XNA Framework, simple collision detection is easy. In this step, you will be using a BoundingSphere, which is an object that creates the smallest-sized sphere (by default) that can enclose the target model. The BoundingSphere contains many different intersection tests, including the ability to detect intersections with planes, rays, boxes, and, of course, other spheres (among other things). Hence, you will put an invisible bubble around each object you want to test, and then determine if they intersect each other.

One trick to remember in gameplay is that you should consider different rules for collisions, depending on the context. In this case, you will deliberately create a bounding sphere around the ship that is smaller than the ship. This is a little game programming trick. Most models are uneven in shape, but a BoundingSphere only takes into account the point farthest from the model's center when creating the sphere's radius. This results in collisions that often appear like they were nowhere near the player's ship. In addition, creating a slightly smaller sphere gives a little more "forgiveness" in case a player gets too close to an asteroid. So, create two constants in the GameConstants class that sets bounding sphere sizes for the asteroids and ship:

public const float AsteroidBoundingSphereScale = 0.95f;  //95% size
public const float ShipBoundingSphereScale = 0.5f;  //50% size

Now, create the actual bounding sphere around the ship, just after you update the asteroid positions in the Update method of the Game1 class. Then create a loop that visits each asteroid. Inside this loop, you create a temporary bounding sphere around the asteroid and determine whether the ship and asteroid sphere are intersecting. If the two spheres intersect, you play an explosion sound and break out of the loop:

//ship-asteroid collision check
BoundingSphere shipSphere = new BoundingSphere(
    ship.Position, ship.Model.Meshes[0].BoundingSphere.Radius *
                         GameConstants.ShipBoundingSphereScale);
for (int i = 0; i < asteroidList.Length; i++)
{
    BoundingSphere b = new BoundingSphere(asteroidList[i].position,
    asteroidModel.Meshes[0].BoundingSphere.Radius *
    GameConstants.AsteroidBoundingSphereScale);
    if (b.Intersects(shipSphere))
    {
        //blow up ship
        soundExplosion3.Play();
        break; //exit the loop
    }
}

Running this program now gives you some great feedback. First, the collision check seems to work pretty well. Second, you hear a collision sound. Third, the sound does not seem right. This is because as the asteroid and ship move through each other, the collision check is constantly firing every frame, with the XNA Framework trying to play the explosion in every frame, causing garbled sound. You can solve this problem by removing the colliding objects from the updating and rendering. In a real game, this means the ship explodes and you lose a life. In the tutorial, simply remove the ship and the offending asteroid from the display and then update. You'll add this feature in the next step.

Step 5: Boom - You're Dead

The code starts getting a little more complex now, but you will leverage some handy secrets in XNA Game Studio to make it easy. To start, you need to create a Boolean flag that tells you if the ship is alive or dead. This will go in the Ship class, right after the declaration of VelocityScale:

public bool isActive = true;

Before you test for a ship and asteroid collision, you need to verify the isActive flag is true. This is done by wrapping the collision code you already wrote in an if statement. This is easy with XNA Game Studio. Highlight the entire block of code that does the collision check (the BoundingSphere declaration and the loop right after it), then right-click the selected code and click Surround With, then select the if statement (not the #if statement) from the list. You will see your code is now wrapped in an if statement, awaiting a Boolean condition. Now all you have to do is replace true with ship.isActive. Finally, set ship.isActive to false after you play the explosion sound.

This fixes the explosion sound, but both the ship and the offending asteroid are still visible in the game. First, remove the ship. Since you have set the flag in the Update() method, that still leaves you the responsibility to not draw the ship anymore. So once again wrap a chunk of code in the Draw() method with the if statement. By now you should be familiar with what portion of the code draws the ship. Select the line of code that draws the ship, right-click, click Surround With, and insert an if (ship.isActive) test.

Running the code now should let you merrily smash your ship into an asteroid, with an accompanying explosion and the disappearance of your ship.

Finally, you need to remove the colliding asteroid. This requires a flag just like the ship. Each asteroid needs an isActive flag that tells whether you should draw or update the asteroid. This is accomplished in five steps, which you should attempt to do on your own:

  1. Create an isActive flag inside the Asteroid class, similar to what you did with the ship.
  2. Set the isActive flag to true when you create each asteroid in the ResetAsteroids method in the Game1 class.
  3. In the code where you draw the asteroids, surround the drawing code in an if statement. This happens inside the loop where you iterate through each asteroid.
  4. Similarly, you now need to do the same thing in the update section, checking to see whether an asteroid is active before you execute a collision test with the ship.
  5. If a ship does collide with an asteroid, set that asteroid's active state to false just after you play the explosion sound.

If you did all the steps correctly, you should have an almost-functional game! Collisions, sounds, moving ships. It's all starting to come together! This leads to the next question: What to do once you blow up the ship? Easy. Press the Warp button. In Tutorial 2: Making Your Model Move Using Input, you wrote some code that reset the ship back to the center. It's still there and still useful (except back then it was the A button). Now go ahead and add a ship.isActive = true statement in the code block for pressing the B button. (Hint: Look in the UpdateInput method in the Game1 class). Also, if you have not changed the Warp button from A to B, now is the time to do it. Instant life-regeneration!

The next step will add bullets to the game, so you can shoot back. The good news is that all the work you have done up to now will make the bullet work seem easy.

Step 6: Revenge of the Ship

In many ways, a bullet in the game is like an asteroid: it travels in a direction and collides with things. You are going to treat bullets just a little differently though, giving the game a little fine-tuning in the process.

Conveniently, the Bullet structure is exactly like the Asteroid structure, so all you need to do is copy the Asteroid implementation file, rename the new file to Bullet.cs, and the structure name to Bullet. In addition, you will want to add these new constants to the GameConstants class for later use:

public const int NumBullets = 30;
public const float BulletSpeedAdjustment = 100.0f;

Think ahead a little bit right now, though. How long do you want the bullets to fly around in space? Do you want them to wrap around the screen? Maybe only live for a certain number of seconds or travel a certain distance? Do you want the bullets to be able to collide with both asteroids and the ship? Any of these approaches are legitimate ways to make the game physics behave. In this case, though, the bullets are simply going to disappear once they go off the screen. This means the Update() method in the Bullet class will flag the bullet as inactive once it drifts off the view.

This is a simple check, similar to what was done with the Asteroid structure:

public void Update(float delta)
{
    position += direction * speed *
                GameConstants.BulletSpeedAdjustment * delta;
    if (position.X > GameConstants.PlayfieldSizeX ||
        position.X < -GameConstants.PlayfieldSizeX ||
        position.Y > GameConstants.PlayfieldSizeY ||
        position.Y < -GameConstants.PlayfieldSizeY)
        isActive = false;
}

As with the Asteroid structure, you are now done with the Bullet structure. However, you have to do several things to make the bullets actually work in the game. You have done this all before with the asteroids, but review the basic steps:

  1. Load the model into the Content Pipeline and set the effect transforms.
  2. Create a list to track all bullets in the game.
  3. Create a bullet and make a firing sound when a player presses a specific button.
  4. Draw the bullet in-flight.
  5. Test the asteroids and bullets for collisions. If they collide, make an explosion sound and remove the colliding bullet and asteroid.

Begin by creating the needed instance variables. Underneath the same place that you created the asteroidList and asteroidModel variables, create a list to hold the bullets and a model to hold the bullet's shape.

Model bulletModel;
Matrix[] bulletTransforms;
Bullet[] bulletList = new Bullet[GameConstants.NumBullets];

Then in the LoadContent() method, assign the pea_proj model to bulletModel. Remember, you added pea_proj.x to the Content/Models directory earlier:

bulletModel = Content.Load<Model>("Models/pea_proj");
bulletTransforms = SetupEffectDefaults(bulletModel);

Unlike the asteroids, you do not create bullets inside Initialize. Instead, create a bullet every time a user presses the A button on the controller. Add a new condition to the UpdateInput() method at the very end:


//are we shooting?
if (ship.isActive && currentState.Buttons.A == ButtonState.Pressed)
{
  //add another bullet.  Find an inactive bullet slot and use it
  //if all bullets slots are used, ignore the user input
  for (int i = 0; i < GameConstants.NumBullets; i++)
  {
    if (!bulletList[i].isActive)
    {
      bulletList[i].direction = ship.RotationMatrix.Forward;
      bulletList[i].speed = GameConstants.BulletSpeedAdjustment;
      bulletList[i].position = ship.Position + (200 * bulletList[i].direction);
      bulletList[i].isActive = true;
      soundWeaponsFire.Play();
      score -= GameConstants.ShotPenalty;
      break; //exit the loop
    }
  }
}

There is an interesting trick in the above code that needs explaining. When it calculate the initial position of the bullet, it appears as if it's firing out the nose of the ship. Thus, the code begins by determining where the bullet is starting from, which is the ship's center. Then it translates the bullet 200 additional units in the direction of the bullet (200 is the rough approximation of the distance from the ship's center to the nose of the ship).

This kind of "motion offset" is very common in game development. One "extra credit" feature you can do is to add the ship's current velocity to the bullet's velocity.

Now it's actually possible to run your game and press the fire (A) button, but you will not yet be able to see the bullets (because you have not drawn them). When you press the fire button (the A button), you might have observed that the sound behaves just like the original problem you had with the asteroid/ship explosions. You are triggering the sound too many times. In fact, you probably noticed that you can hold the fire button down and it will fire a continuous "stream" of bullets (until all the bullet "slots" are used). There is a simple fix to the UpdateInput() method to fire the bullet only once every time the button is pressed.

The problem with UpdateInput is that it is failing to track the user's previous input state. Create a variable that does this. Just after the GraphicsDeviceManager declaration (near the beginning of the Game1 class), add this variable:

GamePadState lastState = GamePad.GetState(PlayerIndex.One);

Then, at the end of the UpdateInput method, save the user's game pad state:

lastState = currentState;

Now all you need to do is change the if statement for the "fire" effect to verify that the button was not held down the last time the code updated:


      if (ship.isActive && currentState.Buttons.A == ButtonState.Pressed &&
      lastState.Buttons.A == ButtonState.Released)
    

When you run the program, you will now hear a firing sound for every time you individually press the A button. Now that you see how to do this, add the same check to your hyperspace button for consistency reasons. The next step is to draw the bullet as it is flying around the screen. Conveniently, this code is identical to the code that draws the asteroids, except you replace the word "asteroid" with "bullet" (in the Draw method):

for (int i = 0; i < GameConstants.NumBullets; i++)
{
    if (bulletList[i].isActive)
    {
        Matrix bulletTransform =
          Matrix.CreateTranslation(bulletList[i].position);
        DrawModel(bulletModel, bulletTransform, bulletTransforms);
    }
}

Then you will again do exactly the same thing in the Update method. Just after the part where you update the asteroid positions (but before you do the asteroid/ship collision test), add the code to update the bullets:

for (int i = 0; i < GameConstants.NumBullets; i++)
{
    if (bulletList[i].isActive)
    {
        bulletList[i].Update(timeDelta);
    }
}

If you run the code at this point in time, you actually have an "almost working" game! All that is left is testing for collisions between the bullet and the asteroids. This process is really quite easy. All you need to do is loop through each asteroid, checking to see if a bullet is colliding with it. If so, deactivate both the colliding bullet and asteroid and continue through the list of asteroids until you are done. The code is almost literally a copy of the ship/asteroid collision code, except instead of if (shipAlive) you have a loop through each asteroid. One thing to note: Do this collision check before checking to see if the ship collides with an asteroid—that way, the player gets credit for a "kill" before getting destroyed!

//bullet-asteroid collision check
for (int i = 0; i < asteroidList.Length; i++)
{
    if (asteroidList[i].isActive)
    {
        BoundingSphere asteroidSphere =
          new BoundingSphere(asteroidList[i].position,
                   asteroidModel.Meshes[0].BoundingSphere.Radius *
                         GameConstants.AsteroidBoundingSphereScale);
        for (int j = 0; j < bulletList.Length; j++)
        {
            if (bulletList[j].isActive)
            {
                BoundingSphere bulletSphere = new BoundingSphere(
                  bulletList[j].position,
                  bulletModel.Meshes[0].BoundingSphere.Radius);
                if (asteroidSphere.Intersects(bulletSphere))
                {
                    soundExplosion2.Play();
                    asteroidList[i].isActive = false;
                    bulletList[j].isActive = false;
                    break; //no need to check other bullets
                }
            }
        }
    }
}

If everything went well, you can now fly a ship around, shoot asteroids, and collide with asteroids. Congratulations, you have written your first XNA Framework game! But wait, the blue background looks, well, nothing at all like a good Asteroids game. You need a space background and, of course, a way to keep score. That is the last step.

Step 7: Space, the Final Frontier

The last step will be to add finishing touches to the game to make it both visually appealing and to give it more of a game feel. You will do this in two parts. The first part it to add a 2D background texture to the game to give it a nice space appearance. The second part will be adding a simple scoring mechanism to the game. When it comes to doing either step, the first thing to remember is that all 2D items are drawn as sprites. A background and score are no different in terms of how they are drawn, but as you will learn, it does matter when they are drawn.

For the first step, you need to create a texture for the starry background. Begin by adding the stars Texture2D object in the same place you declared your Asteroid and Bullet models:

Texture2D stars;

Just after you create the bulletModel and bulletTransforms objects, load the texture:

stars = Content.Load<Texture2D>("Textures/B1_stars");

Lastly, at the beginning of the Draw() method, just after you call Clear on the graphics device, draw the star background. It's important to draw the background at the beginning instead of the end, otherwise, it will obscure everything already drawn (asteroids, and so on) by laying the background on top of the previously drawn objects.

spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, 
    SaveStateMode.None);
spriteBatch.Draw(stars, new Rectangle(0, 0, 800, 600), Color.White);
spriteBatch.End();

Now add the B1_stars.tga file into the Content/Textures area in your project (right-click Textures, click Add, and then click Existing Item. Then browse to the Content\Textures folder of the extracted sample and select the B1_stars.tga file. When you run your game now, you should see a pretty star field in the background, with all your gameplay in the foreground.

All that is left is keeping score in the game. This is accomplished in a few simple steps:

  1. Create a sprite font and add it to the Content Pipeline processing.
  2. Load the sprite font with the rest of your content.
  3. Set the display string and call the DrawString method.

Creating the sprite font is simple. The first thing to do is create a new folder under the Content folder called Fonts. Then right-click this folder, click Add, and then click New Item. From the menu, pick Sprite Font. The default file name for this file is SpriteFont1.spritefont. While you could leave it that way, give it the same name as the font you want to use. Since you will be using the Kootenay font, name the file Kootenay.spritefont. Feel free to experiment with different fonts later, once you are comfortable with this process. Once you create the file, it will open to allow you to edit the different font parameters. Just accept the settings and close it for now.

Bb975644.note(en-us,XNAGameStudio.31).gifNote

Before you go on, it's important to understand that fonts are very technical pieces of art. The people and companies that create them pour an enormous amount of work in them. In many cases, fonts are protected under copyright and licensing terms that widely vary. Just because a font is installed on your computer does not mean you automatically have the right to redistribute the font to anybody else. Keep this in mind if you ever decide to share games that you write. Fortunately, the default font used by the Sprite Font item is redistributable. For more information, see How To: Draw Text.

Now that you created the sprite font, add some code in the Game1 class so that you can display something. Just after you declare the stars object, add a few more declarations:

SpriteFont kootenay;
int score;
Vector2 scorePosition = new Vector2(100, 50);

The first declaration will hold the sprite font. The second is a simple counter for the score. Finally, the scorePosition object will let you position the score in screen coordinates. You could just as well move the scorePosition into the GameConstants class, but due to compilation rules regarding the Vector2 class, you cannot make it a const value.

Loading the sprite font is a one-line addition to the end of the LoadContent method:

kootenay = Content.Load<SpriteFont>("Fonts/Kootenay");

All that is left is to display the score on the screen. This is pretty simple, provided you respect the rules of drawing order. So far, there are four very distinct drawing steps in the Draw method. Draw the background and then the game elements (ship, then asteroids, then bullets). As mentioned previously, if you draw the background after the game elements, all you see is the star field, because drawing the star field last covers the entire screen space. This same issue applies for the game score. Draw the game score last so that it appears overlaid on the rest of the game.

Hopefully by now, you will realize that the score will be drawn just before the base.Draw call is made in the Draw method. The actual code to draw the string is simply a sprite batch Begin/End pair, with the call to DrawString in between:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
      SpriteSortMode.Immediate, SaveStateMode.None);
spriteBatch.DrawString(kootenay, "Score: " + score,
                       scorePosition, Color.LightGreen);
spriteBatch.End();

When you run the game now, you should see a score displayed in the upper-left corner. You will also notice that the game elements appear to render underneath the score, giving the effect you want. Now think about how you want to score the game.

Good gameplay not just about "running and gunning," it's about forcing the player to make decisions and tradeoffs to achieve one or more goals. In this game, you are going to penalize the player for each round the player fires (offensive actions come at a cost) and presses the Warp button (defensive actions come at a cost). You will also penalize the player for dying. In most video games, you are given a limited number of lives, and you subtract a "life" when the player's avatar gets destroyed. However, in this game, a multi-life system is not implemented (you should do that as "extra credit"), so simply take away points. Also, reward points for each asteroid destroyed. First, set up some scoring values in the GameConstants class:

public const int ShotPenalty = 1;
public const int DeathPenalty = 100;
public const int WarpPenalty = 50;
public const int KillBonus = 25;

Now, alter the scores in the appropriate places. For instance, the shot penalty would be added to the UpdateInput method, just after it registers that the player fired a bullet, most likely just after the soundWeaponsFire.Play(); line:

score -= GameConstants.ShotPenalty;

You will need similar approaches in three other areas, which you should accomplish on your own:

  • When the ship is determined to have collided with an asteroid (subtract DeathPenalty from score).
  • When a bullet is determined to have collided with an asteroid (add KillBonus to score).
  • When the player presses the warp button (subtract WarpPenalty from score).

Finally

The initial goal of this tutorial was to show you that the tools, materials, and knowledge to write a game are right at your fingertips, and to guide you through the process of writing your first game. By now, you have learned how to:

  • Change camera views to achieve different rendering perspectives.
  • Write simple collision-detection routines.
  • Create a game environment where many things appear to be happening at once.
  • Integrate 2D and 3D rendering.
  • Render text in your game.
  • Create a feel of "gameplay" where the player has both benefits and penalties with their decisions.

Hopefully, you have also enjoyed the process of making the game. After all, making a game should be just as much fun as playing one! But this is only the beginning. While the game you made is interesting, there are many things you can still do to make the game more engaging and enjoyable. Here are several suggestions (but by no means a complete list) on how you can take your game to the next level:

  • Wrap the ship around on the screen.
  • Vibrate the controller when a ship collides with an asteroid.
  • Split the big asteroids into successively smaller ones.
  • Add explosion effects when a bullet hits an asteroid.
  • Add engine particle effects as the ship flies around.
  • Add a smart "UFO" that attacks the player's ship.
  • Add a "high score" capability to the game.
  • Determine when the playing field is cleared and start a new level, perhaps with more or faster asteroids.

At this point, you've been given many of the basic elements you need to build a game: graphics, input, and sound. Even so, you may be wondering, "How do I build a game?"

Games are an expressive process, with plenty of room for creative problem solving. There is truly no one right way to make a game. With the example you have created, there are still many missing elements. What else does the ship interact with? Does it have a goal? What obstacles prevent the ship from reaching the goal?

Answering these questions will define your game, and make it your own. Play some games that inspire you, check out the XNA Creators Club Online, read up on the Programming Guide, explore the XNA Framework, and have fun building a game of your very own. We hope you enjoy XNA Game Studio!

Community Additions

ADD
Show:
© 2014 Microsoft