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.
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. |
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. ASpriteBatch
, 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
andDraw
. For our 2-D game, we will be using the previously discussedSpriteBatch
to draw our sprites to the screen at the positions calculated during ourUpdate
method.
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.
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.
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).
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.
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.
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.
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.
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.
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 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.
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.
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 playerif(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.
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.
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 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; } } } }
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); #endifenemyGroup = 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.
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 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.
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.
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.
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.