Windows Version

Let's start by building the Windows version of the game. In general, when building an XNA game, you will start by developing a Windows version of the game and then "port" that game to an Xbox 360 or Zune project. This is done simply to ease debugging and deployment over the course of development.

Start Visual C# 2008 Express or Visual Studio 2008, and create a new Windows Game (3.0) project named AlienAttack from the Visual C#→XNA Game Studio 3.0 project types, as shown in Figure 1-2.

Creating a Windows Game (3.0) project

Figure 1-2. Creating a Windows Game (3.0) project

A default Windows Game project will be created. Go ahead and either press F5 or click Debug→Start Debugging to run the application. The game will run and you'll be presented with a window filled with the color CornflowerBlue. All you need to do is fill in, well, everything.

Start by renaming the Game1.cs file to AlienAttackGame.cs. The IDE will ask you if you wish to rename all project references to the new name. Select "Yes" when prompted.

If you look through this class, you will see the basic architecture of any XNA game. There are five methods you need to worry about as you progress. They are outlined in Table 1-1.

Table 1-1. Base XNA methods

METHOD

DESCRIPTION

Initialize

Set up anything related to your game that's not content-related.

LoadContent

Load sounds, music, graphics, fonts, or anything else.

UnloadContent

Unload anything that's not part of the ContentManager (see the "Code" section).

Update

Handle any game logic, such as reading the keyboard, controllers, moving sprites, etc.

Draw

Draw everything to the screen.

Let's discuss a few of these methods in detail.

LoadContent

This method is used to, as its name implies, load content. Content can be anything from graphics to sound, music to fonts, and anything else supported by the Content Pipeline. This may be called multiple times per game, whenever the system requires an asset that may no longer be in memory.

At its simplest, the Content Pipeline takes assets (graphics, sounds, fonts, etc.) included in your project and, at compile time, turns them into custom files that can be used directly by the XNA runtime on your PC, Xbox 360, or Zune device. This means you don't need to worry about writing custom image type converters, audio file importers, etc. You can just drag and drop your game assets directly to the Content folder in your game project, and the XNA framework will take care of the rest.

In this game, we will be using three types of content: graphics, sounds, and fonts. As we progress, we will discuss how these items are loaded, the objects that represent them, and how to use them.

In this method, you will see that a SpriteBatch object is created. A SpriteBatch, as its name implies, is used to draw a batch of sprites that all require the same parameters for drawing. We will see this object used later in our drawing methods.

Update

This method is used to update game logic. By default, it is called 60 times per second. This is where user input is handled, the positions of sprites are changed, AI is performed, and so on. Anything that doesn't require drawing is done in this method.

Draw

This method is where everything is drawn to the screen. By default, it is called 60 times per second on the PC and Xbox 360, and 30 times per second on the Zune. Note, however, that there is not necessarily a 1:1 calling of Update and Draw. For our 2-D game, we will be using the previously discussed SpriteBatch to draw our sprites to the screen at the positions calculated during our Update method.

Screens

This game will contain two "screens": the title screen and the game screen. To help separate out the logic for these two screens, you will create an interface named IScreen which will be implemented by two classes: TitleScreen and GameScreen. To start, create a new enumeration named GameState so you can later keep track of which screen you are currently displaying. This can be created above the AlienAttackGame class in the AllienAttackGame.cs file, as shown in Example 1-1.

Example 1-1. GameState enumeration

public enum GameState
{
    TitleScreen,
    GameScreen
};

Next, to keep things organized, start by creating a new folder in your project named Screens. You can do this by right-clicking on the project root in Solution Explorer and selecting Add→New Folder. Inside this folder, add a new Interface named IScreen. To do this, right-click on the Screens folder and select Add→New Item..., selecting Interface in the Templates pane, and then entering the filename IScreen.cs. Our title and game screens will implement this interface, which will provide a generic way to refer to the classes later on. The code for this interface is presented in Example 1-2.

Example 1-2. IScreen interface

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace AlienAttack
{
    public interface IScreen
    {
        GameState Update(GameTime gameTime);
        void Draw(GameTime gameTime, SpriteBatch spriteBatch);
    }
}

You should notice two methods that look very familiar: Update and Draw. The existing AlienAttackGame class also contains these two methods, albeit with slightly different signatures. These methods will be called on the screen object during the regular game's update and draw cycles. Essentially, you will pass off the updating and drawing work to these methods, depending on what screen is currently being displayed.

Title screen

The title screen will contain a pretty simple implementation. You need to show the title screen logo, a message to the user to press a button to start the game, and handle the user pressing that button.

Content. First, let's add our content for this screen. Create a new folder named gfx in the Content folder of the project. All game-related graphics will be stored in this directory. From the Windows game assets that were downloaded earlier, drag and drop bgScreen.png and titleScreen.png from the gfx folder into the Content Pipeline's gfx folder. Then, drag the Arial.spritefont file from the root directory into the root of the Content folder.

PNG files are simply graphics files like any other format, though this format allows for easy application of a transparency channel. This helps draw images without a background getting in the way.

The SpriteFont file is something special. XNA contains methods to draw text to the screen, but it needs a SpriteFont object to know how to render the font. Arial.spritefont is an XML file that defines the characteristics of the font. The Content Pipeline takes this XML file and, at compile time, creates a custom image file containing the alphanumeric characters requested in the font and style specified. Feel free to open the file to view its contents. All of its parameters are very well documented. You can also create your own SpriteFont file by right-clicking the Content folder, selecting Add→New Item, and then selecting Sprite Font.

Code. Now you can start writing the implementation of the title screen. Start by creating a new class in the previously created Screens folder named TitleScreen. This class must implement the previously created IScreen interface and provide code for its two methods, Update and Draw. The basic class format can be seen in Example 1-3.

Example 1-3. TitleScreen base class

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace AlienAttack
{
    public class TitleScreen : IScreen
    {
        public TitleScreen(ContentManager contentManager)
        {
        }

        public GameState Update(GameTime gameTime)
        {
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
        }
    }
}

Previously, you added three items to the content manager. To load them into the game, you need variables to hold them. The PNG files will map to Texture2D objects, and the font will map to a SpriteFont object when all is said and done. So, create three member variables to hold these items, as shown in Example 1-4.

Example 1-4. TitleScreen member variables

public class TitleScreen : IScreen
    {
        private Texture2D titleScreen;
        private Texture2D bgScreen;
        private SpriteFont arialFont;

Next, you need to actually load the content into those variables. This is handled in the TitleScreen constructor. Create the constructor as shown in Example 1-5.

Example 1-5. TitleScreen constructor

public TitleScreen(ContentManager contentManager)
{
    titleScreen = contentManager.Load<Texture2D>("gfx\\titleScreen");
    bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");
    arialFont = contentManager.Load<SpriteFont>("Arial");
}

As you can see, the constructor will expect a ContentManager object to be passed in. The ContentManager is what actually handles the loading of the pipeline-created content at runtime. The Load method is a generic method that must specify a type to load (in this case, Texture2D and SpriteFont), and whose first argument is a path to the content itself. By default, the content pipeline will place the content at the same path shown in the project, and it will give the content the same name as the filename but without the extension.

When this screen is created in the game engine, you will see where that Content Manager object comes from.

Now that the content is loaded, you can implement the Update and Draw methods. First, let's create the Update method as shown in Example 1-6.

Example 1-6. Update method for TitleScreen class

public GameState Update(GameTime gameTime)
{
    if(InputManager.ControlState.Start)
        return GameState.GameScreen;
    return GameState.TitleScreen;
}

This class simply uses the InputManager class (see the "InputManager" section next), and, if the Start button is pressed, returns the GameScreen element from the GameState enumeration, telling the main game engine that it should change to that state. Otherwise, it will continue to return the TitleScreen state.

Before getting to the InputManager class, let's write the Draw method and finish off the implementation of this class. This can be seen in Example 1-7.

Example 1-7. Draw method for TitleScreen class

public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);
    spriteBatch.Draw(titleScreen, Vector2.Zero, Color.White);
#if WINDOWS
    spriteBatch.DrawString(arialFont, "Press Enter or Start to Play",
                           new Vector2(600, 680), Color.White);
#endif

#if XBOX
    spriteBatch.DrawString(arialFont, "Press Start to Play", new Vector2(600, 680),
                           Color.White);
#endif

#if ZUNE
    spriteBatch.DrawString(arialFont, "Press Play to Play", new Vector2(80, 290),
                           Color.White);
#endif
}

This method uses the SpriteBatch object discussed earlier to draw three things to the screen: a background image, then a title screen image, and finally the appropriate text at the bottom of the screen that explains how to start the game.

Each call to the Draw method of the SpriteBatch object requires a Texture2D object (which was loaded earlier), a position at which to draw it, and a color "tint" to give it. The first line of this method draws the background screen shown in Figure 1-3 at position (0, 0), which is what Vector2.Zero equates to, with a color tint of White (that is, no color).

Background image

Figure 1-3. Background image

The second line does the same thing, but this time draws the titleScreen logo image, shown in Figure 1-4, to the screen, layering it on top of the previous image.

Title screen logo

Figure 1-4. Title screen logo

Since the title screen logo image is transparent in the right places, it will seamlessly overlay on top of the background stars.

Finally, it overlays the text as specified on the two layered images at various positions. You'll notice the use of our WINDOWS, XBOX, and ZUNE compiler directives here to draw different text to the screen at different locations depending on the platform you are compiling for. Since the Zune screen is so much smaller, you need to position it at a different location so it will fit on the screen correctly. All of this comes together to produce the screen shown in Figure 1-5.

Final title screen image

Figure 1-5. Final title screen image

InputManager

Before continuing on, let's discuss the InputManager object mentioned earlier. This is a pretty simple helper class that manages input from the keyboard, Xbox 360 controller, or Zune device in one fell swoop. You will again be using the compiler directives to handle input devices for the correct platforms.

Create a new class named InputManager in the root of the project. This will be a public, static class, so be sure to change the class definition to include the public static keywords. Replace the "using" statements with the code in Example 1-8.

Example 1-8. Using statements for InputManager class

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

At the top, before the class definition, create a struct named ControlState as shown in Example 1-9.

Example 1-9. ControlState structure

public struct ControlState
{
    public bool Left;
    public bool Right;
    public bool Start;
    public bool Quit;
    public bool Fire;
}

This struct contains the five input elements we care about for our game. The player ship can move left and right, and it can fire. At a global level, you can start the game or quit the game.

Next you will need to read the current keyboard and control pad states, and you will need to retain a reference to the previous frames states so that you can determine whether a button was newly pressed or is still held down from the previous frame. The main reason for this is that you want to fire only a single shot when the player fires, not one shot for every frame that they have the button held down.

Add the member variables as shown in Example 1-10.

Example 1-10. InputManager member variables

#if !ZUNE
    private static KeyboardState keyboardState, lastKeyboard;
#endif
    private static GamePadState gamePadState, lastGamePad;
    private static ControlState controlState;

Next, we create a method named Update that will be called once per frame by the game's Update method shown earlier. The code for the Update method can be seen in Example 1-11.

Example 1-11. Update method for InputManager class

public static void Update()
{
    #if !ZUNE
       keyboardState = Keyboard.GetState();
    #endif
    gamePadState = GamePad.GetState(PlayerIndex.One);

    controlState.Quit   = (gamePadState.Buttons.Back==ButtonState.Pressed);
    controlState.Start  = (gamePadState.Buttons.B   ==ButtonState.Pressed);
    controlState.Left   = (gamePadState.DPad.Left   ==ButtonState.Pressed);
    controlState.Right  = (gamePadState.DPad.Right  ==ButtonState.Pressed);
    controlState.Fire   = (gamePadState.Buttons.B   ==ButtonState.Pressed &&
                           lastGamePad.Buttons.B    ==ButtonState.Released);

#if !ZUNE
    controlState.Quit   = (controlState.Quit    ||
                           keyboardState.IsKeyDown(Keys.Escape));
    controlState.Start  = (controlState.Start   ||
                           keyboardState.IsKeyDown(Keys.Enter) ||
                           gamePadState.IsButtonDown(Buttons.Start));
    controlState.Left   = (controlState.Left    ||
                           gamePadState.ThumbSticks.Left.X < −0.1f);
    controlState.Right  = (controlState.Right   ||
                           gamePadState.ThumbSticks.Left.X > 0.1f);
    controlState.Left   = (controlState.Left    ||
                           keyboardState.IsKeyDown(Keys.Left));
    controlState.Right  = (controlState.Right   ||
                           keyboardState.IsKeyDown(Keys.Right));
    controlState.Fire   = (controlState.Fire    ||
                           keyboardState.IsKeyDown(Keys.Space) &&
                           !lastKeyboard.IsKeyDown(Keys.Space));
#endif

    lastGamePad = gamePadState;

#if !ZUNE
    lastKeyboard = keyboardState;
#endif
}

This code gets the state of the keyboard and the state of the first controller and stores it away. It then checks the various buttons using the GamePadState and KeyboardState objects, and assigns Boolean values to the controlState struct based on what is pressed and what is not. Finally, it stores the current values away to be used next frame to determine whether buttons are newly pressed or held down.

Also note that this class uses compiler directives to compile only certain sections for certain versions. The Zune has no keyboard, but does have its buttons mapped to the GamePadState object. The Xbox 360 does have a keyboard attachment for the controller (the Chatpad device), and its keys can be read like that of a normal PC keyboard.

Finally, you add a property to return the current value of the controlState member variable, as shown in Example 1-12.

Example 1-12. ControlState property

public static ControlState ControlState
{
    get { return controlState; }
}

Back to the Game

Now that we have a title screen object, its assets, and a way to manage input from the user, we can actually hook up the title screen and have it processed and drawn by the main game class.

Back in the AlienAttackGame class, add the member variables as shown in Example 1-13. Note the usage of the #if compiler directive to set the screen size differently on the Zune build.

Example 1-13. AlienAttackGame member variables

private GameState gameState;
    private IScreen screen;
#if ZUNE
    public static int ScreenWidth = 240;
    public static int ScreenHeight = 320;
#else
    public static int ScreenWidth = 1024;
    public static int ScreenHeight = 768;
#endif

The gameState member will maintain the current state of the game (that is, what screen is being displayed), and the screen member will hold an instance of a class that implements the IScreen interface, namely our TitleScreen class and soon the GameScreen class.

ScreenWidth and ScreenHeight define the size of the screen on which you will be drawing the game. For the PC and Xbox 360 versions, this defaults to 1024×768 as shown. The Zune's native screen resolution is 240×320, and it is defaulted as such. Note that these are public, static variables so that they can be easily used by other parts of the game engine later on.

Next, you need to tell XNA that you want your game to run at the specified resolution. This can be done by setting the PreferredBackBufferWidth and PreferredBack BufferHeight properties on the GraphicsDevice object created in the constructor, as shown in Example 1-14.

Example 1-14. Setting the screen size in AlienAttackGame constructor

public AlienAttackGame()
{
    graphics = new GraphicsDeviceManager(this);

    // set our screen size based on the device
    graphics.PreferredBackBufferWidth = ScreenWidth;
    graphics.PreferredBackBufferHeight = ScreenHeight;

    Content.RootDirectory = "Content";
}

Now we use the Initialize method to instantiate the TitleScreen class and set up our initial game state, as shown in Example 1-15.

Example 1-15. Initialize method

protected override void Initialize()
{
    // create the title screen
    screen = new TitleScreen(this.Content);
    gameState = GameState.TitleScreen;

    base.Initialize();
}

Next, you need to tell the game's Update method to handle input from the user via the InputManager class that was just created, and then update the instance of the IScreen object (now holding an instance of our TitleScreen object). This is shown in Example 1-16, and it replaces the existing Update method in its entirety.

Example 1-16. Update method

protected override void Update(GameTime gameTime)
{
    // update the user input
    InputManager.Update();

    // Allows the game to exit
    if(InputManager.ControlState.Quit)
        this.Exit();

    // update the current screen
    GameState newState = screen.Update(gameTime);

    base.Update(gameTime);
}

Note that this method uses the InputManager to directly determine whether the "Quit" button is pressed and exits immediately if it is.

Finally, we need to draw our title screen in the Draw method of the AlienAttackGame class. This is shown in Example 1-17, and as before, replaces the entire Draw method.

Example 1-17. Draw method

protected override void Draw(GameTime gameTime)
{
    // open the spritebatch, draw the screen, close it up
    this.spriteBatch.Begin();
        screen.Draw(gameTime, this.spriteBatch);
    this.spriteBatch.End();
}

This method tells the sprite batch that a pile of sprites with the same parameters are coming with the Begin method, then hands over drawing to the screen object by calling its Draw method, and finally closes the sprite batch with the End method.

Seeing the Title Screen

With all of this in place, there is actually enough to see something on the screen for the first time. If you compile and run the application, you should see what appears in Figure 1-6.

The title screen

Figure 1-6. The title screen

Game Screen

Now that we have the title screen done, we can begin implementing the actual game. To start, add a new class named GameScreen to the Screens folder. As with the TitleScreen class, GameScreen will also implement the IScreen interface and provide implementations for the Update and Draw methods. As with the title screen, the first thing to draw to the screen is the background star field. This code looks identical to the title screen class and is presented along with the base GameScreen class in Example 1-18.

Example 1-18. GameScreen base class implementation

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

namespace AlienAttack
{
    public class GameScreen : IScreen
    {
        private ContentManager contentManager;
        private Texture2D bgScreen;

        public GameScreen(ContentManager cm)
        {
            contentManager = cm;

            bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");
        }

        public GameState Update(GameTime gameTime)
        {
            return GameState.GameScreen;
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);
        }
    }
}

Adding GameScreen to the main game

Adding this class to the main game is quite simple. Back in the AlienAttackGame class, change the Update method as shown in Example 1-19. The new and modified lines of code are emphasized (and will be henceforth) to help them stand out.

Example 1-19. Rewritten Update method

protected override void Update(GameTime gameTime)
{
    // update the user input
    InputManager.Update();

    // Allows the game to exit
    if(InputManager.ControlState.Quit)
        this.Exit();

    // update the current screen
    GameState newState = screen.Update(gameTime);
    // if the screen returns a new state, change it here
    if(gameState != newState)
    {
        switch(newState)
        {
            case GameState.TitleScreen:
                screen = new TitleScreen(this.Content);
                break;
            case GameState.GameScreen:
                screen = new GameScreen(this.Content);
                break;
        }
        gameState = newState;
    }

    base.Update(gameTime);
}

In this changed method, we now grab the returned value from the screen's Update method. If it returns a state other than the one we are in, we create the screen that handles that new state and continue.

Sound engine

With the release of XNA 3.0 comes a brand-new sound API that is much simpler than the existing XACT API. The new sound library allows you to very easily play simple sound effects, which is all that is required for this game.

First, drag the sfx folder from the downloaded assets to the Content Pipeline. Then, create a new class named AudioManager in the root of the project. We will define an enumeration of the various sound effects that can be played, load them when the AudioManager class is instantiated, and then create a PlayCue method that will play the requested sound effect. All of this is shown in Example 1-20.

Example 1-20. AudioManager class

using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;

namespace AlienAttack
{
    public class AudioManager
    {
        // the different fx that can be played
        public enum Cue
        {
            Theme,
            EnemyShot,
            PlayerShot,
            Explosion
        };

        // instances of the effects
        private SoundEffect theme;
        private SoundEffect enemyShot;
        private SoundEffect playerShot;
        private SoundEffect explosion;

        public AudioManager(ContentManager contentManager)
        {
            // load 'em up
            theme = contentManager.Load<SoundEffect>("sfx\\theme");
            enemyShot = contentManager.Load<SoundEffect>("sfx\\enemyShot");
            playerShot = contentManager.Load<SoundEffect>("sfx\\playerShot");
            explosion = contentManager.Load<SoundEffect>("sfx\\explosion");
        }

        public void PlayCue(Cue cue)
        {
            // play the effect requested
            switch(cue)
            {
                case Cue.Theme:
                    theme.Play();
                    break;
                case Cue.EnemyShot:
                    enemyShot.Play();
                    break;
                case Cue.PlayerShot:
                    playerShot.Play();
                    break;
                case Cue.Explosion:
                    explosion.Play();
                    break;
            }
        }
    }
}

Next, we can add the AudioManager to our AlienAttackGame class and make it available to all of our other classes by making it a static instance. Add a static member variable of type AudioManager to the AlienAttackGame class and then instantiate it in the Initialize method, as shown in Example 1-21.

Example 1-21. AudioManager creation

public static AudioManager AudioManager;

protected override void Initialize()
{
    // create the title screen
    screen = new TitleScreen(this.Content);
    gameState = GameState.TitleScreen;
    // create the audio helper
    AudioManager = new AudioManager(this.Content);

    base.Initialize();
}

This can be immediately used to play the background music for the game screen by adding a call to the PlayCue method in the GameScreen constructor, as shown in Example 1-22.

Example 1-22. Play background music

public GameScreen(ContentManager cm)
{
    contentManager = cm;
    AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Theme);
    bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");
}

Player ship and sprites

The next thing we will add to the game screen is the player ship. This will appear at the bottom of the screen and can move left and right to the edges. To begin, drag the player.png file from the previously downloaded assets to the root of the gfx folder in the solution explorer. This will add it to the content pipeline, just like our previous images, and allow it to be used by the game directly.

A sprite is simply a graphical element drawn to the screen, and the player ship will be one of many sprites in our game. We will be drawing this player ship, the enemies, player shots, and enemy shots throughout the course of the game, so it makes sense to create a Sprite class from which these objects can inherit.

This object will allow you to easily load a single sprite image, provide a position for it on-screen, provide a velocity at which it will move for every frame, and a few extra properties to determine its width, height, and bounding box.

Create a new folder named Sprites in the project. In this folder, add a class named Sprite, and implement it as shown in Example 1-23.

Example 1-23. Sprite class implementation

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace AlienAttack
{
    public abstract class Sprite
    {
        // all textures in animation set
        protected Texture2D[] spriteTextures;

        // current frame to draw
        protected int frameIndex;

        public ContentManager Content;
        public Vector2 Position;
        public Vector2 Velocity;

        // bounding box of image...used for collision detection
        private Rectangle boundingBox;

        public Sprite()
        {
        }

        public Sprite(ContentManager contentManager)
        {
            this.Content = contentManager;
        }

        public Sprite(ContentManager contentManager, string contentName) :
            this(contentManager)
        {
            spriteTextures = new Texture2D[1];

            // load the image
            spriteTextures[0] = this.Content.Load<Texture2D>(contentName);
        }

        public virtual void Update(GameTime gameTime)
        {
            // move the sprite based on the provided velocity
            this.Position += this.Velocity;
        }

        public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(spriteTextures[frameIndex], this.Position, Color.White);
        }

        public virtual int Width
        {
            get { return spriteTextures[0].Width; }
        }

        public virtual int Height
        {
            get { return spriteTextures[0].Height; }
        }

        public virtual Rectangle BoundingBox
        {
            get
            {
                // only need to assign this once
                if(boundingBox == Rectangle.Empty)
                {
                    boundingBox.Width = this.Width;
                    boundingBox.Height = this.Height;
                }
                boundingBox.X = (int)this.Position.X;
                boundingBox.Y = (int)this.Position.Y;

                return boundingBox;
            }
        }
    }
}

This class represents a very simple sprite. It has a position on-screen, a velocity at which it moves, a width, a height, and a bounding box based on where it is on the screen (see the later section Collision detection and explosions). Additionally, it contains an array of images named spriteTextures (Texture2D objects) to be loaded. These would be all the frames of animation required to draw the sprite to the screen. The frameIndex variable maintains the current frame to draw and will be updated by the derived sprites later on as required.

With the base class in place, we can now implement the player object, which will represent the player's ship on-screen.

Add a new class named Player to the sprites folder, and implement as shown in Example 1-24.

Example 1-24. Player class implementation

using Microsoft.Xna.Framework.Content;

namespace AlienAttack
{
    public class Player : Sprite
    {
        public Player(ContentManager contentManager) : base(contentManager,
                                                            "gfx\\player")
        {
            this.Position.X = AlienAttackGame.ScreenWidth/2 - this.Width/2;
#if ZUNE
            this.Position.Y = AlienAttackGame.ScreenHeight - 40;
#else
            this.Position.Y = AlienAttackGame.ScreenHeight - 100;
#endif
        }
    }
}

This class simply loads the content for the player (a single sprite frame), and sets its default position to the center of the screen near the bottom. The default Update and Draw methods from the base Sprite class will be used to update and draw the sprite to the screen.

Back in the GameScreen class, we can create a new Player object, update it, and draw it. To do this, we will modify the GameScreen's member variables, constructor, and Update and Draw methods, and add a new method named MovePlayer, as shown in Example 1-25.

Example 1-25. Adding Player to GameScreen

private Player player;
private float PlayerVelocity = AlienAttackGame.ScreenWidth / 200.0f;a

public GameScreen(ContentManager cm)
{
    contentManager = cm;
    AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Theme);
    bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");
    player = new Player(contentManager);
}

public GameState Update(GameTime gameTime)
{
    MovePlayer(gameTime);
               return GameState.GameScreen;
}

public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);

    // draw the player
    if(player != null)
        player.Draw(gameTime, spriteBatch);
}

private void MovePlayer(GameTime gameTime)
{
    if(player != null)
    {
        // move left
        if(InputManager.ControlState.Left && player.Position.X > 0)
            player.Position.X -= PlayerVelocity;

        // move right
        if(InputManager.ControlState.Right&&
            player.Position.X + player.Width < AlienAttackGame.ScreenWidth)
        player.Position.X += PlayerVelocity;
        player.Update(gameTime);
    }
}

Note that the Update method uses the InputManager object we previously wrote to check the state of the Left and Right controls. If either is pressed, the player ship's position is moved either left or right as appropriate.

If you were to run the game at this point, you would hear the background music and should see the background and the lonely player ship at the bottom of the screen, which can move left and right via the arrow keys and gamepad. Not much of a game, but it's a good start.

Player shots

Now let's add the ability for the player to fire. First, let's add the content to the content pipeline. To do this, drag the pshot folder from the downloaded assets to the gfx directory in the project. This will add the directory and its contents—two image files that will be played as a short animation.

Next, we will create another object in the Sprites folder that inherits from the Sprite class named PlayerShot, as shown in Example 1-26.

Example 1-26. PlayerShot class

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace AlienAttack
{
    public class PlayerShot : Sprite
    {
        // all frames of animation
        private double lastTime;

        public PlayerShot(ContentManager contentManager) : base(contentManager)
        {
            spriteTextures = new Texture2D[2];

            // load the frames
            for(int i = 0; i <= 1; i++)
                spriteTextures[i] =
                    contentManager.Load<Texture2D>("gfx\\pshot\\pshot_" + i);
        }
        public override void Update(GameTime gameTime)
        {
            // draw a new frame very 200ms...seems to be a good value
            if(gameTime.TotalGameTime.TotalMilliseconds - lastTime > 200)
            {
                // toggle between frames
                frameIndex = frameIndex == 0 ? 1 : 0;
                lastTime = gameTime.TotalGameTime.TotalMilliseconds;
            }

            this.Position.Y -= 5;
        }
    }
}

This class loads the two animation frames in the constructor and stores them away. The two frames are used to give the sprite a flashing effect as it travels up the screen. The Update method determines when 200 milliseconds have elapsed and, when it has, moves to the next frame to display. The base Sprite class handles the drawing.

Back in the GameScreen class, we can now allow the player to fire from the ship. Since many shots can appear on the screen at once, we will use a List object to store all instances of the player's shots currently on the screen. In the Update method we will call a new UpdatePlayerShots method, which will determine when the Fire button has been pressed and, if a specified time interval has elapsed (so we don't create a shot per frame), we will create a new PlayerShot object and add it to the list, as well as play the sound cue to go with it. Finally, the Update method will enumerate through all player shots, update their positions, and remove any that have gone off the top of the screen, and the Draw method will enumerate all shots and draw them to the screen. All of this can be seen in Example 1-27.

Example 1-27. PlayerShot usage

private List<PlayerShot> playerShots;
private double lastTime;

public GameScreen(ContentManager cm)
{
    contentManager = cm;
    AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Theme);
    bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");
    player = new Player(contentManager);

    playerShots = new List<PlayerShot>();
}
public GameState Update(GameTime gameTime)
{
    MovePlayer(gameTime);
    UpdatePlayerShots(gameTime);

    return GameState.GameScreen;
}

public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);

    // draw the player
    if(player != null)
        player.Draw(gameTime, spriteBatch);

    // draw the player shots
    foreach(PlayerShot playerShot in playerShots)
        playerShot.Draw(gameTime, spriteBatch);
}

private void UpdatePlayerShots(GameTime gameTime)
{
    // if we are allowed to fire, add a shot to the list
    if(InputManager.ControlState.Fire &&
       gameTime.TotalGameTime.TotalMilliseconds - lastTime > 500)
    {
        // create a new shot over the ship
        PlayerShot ps = new PlayerShot(this.contentManager);
        ps.Position.X = (player.Position.X + player.Width/2) - ps.Width/2;
        ps.Position.Y = player.Position.Y - ps.Height;
        playerShots.Add(ps);
        lastTime = gameTime.TotalGameTime.TotalMilliseconds;
        AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.PlayerShot);
    }

    // enumerate the player shots on the screen
    for(int i = 0; i < playerShots.Count; i++)
    {
        PlayerShot playerShot = playerShots[i];

        playerShot.Update(gameTime);

        // if it's off the top of the screen, remove it from the list
        if(playerShot.Position.Y + playerShot.Height < 0)
        {
            playerShots.RemoveAt(i);
            playerShot = null;
        }
    }
}

Running the game at this point will allow you to fire shots up the screen.

Scoring and lives remaining

Next, let's add the score display and the lives remaining icon at the lower left of the screen. For this, we will need our Arial font that we used earlier, but since it's already in the Content Pipeline, we do not need to add it again.

We will use the existing Arial SpriteFont to draw the score and number of lives remaining. Additionally, we will use our already created Player sprite to draw the ship icon to the left of the lives remaining. This can all be seen in Example 1-28.

Example 1-28. Score and lives remaining

private Player livesIcon;
private int score;
private int lives;
private SpriteFont arial;
private bool loseGame;

public GameScreen(ContentManager cm)
{
    contentManager = cm;
    AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Theme);
    bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");
    player = new Player(contentManager);

    playerShots = new List<PlayerShot>();

    arial = contentManager.Load<SpriteFont>("arial");

    // draw a lives status icon in the lower left
    livesIcon = new Player(contentManager);

#if ZUNE
    livesIcon.Position = new Vector2(0, AlienAttackGame.ScreenHeight-20);
#else
    livesIcon.Position = new Vector2(40, AlienAttackGame.ScreenHeight-60);
#endif
    lives = 2;
}

public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);

    // draw the player
    if(player != null)
        player.Draw(gameTime, spriteBatch);

    // draw the player shots
    foreach(PlayerShot playerShot in playerShots)
        playerShot.Draw(gameTime, spriteBatch);

#if ZUNE
    // draw the score
    spriteBatch.DrawString(arial, "Score", new Vector2(0, 0), Color.White);
    spriteBatch.DrawString(arial, score.ToString(), new Vector2(0, 20), Color.White);

    // draw the lives icon
    livesIcon.Draw(gameTime, spriteBatch);
    spriteBatch.DrawString(arial, "x" + lives.ToString(),
        new Vector2(livesIcon.Position.X + livesIcon.Width + 4,
                    livesIcon.Position.Y),
        Color.White);
#else
    // draw the score
    spriteBatch.DrawString(arial, "Score", new Vector2(50, 50), Color.White);
    spriteBatch.DrawString(arial, score.ToString(), new Vector2(50, 80),
        Color.White);

    // draw the lives icon
    livesIcon.Draw(gameTime, spriteBatch);
    spriteBatch.DrawString(arial, "x" + lives.ToString(),
        new Vector2(livesIcon.Position.X + livesIcon.Width + 4,
                    livesIcon.Position.Y+8),
        Color.White);
#endif
}

If you run the project at this point, you should see a score and lives remaining display, and you should be able to shoot using either the space bar or the A button on the game pad.

Now we need enemies.

Enemies

Enemies are just another sprite in our game engine. We must first drag the content to the Content Pipeline for the enemy. In this case, drag the enemy1 folder from the downloaded assets to the gfx folder in the project. Then, create a new class named Enemy in our Sprites folder. This class, as with all of our other sprites, will inherit from the base Sprite class. The implementation for this class looks very similar to that of our PlayerShot class. It will load all frames of animation, and change the animation frame index in the Update method. The animation simply moves forward through the animation set, and then backward. The implementation is shown in Example 1-29.

Example 1-29. Enemy class implementation

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace AlienAttack
{
    public class Enemy : Sprite
    {
        // which direction are we moving through the animation set?
        private int direction = 1;

        double lastTime;

        public Enemy(ContentManager contentManager)
        {
            spriteTextures = new Texture2D[10];

            // load the spriteTextures
            for(int i = 0; i <= 9; i++)
                spriteTextures[i] =
                    contentManager.Load<Texture2D>("gfx\\enemy1\\enemy1_" + i);

            this.Velocity.X = 1;
        }

        public override void Update(GameTime gameTime)
        {
            // if we're at the end of the animation, reverse direction
            if(frameIndex == 9)
                direction = −1;

            // if we're at the start of the animation, reverse direction
            if(frameIndex == 0)
                direction = 1;

            // every 70ms, move to the next frame
            if(gameTime.TotalGameTime.TotalMilliseconds - lastTime > 70)
            {
                frameIndex += direction;
                lastTime = gameTime.TotalGameTime.TotalMilliseconds;
            }
        }
    }
}

EnemyGroup

As shown in the earlier game screenshot, the screen contains a grid of enemies that will move back and forth. In order to easily maintain this entire group, we will create another Sprite-based object named EnemyGroup.EnemyGroup will create the grid of enemies, move them around the screen, make them fire shots at the player, and handle collisions from the player shots.

Create a new class named EnemyGroup in the Sprites folder. As with all sprites, this will inherit from the base Sprite class. This won't be quite like the rest of the sprites we already created, but it allows us to take advantage of some of the inherent sprite functionality we've already written.

This class is a bit large, so I'm going to break it down a bit differently than the previous sections. For now, we are only going to worry about creating the grid of enemy sprites and moving them around the screen. First, let's set up the base class as shown in Example 1-30.

Example 1-30. EnemyGroup base class

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

namespace AlienAttack
{
    public class EnemyGroup : Sprite
    {
        // grid of enemies
        private Enemy[,] enemies;

        // width of single enemy
        private int enemyWidth;
#if ZUNE
        private const int EnemyRows = 4; // number of rows in grid
        private const int EnemyCols = 6; // number of cols in grid
        private const int EnemyVerticalJump = 3; // number of pixels to jump
                                                 // vertically after hitting edge
        private const int EnemyStartPosition = 10; // vertical position of grid
        private const int ScreenEdge = 3; // virtual edge of screen to change dir
        private Vector2 EnemySpacing = new Vector2(2, 2); // space between sprites
        private const float EnemyVelocity = 0.5f; //  speed at which grid moves per
                                                  //  frame
#else
        private const int EnemyRows = 4; // number of rows in grid
        private const int EnemyCols = 8; // number of cols in grid
        private const int EnemyVerticalJump = 10; // number of pixels to jump
                                                  // vertically after hitting edge
        private const int EnemyStartPosition = 100; // vertical position of grid
        private const int ScreenEdge = 20; // virtual edge of screen to change dir
        private Vector2 EnemySpacing = new Vector2(4, 4); // space between sprites
        private const float EnemyVelocity = 1.5f; // speed at which grid moves per
                                                  // frame
#endif
        public EnemyGroup(ContentManager contentManager) : base(contentManager)
        {
        }

        public override void Update(GameTime gameTime)
        {
        }

        public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
        }
    }
}

All of those constant values will be used later as we progress though this class.

The constructor for this object will create and center our enemy grid on the screen, as shown in Example 1-31.

Example 1-31. EnemyGroup constructor

public EnemyGroup(ContentManager contentManager) : base(contentManager)
{
    enemies = new Enemy[EnemyRows,EnemyCols];

    // create a grid of enemies
    for(int y = 0; y < EnemyRows; y++)
    {
        for(int x = 0; x < EnemyCols; x++)
        {
            Enemy enemy = new Enemy(contentManager);
            enemy.Position.X = x * enemy.Width + EnemySpacing.X;
            enemy.Position.Y = y * enemy.Height + EnemySpacing.Y;
            enemies[y,x] = enemy;
        }
    }

    enemyWidth = enemies[0,0].Width;

    // position the grid centered at the vertical position specified above
    this.Position.X = AlienAttackGame.ScreenWidth/2 -
                      ((EnemyCols * (enemyWidth + EnemySpacing.X)) / 2);
    this.Position.Y = EnemyStartPosition;
    this.Velocity.X = EnemyVelocity;
}

The Update method shown in Example 1-32 will use a method we will write named MoveEnemies to move the enemies left and right across the screen, dropping them a few pixels when they hit the edge. We will also write a few helper methods to determine the leftmost and rightmost enemies still left in the grid so we know when the left and right side of the grid hits the edges of the screen.

Example 1-32. Update method for EnemyGroup class

public override void Update(GameTime gameTime)
{
    MoveEnemies(gameTime);
}

private Enemy FindRightMostEnemy()
{
    // find the enemy in the right-most position in the grid
    for(int x = EnemyCols-1; x > −1; x--)
    {
        for(int y = 0; y < EnemyRows; y++)
        {
            if(enemies[y,x] != null)
                return enemies[y,x];
        }
    }
    return null;
}

private Enemy FindLeftMostEnemy()
{
    // find the enemy in the left-most position in the grid
    for(int x = 0; x < EnemyCols; x++)
    {
        for(int y = 0; y < EnemyRows; y++)
        {
            if(enemies[y,x] != null)
                return enemies[y,x];
        }
    }
    return null;
}

public bool AllDestroyed()
{
    // we won if we can't find any enemies at all
    return (FindLeftMostEnemy() == null);
}

private void MoveEnemies(GameTime gameTime)
{
    Enemy enemy = FindRightMostEnemy();

    // if the right-most enemy hit the screen edge, change directions
    if(enemy != null)
    {
        if(enemy.Position.X + enemy.Width > AlienAttackGame.ScreenWidth - ScreenEdge)
        {
            this.Position.Y += EnemyVerticalJump;
            this.Velocity.X = -EnemyVelocity;    // move left
        }
    }

    enemy = FindLeftMostEnemy();

    // if the left-most enemy hit the screen edge, change direction
    if(enemy != null)
    {
        if(enemy.Position.X < ScreenEdge)
        {
            this.Position.Y += EnemyVerticalJump;
            this.Velocity.X = EnemyVelocity;    // move right
        }
    }

    // update the positions of all enemies
    for(int y = 0; y < EnemyRows; y++)
    {
        for(int x = 0; x < EnemyCols; x++)
        {
            if(enemies[y,x] != null)
            {
                // X = position of the whole grid +
                // (X grid position * width of enemy) + padding
                // Y = position of the whole grid +
                // (Y grid position * width of enemy) + padding //
                enemies[y,x].Position.X =
                   (this.Position.X + (x * (enemyWidth + EnemySpacing.X)));
                enemies[y,x].Position.Y =
                   (this.Position.Y + (y * (enemyWidth + EnemySpacing.X)));
                enemies[y,x].Update(gameTime);
            }
        }
    }

    this.Position += this.Velocity;
}

The final methods will be used later, to determine when all enemies have been destroyed and to reset the board appropriately.

And finally, we will draw all enemies in the grid at their current positions, shown in Example 1-33.

Example 1-33. Draw method for EnemyGroup class

public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    // draw all active enemies
    foreach(Enemy enemy in enemies)
    {
        if(enemy != null)
            enemy.Draw(gameTime, spriteBatch);
    }
}

Now we can hook up the EnemyGroup class in our GameScreen. We create an instance of the EnemyGroup in the constructor, update it in the Update method, and draw it in the Draw method as shown in Example 1-34.

Example 1-34. Using EnemyGroup in GameScreen

private EnemyGroup enemyGroup;

public GameScreen(ContentManager cm)
{
    contentManager = cm;
    AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Theme);
    bgScreen = contentManager.Load<Texture2D>("gfx\\bgScreen");

    player = new Player(contentManager);

    playerShots = new List<PlayerShot>();

    arial = contentManager.Load<SpriteFont>("arial");

    // draw a lives status icon in the lower left
    livesIcon = new Player(contentManager);
#if ZUNE
    livesIcon.Position = new Vector2(0, AlienAttackGame.ScreenHeight-20);
#else
    livesIcon.Position = new Vector2(40, AlienAttackGame.ScreenHeight-60);
#endif

    enemyGroup = new EnemyGroup(contentManager);

    lives = 2;
}

public GameState Update(GameTime gameTime)
{
    MovePlayer(gameTime);
    HandlePlayerShots(gameTime);

    // as long as we're not in the lose state, update the enemies
    if(!loseGame)
        enemyGroup.Update(gameTime);

    return GameState.GameScreen;
}

public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);

    // draw the player
    if(player != null)
        player.Draw(gameTime, spriteBatch);

    // draw the enemy board
    enemyGroup.Draw(gameTime, spriteBatch);

    // draw the player shots
    foreach(PlayerShot playerShot in playerShots)
        playerShot.Draw(gameTime, spriteBatch);

    // draw the score
#if ZUNE
    spriteBatch.DrawString(arial, "Score", new Vector2(0, 0), Color.White);
    spriteBatch.DrawString(arial, score.ToString(), new Vector2(0, 30), Color.White);
#else
    spriteBatch.DrawString(arial, "Score", new Vector2(50, 50), Color.White);
    spriteBatch.DrawString(arial, score.ToString(), new Vector2(50, 80),
                           Color.White);
#endif

    // draw the lives icon
    livesIcon.Draw(gameTime, spriteBatch);
    spriteBatch.DrawString(arial, "x" + lives.ToString(),
        new Vector2(livesIcon.Position.X + livesIcon.Width + 4,
                    livesIcon.Position.Y + 8),
        Color.White);
}

Running the game now will show the enemy grid moving left and right, and advancing down the screen after hitting the screen boundaries.

Next, you need the enemies to shoot at the player, or otherwise the game won't be very challenging. So, you need the opposite of the PlayerShot—the EnemyShot.

Drag the eshot folder into the gfx folder in the Content Pipeline so you have something to draw on the screen. These animation frames look just like the player shot, except they are pink.

Next, create a new class named EnemyShot in the Sprites folder. The code for this is almost identical to the PlayerShot class, except that it loads different content when created. The full class implementation can be found in Example 1-35.

Example 1-35. EnemyShot class

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

namespace AlienAttack
{
    public class EnemyShot : Sprite
    {
        double lastTime;

        public EnemyShot(ContentManager contentManager) : base(contentManager)
        {
            spriteTextures = new Texture2D[2];

            for(int i = 0; i <= 1; i++)
                spriteTextures[i] =
                    contentManager.Load<Texture2D>("gfx\\eshot\\eshot_" + i);

            this.Velocity.Y = 3;
        }

        public override void Update(GameTime gameTime)
        {
            if(gameTime.TotalGameTime.TotalMilliseconds - lastTime > 200)
            {
                frameIndex = frameIndex == 0 ? 1 : 0;
                lastTime = gameTime.TotalGameTime.TotalMilliseconds;
            }

            this.Position += this.Velocity;
        }
    }
}

Next, you need to actually drop the shot from the enemy toward the player. The easiest way to implement this is to create a random number generator and get a new value on every frame. When that value is above a certain threshold, you will create a new enemy shot on the screen and drop it toward the player.

Back in your EnemyGroup object, you will create a list to hold enemy shots currently on screen and set up the random number generator, as shown in Example 1-36.

Example 1-36. EnemyGroup constructor with EnemyShot set up

// all enemy shots
private List<EnemyShot> enemyShots;

private Random random;

public EnemyGroup(ContentManager contentManager) : base(contentManager)
{
    random = new Random();

    enemyShots = new List<EnemyShot>();

    enemies = new Enemy[EnemyRows,EnemyCols];

    // create a grid of enemies
    for(int y = 0; y < EnemyRows; y++)
    {
        for(int x = 0; x < EnemyCols; x++)
        {
            Enemy enemy = new Enemy(contentManager);
            enemy.Position.X = x * enemy.Width + EnemySpacing.X;
            enemy.Position.Y = y * enemy.Height + EnemySpacing.Y;
            enemies[y,x] = enemy;
        }
    }

    enemyWidth = enemies[0,0].Width;

    // position the grid centered at the vertical position specified above
    this.Position.X = AlienAttackGame.ScreenWidth/2 -
                      ((EnemyCols * (enemyWidth + EnemySpacing.X)) / 2);
    this.Position.Y = EnemyStartPosition;
    this.Velocity.X = EnemyVelocity;
}

In the Update method we will call a new method named EnemyFire that will grab a random value and then create an enemy shot under the selected enemy if the random value is above a certain threshold. The NextDouble method from the Random object will provide us with a decimal value between 0 and 1. We will check whether this value is above 0.99 and create a new enemy shot. It will also update all shots in the list to move them down the screen, removing any that have fallen off the bottom, as shown in Example 1-37.

Example 1-37. Updated Update method

public override void Update(GameTime gameTime)
{
    MoveEnemies(gameTime);
    EnemyFire(gameTime);
}

private void EnemyFire(GameTime gameTime)
{
    // at random times, drop an enemy shot
    if(random.NextDouble() > 0.99f)
    {
        int x, y;

        // find an enemy that hasn't been destroyed
        do
        {
            x = (int)(random.NextDouble() * EnemyCols);
            y = (int)(random.NextDouble() * EnemyRows);
        }
        while(enemies[y,x] == null);

        // create a shot for that enemy and add it to the list
        EnemyShot enemyShot = new EnemyShot(this.Content);
        enemyShot.Position = enemies[y,x].Position;
        enemyShot.Position.Y += enemies[y,x].Height;
        enemyShots.Add(enemyShot);

        AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.EnemyShot);
    }

    for(int i = 0; i < enemyShots.Count; i++)
    {
        // update all shots
        enemyShots[i].Update(gameTime);

        // remove those that are off the screen
        if(enemyShots[i].Position.Y > AlienAttackGame.ScreenHeight)
            enemyShots.RemoveAt(i);
    }
}

Our Draw method will enumerate the enemy shot list and draw all enemy shots to the screen. This can be seen in the updated code in Example 1-38.

Example 1-38. Updated Draw method

public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    // draw all active enemies
    foreach(Enemy enemy in enemies)
    {
        if(enemy != null)
            enemy.Draw(gameTime, spriteBatch);
    }

    // draw all enemy shots
    foreach(EnemyShot enemyShot in enemyShots)
        enemyShot.Draw(gameTime, spriteBatch;)
}

Finally, add a method named Reset that will clear the screen of all EnemyShot objects, as shown in Example 1-39.

Example 1-39. Reset method

public void Reset()
{
    enemyShots.Clear();
}

Running the game at this point will now show a pretty complete game. The player can move and fire, the enemies can move and fire, but you'll notice that shots will travel right through the player and the enemies. We now need to add collision detection and explosions.

Collision detection and explosions

Collision detection does exactly what it sounds like: it detects collisions. In our game, we need to know when a player bullet hits an enemy ship, when their bullets hit the player, and when an enemy and player collide.

There are a variety of methods to handle collision detection, but we will use one of the simplest of all since our game really doesn't require more: bounding-box collision detection.

The concept here is that each sprite on screen has an invisible border drawn around its outermost edges, as shown in Figure 1-7. If two of these borders intersect each other, as shown in Figure 1-8, we know the two objects touched each other and we respond appropriately.

Bounding box around sprite

Figure 1-7. Bounding box around sprite

Two bounding boxes intersecting

Figure 1-8. Two bounding boxes intersecting

The response will be an explosion at the site of the collision. So, before we implement the actual collision detection, let's set up our explosion animation.

First, drag the explosion folder from the downloaded assets to the gfx folder in the Content Pipeline. Then, create a new sprite named Explosion in the Sprites folder. This class will inherit from the base Sprite object and provide the usual loading and animation routines. The full class is shown in Example 1-40.

Example 1-40. Explosion class

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace AlienAttack
{
    public class Explosion : Sprite
    {
        double lastTime;

        public Explosion(ContentManager contentManager)
        {
            spriteTextures = new Texture2D[10];

            for(int i = 0; i <= 9; i++)
                spriteTextures[i] =
                    contentManager.Load<Texture2D>("gfx\\explosion\\explosion_" + i);
        }

        public new bool Update(GameTime gameTime)
        {
            // if it's the final frame,
            // return true to let the other side know we're done
            if(frameIndex == 9)
                return true;

            // new frame every 70ms
            if(gameTime.TotalGameTime.TotalMilliseconds - lastTime > 70)
            {
                frameIndex++;
                lastTime = gameTime.TotalGameTime.TotalMilliseconds;
            }

            return false;
        }
    }
}

To actually handle collisions, we will rely on our EnemyGroup object, since it knows where all enemies and enemy shots are at any given time. Let's start with player shot and enemy collisions. We will pass all the player shots on-screen to a new method named HandlePlayerShotCollision. This will enumerate through all enemies left on the screen and determine whether their bounding boxes intersect with the player shots. If there is an intersection, it will drop an animation sprite at the appropriate position. This is all shown in Example 1-41.

Example 1-41. HandlePlayerShotCollision method

private List<Explosion> explosions;

public EnemyGroup(ContentManager contentManager) : base(contentManager)
{
    random = new Random();

    enemyShots = new List<EnemyShot>();

    enemies = new Enemy[EnemyRows,EnemyCols];

    explosions  = new List<Explosion>();

    // create a grid of enemies
    for(int y = 0; y < EnemyRows; y++)
    {
        for(int x = 0; x < EnemyCols; x++)
        {
            Enemy enemy = new Enemy(contentManager);
            enemy.Position.X = x * enemy.Width + EnemySpacing.X;
            enemy.Position.Y = y * enemy.Height + EnemySpacing.Y;
            enemies[y,x] = enemy;
        }
    }

    enemyWidth = enemies[0,0].Width;

    // position the grid centered at the vertical position specified above
    this.Position.X = AlienAttackGame.ScreenWidth/2 -
                      ((EnemyCols * (enemyWidth + EnemySpacing.X)) / 2);
    this.Position.Y = EnemyStartPosition;
    this.Velocity.X = EnemyVelocity;
}

public bool HandlePlayerShotCollision(PlayerShot playerShot)
{
    for(int y = 0; y < EnemyRows; y++)
    {
        for(int x = 0; x < EnemyCols; x++)
        {
            // if a player shot hit an enemy, destroy the enemy
            if(enemies[y,x] != null && CheckCollision(playerShot, enemies[y,x]))
            {
                Explosion explosion = new Explosion(this.Content);
                explosion.Position = enemies[y,x].Position;
                explosions.Add(explosion);
                enemies[y,x] = null;
                return true;
            }
        }
    }
    return false;
}

public bool CheckCollision(Sprite s1, Sprite s2)
{
    // simple bounding box collision detection
    return s1.BoundingBox.Intersects(s2.BoundingBox);
}

You will note that this method (and the next two) use the CheckCollision method shown in Example 1-41. This method uses the BoundingBox property we created on our Sprite object earlier, a .NET Rectangle object, and the Intersects method of that Rectangle object to determine whether the two bounding boxes intersect.

Next, let's write the enemy shot/player collision detection method, HandleEnemyShotCollision. In this method, shown in Example 1-42, we enumerate all enemy shots on the screen and see if any of their bounding boxes intersect with the player ship's bounding box.

Example 1-42. HandleEnemyShotCollision method

public bool HandleEnemyShotCollision(Player player)
{
    for(int i = 0; i < enemyShots.Count; i++)
    {
        // if an enemy shot hit the player, destroy the player
        if(CheckCollision(enemyShots[i], player))
        {
            enemyShots.RemoveAt(i);
            return true;
        }
    }
    return false;
}

And finally, we can write the HandleEnemyPlayerCollision method to determine whether any enemy remaining on-screen intersects with the player's ship, as shown in Example 1-43.

Example 1-43. HandleEnemyPlayerCollision method

public bool HandleEnemyPlayerCollision(Player player)
{
    for(int y = 0; y < EnemyRows; y++)
    {
        for(int x = 0; x < EnemyCols; x++)
        {
            // if an enemy hit the player, destroy the enemy
            if(enemies[y,x] != null && CheckCollision(enemies[y,x], player))
            {
                Explosion explosion = new Explosion(this.Content);
                explosion.Position = enemies[y,x].Position;
                explosions.Add(explosion);
                enemies[y,x] = null;
                return true;
            }
        }
    }
    return false;
}

We also need to update and display the explosions list. Update the Update and Draw methods of EnemyGroup as shown in Example 1-44.

Example 1-44. Updated Update and Draw methods

public override void Update(GameTime gameTime)
{
    MoveEnemies(gameTime);
    EnemyFire(gameTime);

    for(int i = 0; i < explosions.Count; i++)
    {
        // update all explosions, remove those whose animations are over
        if(explosions[i].Update(gameTime))
            explosions.RemoveAt(i);
    }
}

public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    // draw all active enemies
    foreach(Enemy enemy in enemies)
    {
        if(enemy != null)
            enemy.Draw(gameTime, spriteBatch);
    }

    // draw all enemy shots
    foreach(EnemyShot enemyShot in enemyShots)
        enemyShot.Draw(gameTime, spriteBatch);

    // draw all explosions
    foreach(Explosion explosion in explosions)
        explosion.Draw(gameTime, spriteBatch);
}

Now we can hook up these methods back in our GameScreen class and even handle scoring. The Update method handles the brunt of the work. Because this method is getting rather large, I'm going to break the following code sections up a bit so I can more easily explain what is happening, but remember that this is all sequential code from the one and only Update method.

First, we need a single member variable to hold an instance of the animation that shows the player ship exploding. To the other member variables, add the line shown in Example 1-45.

Example 1-45. playerExplosion member variable

private Explosion playerExplosion;

Now back to our Update method, shown in Example 1-46.

Example 1-46. Update method, part 1

public GameState Update(GameTime gameTime)
{
    MovePlayer(gameTime);
    UpdatePlayerShots(gameTime);

    // as long as we're not in the lose state, update the enemies
    if(!loseGame)
        enemyGroup.Update(gameTime);

    HandleCollisions(gameTime);

    return GameState.GameScreen;
}

private void HandleCollisions(GameTime gameTime)
{
    // see if a player shot hit an enemy
    for(int i = 0; i < playerShots.Count; i++)
    {
        PlayerShot playerShot = playerShots[i];
        // check the shot and see if it it collided with an enemy
        if(playerShot != null &&
           enemyGroup.HandlePlayerShotCollision(playerShots[i]))
        {
            // remove the shot, add the score
            playerShots.RemoveAt(i);
            score += 100;
            AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Explosion);
        }
    }

    // see if an enemy shot hit the player
    if(player != null && enemyGroup.HandleEnemyShotCollision(player))
    {
        // blow up the player
        playerExplosion = new Explosion(this.contentManager);
        playerExplosion.Position = player.Position;
        player = null;
        AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Explosion);
    }

    // see if an enemy hit the player directly
    if(player != null && enemyGroup.HandleEnemyPlayerCollision(player))
    {
        // blow up the player
        playerExplosion = new Explosion(this.contentManager);
        playerExplosion.Position = player.Position;
        player = null;
        loseGame = true;
        AlienAttackGame.AudioManager.PlayCue(AudioManager.Cue.Explosion);
    }

    // if the player explosion animation is running, update it
    if(playerExplosion != null)
    {
        // if this is the last frame
        if(playerExplosion.Update(gameTime) && !loseGame)
        {
            // remove it
            playerExplosion = null;

            // we lose if we have no lives left
            if(lives == 0)
                loseGame = true;
            else
            {
                // subract 1 life and reset the board
                lives--;
                enemyGroup.Reset();
                playerShots.Clear();
                player = new Player(this.contentManager);
            }
        }
    }
}

The emphasized code in this section calls out a new method named HandleCollisions, which uses our collision detection routines in the EnemyGroup object to determine if an enemy shot hit the player, or if an enemy hit the player directly. If it has, it instantiates a new Explosion object and removes the player from the screen. Note that in the case where the enemy hits the player directly, the game is automatically lost by setting the loseGame variable to true.

The final chunk updates the explosion animation for the destroyed player ship. If the explosion has ended and the player has not lost the game, we check the lives counter. If they have no lives left, they lose; otherwise, we subtract one from the lives counter, reset the screen, and create a new player ship to display on the screen.

The final things to hook up are drawing the player explosion to the screen and drawing the win or lose text. The final Draw method looks like the code in Example 1-47.

Example 1-47. Final GameScreen Draw method

public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
    spriteBatch.Draw(bgScreen, Vector2.Zero, Color.White);

    // draw the player
    if(player != null)
        player.Draw(gameTime, spriteBatch);

    // draw the enemy board
    enemyGroup.Draw(gameTime, spriteBatch);

    // draw the player shots
    foreach(PlayerShot playerShot in playerShots)
        playerShot.Draw(gameTime, spriteBatch);

    // draw the explosion
    if(playerExplosion != null)
        playerExplosion.Draw(gameTime, spriteBatch);

#if ZUNE
    // draw the score
    spriteBatch.DrawString(arial, "Score", new Vector2(0, 0), Color.White);
    spriteBatch.DrawString(arial, score.ToString(), new Vector2(0, 20), Color.White);

    // draw the lives icon
    livesIcon.Draw(gameTime, spriteBatch);
    spriteBatch.DrawString(arial, "x" + lives.ToString(),
                           new Vector2(livesIcon.Position.X + livesIcon.Width + 4,
                                       livesIcon.Position.Y),
                           Color.White);
#else
    // draw the score
    spriteBatch.DrawString(arial, "Score", new Vector2(50, 50), Color.White);
    spriteBatch.DrawString(arial, score.ToString(), new Vector2(50, 80),
                           Color.White);

    // draw the lives icon
    livesIcon.Draw(gameTime, spriteBatch);
    spriteBatch.DrawString(arial, "x" + lives.ToString(),
                           new Vector2(livesIcon.Position.X + livesIcon.Width + 4,
                                       livesIcon.Position.Y+8),
                           Color.White);
#endif

    // draw the proper text, if required
    if(enemyGroup.AllDestroyed())
    {
        Vector2 size = arial.MeasureString("You win!");
        spriteBatch.DrawString(arial, "You win!",
                            new Vector2((AlienAttackGame.ScreenWidth - size.X) / 2,
                                        (AlienAttackGame.ScreenHeight - size.Y) / 2),
                            Color.Green);
    }

    if(loseGame)
    {
        Vector2 size = arial.MeasureString("Game Over");
        spriteBatch.DrawString(arial, "Game Over",
                            new Vector2((AlienAttackGame.ScreenWidth - size.X) / 2,
                                        (AlienAttackGame.ScreenHeight - size.Y) / 2),
                            Color.Red);
    }
}

The updated code here draws the playerExplosion sprite, and then, based on whether the enemy group is destroyed or the player has lost, draws the appropriate text to the center of the screen.

Running the Application

Running the game at this point should produce a fully working game! It's certainly not the most exciting game in the world, but it's a great start to building a game that is. Have at it!

Get Coding4Fun now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.