All right, you’ve built a solid design and have the start of what could become a pretty cool game. Again, the concept of the game is that a player will control one sprite and try to avoid hitting the chasing sprites as they fly across the screen, while also trying to catch the evading sprites. Now you need to add some scoring and some game logic and do some other fine-tuning to get your game to where you want it.
The first thing you’ll do in this chapter is add some scoring to your game. The first step when you’re writing a game and you start to look at scoring is to decide what events will trigger a change in score. Sometimes scores will change when a weapon of some kind hits the player. At other times, you’ll change the score when the player hits something herself. Still other times, you’ll want to change the score when the user accomplishes something (e.g., answers a question, solves a puzzle, etc.).
In this game, you’re going to change the score when an enemy sprite leaves the screen without having run into the player (meaning that the player successfully avoided that sprite).
It would make sense, given that scoring mechanism, for you to add a score value to each individual sprite. Some sprites might be worth more than others, based on their speed or some other factor that you determine.
In addition to deciding how to calculate the score, you need to be able to draw the score on the screen. We’ll tackle that side of the scoring problem first, and then see how to adjust the score whenever a sprite crosses the screen without hitting the player.
In addition to adding scoring to your game and learning to draw text
using SpriteFont
s in this chapter,
you’ll also add some variety to your sprites by introducing different sprite images and different sounds
for each sprite type. You’ll also add a background image, look at game
states, and add a power-up to the game.
This chapter builds on the code that you finished writing in Chapter 7. Open that game project and use it throughout this chapter.
First, you’ll need to add an integer variable
representing a sprite’s score value to the
Sprite
base class (note the
addition of the public get
accessor
as well, via auto-implemented
properties):
public int scoreValue {get; protected set;}
Modify both constructors in the Sprite
class to receive an integer value for
the score value for that sprite. The first constructor should pass the
value to the second constructor, and the second constructor should use
that value to set the scoreValue
member variable. The constructors for the Sprite
class should now look like this:
public Sprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, int scoreValue) : this(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, defaultMillisecondsPerFrame, collisionCueName, scoreValue) { } public Sprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, int scoreValue) { this.textureImage = textureImage; this.position = position; this.frameSize = frameSize; this.collisionOffset = collisionOffset; this.currentFrame = currentFrame; this.sheetSize = sheetSize; this.speed = speed; this.collisionCueName = collisionCueName; this.millisecondsPerFrame = millisecondsPerFrame; this.scoreValue = scoreValue; }
You’ll also have to change the constructors for the derived
classes (AutomatedSprite
, ChasingSprite
, EvadingSprite
, and UserControlledSprite
) to accept an integer
parameter for the score value and to pass that value on to the base
class constructor. The constructors for those sprites should look like
this:
AutomatedSprite
classpublic AutomatedSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, int scoreValue) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName, scoreValue) { } public AutomatedSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, int scoreValue) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, collisionCueName, scoreValue) { }
ChasingSprite
classpublic ChasingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, SpriteManager spriteManager, int scoreValue) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName, scoreValue) { this.spriteManager = spriteManager; } public ChasingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, SpriteManager spriteManager, int scoreValue) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, collisionCueName, scoreValue) { this.spriteManager = spriteManager; }
EvadingSprite
classpublic EvadingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, SpriteManager spriteManager, float evasionSpeedModifier, int evasionRange, int scoreValue) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName, scoreValue) { this.spriteManager = spriteManager; this.evasionSpeedModifier = evasionSpeedModifier; this.evasionRange = evasionRange; } public EvadingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, SpriteManager spriteManager, float evasionSpeedModifier, int evasionRange, int scoreValue) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, collisionCueName, scoreValue) { this.spriteManager = spriteManager; this.evasionSpeedModifier = evasionSpeedModifier; this.evasionRange = evasionRange; }
UserControlledSprite
classpublic UserControlledSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, null, 0) { } public UserControlledSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, null, 0) { }
The UserControlledSprite
will not have a
score value associated with it, because the player can’t be
awarded points for avoiding himself. Therefore, you won’t need
to add a new parameter to the constructor for this class, but
you will need to pass a 0
to the scoreValue
parameter
of the constructors for the base class.
Finally, in the SpriteManager
class, you’ll need to add the score value as the final parameter in the constructor when
initializing new Sprite
objects.
You’re currently creating objects
only of type EvadingSprite
, and
you’re doing this at the end of the SpawnEnemy
method. Add a zero as the
score value for the EvadingSprite
s
you’re creating. (You’ll be adding some logic later in this chapter that
will create different types of sprites and assign different score values
to those sprites based on their types.) The code that creates your
EvadingSprite
objects in the SpawnEnemy
method should now look like
this:
spriteList.Add( new EvadingSprite (Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, .75f, 150, 0));
You now have a way to calculate the score during the game based on
events with different sprites. Even though you’re currently only using
zeros as the score values, the underlying code is now there, so you can
start to write some scoring logic for the game. First, you’ll need to
add to the Game1
class a variable
that represents the total score of the current game:
int currentScore = 0;
Now you’re ready to draw the score on the screen. Drawing text in
2D is done in a very similar manner to the way that you draw sprites.
For every frame that is drawn, you will draw text on that frame using a
SpriteBatch
and an object called a
SpriteFont
. When drawing 2D images
on the screen, you specify an image file to use and a
Texture2D
object
to hold that image. XNA then takes the image from memory and sends the
data to the graphics card.
The same thing happens when drawing with SpriteFont
objects. In this case, a spritefont
file is created. This is an XML file defining the characteristics of a
given font: font family, font size, font spacing, etc. A SpriteFont
object is used in memory to
represent the spritefont. When the SpriteFont
object is drawn, XNA will build a
2D image using the text you want to draw and the font specified in the
XML file. The image is then sent to the graphics device to be drawn on
the screen.
When drawing the game score, you’ll want to use the value of the
currentScore
variable. As the
variable’s value changes, the score on the screen will be
updated.
To draw text on the screen using a SpriteFont
, you need to add a SpriteFont
resource to your project. First,
add a new resource folder to your project for font objects. Right-click
the AnimatedSpritesContent project in Solution
Explorer and select Add→New Folder. Name
the folder Fonts. Now, add the actual spritefont
resource. Right-click the new
AnimatedSpritesContentFonts folder in Solution
Explorer and select Add→New Item…. On
the left side of the Add New Item window, select the Visual C# category.
Select Sprite Font for the template and name the spritefont
Score.spritefont (see Figure 8-1).
Spritefonts are resources that are picked up and processed by the content pipeline. As such, they must be created within the AnimatedSpritesContent project of your project.
By default, XNA 4.0 uses a font named “Kootenay” when creating a new
spritefont. You can change the type of font used in your spritefont by
editing the file and changing the name of the font found in the line of
code that reads <FontName>Kootenay</FontName>
. It
is recommended that you select a font from the Redistributable Font
Pack, which can be found at http://creators.xna.com/en-us/contentpack/fontpack.
Once you click the Add button, the spritefont file will be created and will open in Visual Studio. You’ll notice that it’s an XML file that contains information such as the font name, size, spacing, style, and so on. You can modify these values to customize your font as needed.
Next, you need to add a SpriteFont
variable, which will store your
spritefont in memory. Add the following class-level variable at the top
of your Game1
class:
SpriteFont scoreFont;
Now, load your SpriteFont
object via the content pipeline, the same way that you’ve loaded most
other game assets. You’ll retrieve the font from the content pipeline
using the Content.Load
method. Add
the following line of code to the LoadContent
method in your Game1
class:
scoreFont = Content.Load<SpriteFont>(@"fontsscore");
The next step is to actually draw the text on the screen. As
mentioned earlier, this is done via a SpriteBatch
object. Instead of using the
SpriteBatch
’s Draw
method, you’ll use a method called
DrawString
to draw text using a
SpriteFont
. Add the following code to
the Draw
method of your Game1
class, between the call to GraphicsDevice.Clear
and the call to base.Draw
:
spriteBatch.Begin( ); // Draw fonts spriteBatch.DrawString(scoreFont, "Score: " + currentScore, new Vector2(10, 10), Color.DarkBlue, 0, Vector2.Zero, 1, SpriteEffects.None, 1); spriteBatch.End( );
Notice that the font needs to be drawn between calls to SpriteBatch.Begin
and SpriteBatch.End
, because XNA treats
spritefonts just like any other 2D image resources.
The DrawString
method
has parameters that are similar to the parameters used in
calls to SpriteBatch.Draw
for 2D
images. You can adjust the position, color, scale, etc. of the font to
be drawn. The first parameter passed into the DrawString
method is the SpriteFont
object you wish to use to draw
with, and the second parameter is the actual text you wish to
draw.
In XNA 2.0, monospaced fonts did not render properly. XNA treated them as regular fonts (losing the benefit of a monospaced font). In XNA 4.0 this issue was fixed, and monospaced fonts now render properly.
Compile and run the game at this point and you should see the score drawn at the upper-left corner of the screen, as shown in Figure 8-2.
Excellent work! You now have an overall game score, and that means you’re one step closer to completing your game. However, the problem you have now is that you never update the game score. Before you do that, you need to add logic to create different types of sprites at random intervals rather than always creating the same type of sprite. Once you’ve implemented those changes, you can add scoring for the different sprite types.
To randomly generate sprites of different types, you first
need to determine the likelihood that each type will be created. Most of
the sprites in this game will be AutomatedSprite
s. ChasingSprite
s will be the next most common,
and EvadingSprite
s will show up only
occasionally. In this section, you’ll be assigning a percentage
likelihood to each type of sprite. Each time a new sprite is spawned,
you’ll determine which type of sprite to create based on those
percentages. The exact percentage likelihood for each sprite type is
something that you can play with as you test your game, and you can
adjust the values to the point where they feel right to you, the
developer.
To begin, open the SpriteManager
class and add three new
class-level variables representing the likelihood that each sprite type
will be spawned:
int likelihoodAutomated = 75; int likelihoodChasing = 20; int likelihoodEvading = 5;
You’ll notice that the values added equal 100 (representing 100%).
Essentially, 75% of the time you’ll generate an AutomatedSprite
, 20% of the time a ChasingSprite
, and 5% an EvadingSprite
.
Now you have to add some code that will generate a random number
and, based on the value of that random number, create one of the three
sprite types. Open your SpriteManager
class and replace this call to SpriteList.Add
, which is at the end of the
SpawnEnemy
method:
spriteList.Add( new EvadingSprite (Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, .75f, 150, 0));
with the following code:
// Get random number int random = ((Game1)Game).rnd.Next(100); if (random < likelihoodAutomated) { // Create AutomatedSprite spriteList.Add( new AutomatedSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0,0), new Point(6, 8), speed, "skullcollision", 0)); } else if (random < likelihoodAutomated + likelihoodChasing) { // Create ChasingSprite spriteList.Add( new ChasingSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0,0), new Point(6, 8), speed, "skullcollision", this, 0)); }else { // Create EvadingSprite spriteList.Add( new EvadingSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, .75f, 150, 0)); }
This code first generates a number between 0 and 99, and then
compares that generated number to the value representing how often an
AutomatedSprite
should be generated.
If the generated number is less than that value, an AutomatedSprite
is created. If the randomly
generated number is not less than that value, it is then compared
against the sum of the value representing how often an AutomatedSprite
should be generated and the
value representing how often a ChasingSprite
should be generated. If the
generated value is less than the sum of those two values, a ChasingSprite
is generated. If it isn’t less
than that value, an EvadingSprite
is
generated.
If that doesn’t make sense, think of it this way: using the values
from our current example, the random number (some value between 0 and
99) is first evaluated to see whether it is less than 75. If it is, an
AutomatedSprite
is generated. This
represents a 75% chance that an AutomatedSprite
will be created, which is
exactly what you want. If the random number is greater than 75, it is
then checked to see whether it is less than 95 (i.e., the sum of the
chance of creating an AutomatedSprite
, which is 75%, and the chance
of creating a ChasingSprite
, which is
20%). Because this comparison would never have been performed if the
random number had been less than 75, this is essentially checking to see
whether the value is between 75 and 94—which represents a 20% chance
that a ChasingSprite
will be created
(again, this is exactly what you want). Finally, an EvadingSprite
will be generated if neither
case is true—in other words, if the random value is between 95 and 99,
of which there is a 5% chance, as you wanted.
Oh yeah! This is all starting to come together. Compile and run
the game, and you should notice that the majority of the sprites
generated are AutomatedSprite
s, some
ChasingSprite
s are mixed in, and an
occasional EvadingSprite
shows up.
Good times!
The problem that your game has at this point is that while the sprites have different behaviors, they all look the same. Not only does that make things somewhat boring, but it’s also confusing to the player because one would expect similar-looking sprites to behave similarly. You need to add some diversity to these different types of sprites.
First things first, you need some more images. If you haven’t already done so, download the source code for this chapter of the book. Within this chapter’s source code (in the AnimatedSpritesAnimatedSpritesContentImages folder), you’ll find some sprite sheet files for the different types of sprites.
Right-click the ContentImages folder in Solution Explorer and select Add→Existing Item…. Navigate to the AnimatedSpritesAnimatedSpritesContentImages folder of the source code for this chapter and add the bolt.png, fourblades.png, threeblades.png, and plus.png images to your project.
In addition to a new image for each of the new sprites, you’ll want some different sounds for collisions with each type. Also with the source code for this chapter, in the AnimatedSpritesAnimatedSpritesContentAudio folder, you’ll find some .wav files. Copy the boltcollision.wav, fourbladescollision.wav, pluscollision.wav, and threebladescollision.wav files from that folder to your own project’s AnimatedSpritesContentAudio folder using Windows Explorer. Remember that when dealing with actual sound files, you don’t add them to the project in Visual Studio. You need to copy them to your project’s AnimatedSpritesContentAudio directory, but you’ll add them to the project using XACT rather than within Visual Studio.
Once you’ve copied the files, start XACT so that you can add these new .wav files to your XACT project file.
In XACT, load the sound project for this game (the file should be called GameAudio.xap and should be located in your project’s AnimatedSpritesContentAudio folder). Open the Wave Bank and Sound Bank windows for your XACT project by double-clicking on the Wave Bank and Sound Bank nodes in the tree menu on the left.
In the Wave Bank window, right-click and select Insert Wave File(s)…. Select the four new .wav files that you’ve copied to your project’s ContentAudio directory, as shown in Figure 8-3.
When you click Open, the .wav files will be added to your Wave Bank window, which should now look something like Figure 8-4.
Next, drag the items you just added from the Wave Bank window into the Cue Name section of the Sound Bank window. You need to drop them in the Cue Name section rather than the Sound Name section because you need cue names for each of these sounds in order to play them from your code. Your window should now have the new sounds listed in the Wave Bank window and in the Sound Name and Cue Name sections of the Sound Bank window, as shown in Figure 8-5.
You can adjust the volume levels of the sounds to your liking, as described in Chapter 6. Once you’re satisfied, save your project, exit XACT, and return to Visual Studio.
Now you’ll need to assign the new images and sounds to the
different types of sprites when they spawn. Luckily, because of the way
you’ve designed your SpriteManager
class, this will be easy. You are already randomly creating different
types of sprites, and now all you need to do is apply the different
images and sounds to those sprites.
You may be wondering about some of the files you’ve just added, because currently you have only three types of sprites. However, there are actually six different sprite sheets in your project. Take a look at Table 8-1 to see the intended use for each of the sprites in this game.
Sprite image | Name | Purpose |
| Used for the player-controlled sprite. | |
| This saw image (as well as the four-blade image) is
used in | |
| This saw image (as well as the three-blade image)
is used in | |
| Used in | |
| Used in | |
| Used in |
In the SpriteManager
class’s
SpawnEnemy
method, you added the code
that randomly generates different types of sprites. You’ll need to
change that code to create one of the five nonplayer sprite types
mentioned in the preceding table. If the random-number comparison
indicates that you need to create an AutomatedSprite
, you’ll need to do another
random calculation to determine whether you’ll be creating a three-blade
object or a four-blade object (both should have a 50%
probability).
Likewise, if a ChasingSprite
is
chosen, you’ll need to do a random calculation to determine whether to
create a plus or a skull ball (again, with a 50% chance for either one).
Finally, if an EvadingSprite
is to be
created, you’ll create the bolt sprite. Replace the following code that
you just added to the end of the SpawnEnemy
method:
// Get random number int random = ((Game1)Game).rnd.Next(100); if (random < likelihoodAutomated) { // Create AutomatedSprite spriteList.Add( new AutomatedSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0,0), new Point(6, 8), speed, "skullcollision", 0)); } else if (random < likelihoodAutomated + likelihoodChasing) { // Create ChasingSprite spriteList.Add( new ChasingSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0,0), new Point(6, 8), speed, "skullcollision", this, 0)); } else { // Create EvadingSprite spriteList.Add( new EvadingSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, .75f, 150, 0)); }
with this code:
// Get random number between 0 and 99 int random = ((Game1)Game).rnd.Next(100); if (random < likelihoodAutomated) { // Create an AutomatedSprite. // Get new random number to determine whether to // create a three-blade or four-blade sprite. if (((Game1)Game).rnd.Next(2) == 0) { // Create a four-blade enemy spriteList.Add( new AutomatedSprite( Game.Content.Load<Texture2D>(@"imagesfourblades"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "fourbladescollision", 0)); } else { // Create a three-blade enemy spriteList.Add( new AutomatedSprite( Game.Content.Load<Texture2D>(@"images hreeblades"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "threebladescollision", 0)); } } else if (random < likelihoodAutomated + likelihoodChasing) { // Create a ChasingSprite. // Get new random number to determine whether // to create a skull or a plus sprite. if (((Game1)Game).rnd.Next(2) == 0) { // Create a skull spriteList.Add( new ChasingSprite( Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, 0)); } else { // Create a plus spriteList.Add( new ChasingSprite( Game.Content.Load<Texture2D>(@"imagesplus"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 4), speed, "pluscollision", this, 0)); } } else { // Create an EvadingSprite spriteList.Add( new EvadingSprite( Game.Content.Load<Texture2D>(@"imagesolt"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "boltcollision", this, .75f, 150, 0)); }
One important thing to note is that the sprite sheet for the plus
ChasingSprite
has six columns and
only four rows, whereas all the others have six columns but eight rows.
If you run your game and the plus sprite animates, disappears, and
reappears, that most likely is the cause of your issue.
Compile and run the game now, and you should see a variety of objects running around the screen with different behaviors, as shown in Figure 8-6. These objects should also make different sounds when they collide with the player.
You probably received a compilation warning about the likelihoodEvading
variable in the AnimatedSprite
class. This variable is assigned but never used. If you look at your
code, you’ll see that the variable really only exists to show the
percentage likelihood of creating an evading sprite. The value isn’t
used in the code. To avoid the warning, you can comment out that line
(don’t remove it, though, as it’s useful for maintenance purposes—this
variable quickly shows the percentage likelihood of an evading sprite
being created).
Next, you’ll add a little more spice to your game by adding a background image. With the source code for this chapter (again, in the AnimatedSpritesAnimatedSpritesContentImages folder), you’ll find an image named background.jpg. Add the image to the project the same way you added the other images (right-click the AnimatedSpritesContentImages folder, select Add→Existing Item…, and navigate to the background.jpg image included with the source code).
Your SpriteManager
class was
built to handle animated sprites and derived classes. Something as
simple as a background image can just be added to your Game1
class. You’ll need to add a Texture2D
variable for the image:
Texture2D backgroundTexture;
and load the Texture2D
image in
the LoadContent
method:
backgroundTexture = Content.Load<Texture2D>(@"Imagesackground");
Next, you’ll need to add the code to draw the image. Because
you’ll now have multiple sprites being drawn within your Game1
class (the SpriteFont
counts as a sprite, so you’ll be
drawing a score sprite as well as a background sprite), you need to make
sure that the score text is always on top of the background image.
Typically, when trying to ensure that one sprite is on top of another,
you modify the SpriteBatch.Begin
call
to include an appropriate SpriteSortMode
. However, this is a case where
you’re drawing only two items, and you know that you’ll always want to
draw the score on top of the background. As such, you can forego the
overhead involved in specifying a sort mode in the Begin
method, and instead always draw the
background first and then the score.
Modify the Draw
method of your
Game1
class to look like this:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.White); spriteBatch.Begin( ); // Draw background image spriteBatch.Draw(backgroundTexture, new Rectangle(0, 0, Window.ClientBounds.Width, Window.ClientBounds.Height), null, Color.White, 0, Vector2.Zero, SpriteEffects.None, 0); // Draw fonts spriteBatch.DrawString(scoreFont, "Score: " + currentScore, new Vector2(10, 10), Color.DarkBlue, 0, Vector2.Zero, 1, SpriteEffects.None, 1); spriteBatch.End( ); base.Draw(gameTime); }
Compile and run the game, and you’ll see the impact that a background image can have on the overall look of the game (Figure 8-7). This is getting exciting—things are really starting to come together!
Nice job. You have a background and multiple types of sprites with varying behaviors. Now let’s take a look at finishing up the game scoring logic.
As you’ll recall from our earlier discussion of this topic, the first thing you need to do is determine what event(s) will trigger a change in score. For this game, you’ll be updating the score whenever the user successfully avoids a three-blade, four-blade, skull ball, or plus sprite. You actually have already added the logic to determine when one of those sprites has been successfully avoided; it lies in the code that deletes the sprites when they disappear off the edge of the screen. If a sprite makes it across the screen and needs to be deleted, that means the user has avoided that sprite, and if it was a three-blade, four-blade, skull ball, or plus sprite, you need to give some points to the user.
Any time you’re developing a game, scoring rules and calculations are things you’ll need to think about. You’ll most likely formulate an idea, implement it, and then tweak it while testing your game to see whether it feels right and plays the way you want it to. For the purposes of this book, the scoring calculations and rules are laid out for you to learn. However, as you begin to feel more comfortable with the concepts in this book and this chapter specifically, feel free to change the rules and tweak the game to whatever feels right to you as both the developer and a player.
In the SpriteManager
class, add
three new class-level variables representing the three types of sprites
you’ll be sending at the player, as well as public properties for each
variable:
int automatedSpritePointValue = 10; int chasingSpritePointValue = 20; int evadingSpritePointValue = 0;
The chasing sprites are tougher than the automated ones, which just move in a straight line across the screen. As such, they are worth more points. The evading objects will be used for power-ups, and whereas the player will want to track them down to gain a performance bonus, there will be no scoring penalty or bonus for not colliding with those sprites.
You now need to add to your Game1
class a public method that will allow
your SpriteManager
to add to the game
score. Because the deletion of sprites takes place in the SpriteManager
, it makes sense to calculate the
score at that point in the program. Add the following method to your
Game1
class:
public void AddScore(int score) { currentScore += score; }
Next, you’ll need to locate the code that deletes the sprites when
they go off the edge of the screen. This code resides in the Update
method of your SpriteManager
class. The method actually has
two different places where sprites are deleted: one for sprites that are
deleted because they have gone off the screen and one for sprites that
are deleted because they have collided with
the player object. Both cases use SpriteList.Remove
At(i)
to remove
the sprite from the list of sprites in the game.
Find the code that removes sprites because they have gone off the edge of the screen. Currently, the code should look something like this:
// Remove object if it is out of bounds if(s.IsOutOfBounds(Game.Window.ClientBounds)) { spriteList.RemoveAt(i); −−i; }
You’ll need to modify the code to add the score for that sprite before removing it. Change the code as shown here (the added line is shown in bold):
// Remove object if it is out of bounds
if(s.IsOutOfBounds(Game.Window.ClientBounds))
{
((Game1)Game).AddScore(spriteList[i].scoreValue);
spriteList.RemoveAt(i);
−−i;
}
So you can verify that you placed the line in the correct place,
your Update
method should now look
something like this (the changed code section is highlighted in
bold):
public override void Update(GameTime gameTime) { // Update player player.Update(gameTime, Game.Window.ClientBounds); // Check to see if it's time to spawn a new enemy nextSpawnTime −= gameTime.ElapsedGameTime.Milliseconds; if (nextSpawnTime < 0) { SpawnEnemy( ); // Reset spawn timer nextSpawnTime = ((Game1)Game).GetRandom.Next( ((Game1)Game).EnemySpawnMinMilliseconds, ((Game1)Game).EnemySpawnMaxMilliseconds); } // Update all sprites for (int i = 0; i < spriteList.Count; ++i) { Sprite s = spriteList[i]; s.Update(gameTime, Game.Window.ClientBounds); // Check for collisions if (s.collisionRect.Intersects(player.collisionRect)) { // Play collision sound if(s.GetCollisionCueName != null) ((Game1)Game).PlayCue(s.GetCollisionCueName); // Remove collided sprite from the game spriteList.RemoveAt(i); −−i; } // Remove object if it is out of bounds if(s.IsOutOfBounds(Game.Window.ClientBounds)) { ((Game1)Game).AddScore(spriteList[i].GetScoreValue); spriteList.RemoveAt(i); −−i; } } base.Update(gameTime); }
The Update
method of your
SpriteManager
class is getting pretty
hairy now, so it’s time for a little refactoring. Create a method called
UpdateSprites
that takes a parameter
of type GameTime
. Remove from the
Update
method the section of code
that updates your sprites (player and nonplayer), and place it in the
UpdateSprites
method. In the place of
the original code in the Update
method, call UpdateSprites
. Your
Update
method should now look like
this:
public override void Update(GameTime gameTime) { // Time to spawn enemy? nextSpawnTime −= gameTime.ElapsedGameTime.Milliseconds; if (nextSpawnTime < 0) { SpawnEnemy( ); // Reset spawn timer ResetSpawnTime( ); } UpdateSprites(gameTime); base.Update(gameTime); }
Ahhh, yes…much better. Your UpdateSprites
method, in turn, should look
like this:
protected void UpdateSprites(GameTime gameTime) { // Update player player.Update(gameTime, Game.Window.ClientBounds); // Update all nonplayer sprites for (int i = 0; i < spriteList.Count; ++i) { Sprite s = spriteList[i]; s.Update(gameTime, Game.Window.ClientBounds); // Check for collisions if (s.collisionRect.Intersects(player.collisionRect)) { // Play collision sound if (s.collisionCueName != null) ((Game1)Game).PlayCue(s.collisionCueName); // Remove collided sprite from the game spriteList.RemoveAt(i); −−i; } // Remove object if it is out of bounds if (s.IsOutOfBounds(Game.Window.ClientBounds)) { ((Game1)Game).AddScore(spriteList[i].scoreValue); spriteList.RemoveAt(i); −−i; } } }
Finally, you’ll need to add the appropriate score values to the
constructors used to create each new sprite. For each AutomatedSprite
that is generated, the final
parameter (which represents the score value for that sprite) should be
the automatedSpritePointValue
member
variable. Likewise, for each ChasingSprite
generated, the final parameter
should be the chasingSpritePointValue
, and the final
parameter for each EvadingSprite
should be the evadingSpritePointValue
property.
You’ll have to change these values in the constructors for each
sprite type in the Spawn
Enemy
method of the SpriteManager
class. To find the constructors
easily, search in the
SpriteManager.cs file for each instance of spriteList.Add
. Each time sprite
List.Add
is called, you’re passing in a
new Sprite
object whose constructor
you’ll need to modify. For clarification purposes, your SpawnEnemy
method should now look something
like this (the only changes are the final parameters in the constructors
for each of the sprite types):
private void SpawnEnemy() { Vector2 speed = Vector2.Zero; Vector2 position = Vector2.Zero; // Default frame size Point frameSize = new Point(75, 75); // Randomly choose which side of the screen to place enemy, // then randomly create a position along that side of the screen // and randomly choose a speed for the enemy switch (((Game1)Game).rnd.Next(4)) { case 0: // LEFT to RIGHT position = new Vector2( -frameSize.X, ((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferHeight - frameSize.Y)); speed = new Vector2(((Game1)Game).rnd.Next( enemyMinSpeed, enemyMaxSpeed), 0); break; case 1: // RIGHT to LEFT position = new Vector2( Game.GraphicsDevice.PresentationParameters.BackBufferWidth, ((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferHeight - frameSize.Y)); speed = new Vector2(-((Game1)Game).rnd.Next( enemyMinSpeed, enemyMaxSpeed), 0); break; case 2: // BOTTOM to TOP position = new Vector2(((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferWidth - frameSize.X), Game.GraphicsDevice.PresentationParameters.BackBufferHeight); speed = new Vector2(0, -((Game1)Game).rnd.Next(enemyMinSpeed, enemyMaxSpeed)); break; case 3: // TOP to BOTTOM position = new Vector2(((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferWidth - frameSize.X), -frameSize.Y); speed = new Vector2(0, ((Game1)Game).rnd.Next(enemyMinSpeed, enemyMaxSpeed)); break; } // Get random number between 0 and 99 int random = ((Game1)Game).rnd.Next(100); if (random < likelihoodAutomated) { // Create an AutomatedSprite. // Get new random number to determine whether to // create a three-blade or four-blade sprite. if (((Game1)Game).rnd.Next(2) == 0) { // Create a four-blade enemy spriteList.Add( new AutomatedSprite( Game.Content.Load<Texture2D>(@"imagesfourblades"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "fourbladescollision", automatedSpritePointValue)); } else { // Create a three-blade enemy spriteList.Add( new AutomatedSprite( Game.Content.Load<Texture2D>(@"imageshreeblades"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "threebladescollision", automatedSpritePointValue)); } } else if (random < likelihoodAutomated + likelihoodChasing) { // Create a ChasingSprite. // Get new random number to determine whether // to create a skull or a plus sprite. if (((Game1)Game).rnd.Next(2) == 0) { // Create a skull spriteList.Add( new ChasingSprite( Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, chasingSpritePointValue)); } else { // Create a plus spriteList.Add( new ChasingSprite( Game.Content.Load<Texture2D>(@"images lus"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 4), speed, "pluscollision", this, chasingSpritePointValue)); } } else { // Create an EvadingSprite spriteList.Add( new EvadingSprite( Game.Content.Load<Texture2D>(@"images·olt"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "boltcollision", this, .75f, 150, evadingSpritePointValue)); } }
Oh yeah! Compile and run the game now, and you’ll see that as the sprites are successfully avoided and move off the screen, the point values for those sprites are added to the game score, as shown in Figure 8-8.
Awesome! You have some sprites running around, and the game actually keeps score! You’re all done now, right? Er…uh, wait a minute…the game never ends. That means every time you play you can potentially get a high score by just sitting there and watching. Hmmm, on second thought, we have a ways to go. Let’s add some logic to create different game states and end the game when a player gets hit a given number of times.
Your game is coming along, but there has to be a way to end the game. Typically, when a game ends, the game window doesn’t just disappear; usually there’s some kind of game-over screen that displays your score or at least lets you know that you’ve failed (or succeeded) in your mission. That’s what you need to add next. While you’re at it, it’s also common to have the same kind of thing at the beginning of the game (perhaps a menu enabling the player to select options, or at least a splash screen presenting instructions and maybe displaying your name as the author of this great game). In the following sections, you’ll add both an introductory splash screen and a closing game-over screen.
Throughout the life of any game, the game will go through different states. Sometimes these states indicate that the player has moved to a different level in the game or a different area. Sometimes the game state depicts a status change for a player (like in Pac-Man, when you turn on the ghosts and begin to chase them rather than being chased). Regardless of the specifics, the game moves through different states, and in those different states the game behaves differently. One way to implement splash screens and game-over screens is by making use of these states.
To define some states for your game, you’ll need to enumerate the
different possible states that the game can have. Create an enum
variable at the class level in your
Game1
class. Currently, you have only
three states in your game: Start
(where you display your splash screen), InGame
(where the game is actually running),
and GameOver
(where you’ll display
your game over screen). You’ll also need to create a variable of that
enum
type that will hold the current
state of the game. You’ll want to initialize that current state variable
to the game state representing the start of the game:
enum GameState { Start, InGame, GameOver }; GameState currentGameState = GameState.Start;
Currently in your Game1
class,
you have Update
and Draw
methods that let you draw things on the game screen and update
objects in the game. When you place code in one of those methods (such
as code to draw the score and the background image), that code runs
every time the method is called (i.e., in every frame throughout the
life of the game). You’re going to want to separate the logic in the
Update
and Draw
methods to allow you to write specific
code that will run only in certain situations, depending on the current
state of the game. You can do this by adding a switch
statement to both methods with
different case
statements for each
possible game state. Then, when you want to write specific code to
update or draw items that should take place only in a given game state,
you add that code to the Update
or
Draw
methods within the case for that
particular game state.
First, add a switch
statement
to the Update
method of your Game1
class. The Update
method should now look like
this:
protected override void Update(GameTime gameTime) { // Only perform certain actions based on // the current game state switch (currentGameState) { case GameState.Start: break; case GameState.InGame: break; case GameState.GameOver: break; } // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit( ); audioEngine.Update( ); base.Update(gameTime); }
Next, do the same thing with the Draw
method. Your Draw
method already has logic in it to draw
the score and the background image, but this stuff should be drawn only
when the game is in the GameState.InGame
state, so you’ll need to put
that code in the appropriate case of the switch
statement. Your Draw
method should now look like this:
protected override void Draw(GameTime gameTime) { // Only draw certain items based on // the current game state switch (currentGameState) { case GameState.Start: break; case GameState.InGame: GraphicsDevice.Clear(Color.White); spriteBatch.Begin( ); // Draw background image spriteBatch.Draw(backgroundTexture, new Rectangle(0, 0, Window.ClientBounds.Width, Window.ClientBounds.Height), null, Color.White, 0, Vector2.Zero, SpriteEffects.None, 0); // Draw fonts spriteBatch.DrawString(scoreFont, "Score: " + currentScore, new Vector2(10, 10), Color.DarkBlue, 0, Vector2.Zero, 1, SpriteEffects.None, 1); spriteBatch.End( ); break; case GameState.GameOver: break; } base.Draw(gameTime); }
If you were to compile and run the application at this point, it would look kind of cool but a bit messed up. The score and background would be missing from the game window, and the animated sprites would not be erased from frame to frame, which would result in trails being left for the animations.
The score and background would be missing because the current game
state is set to GameState.Start
by
default, and in that game state you aren’t drawing those items.
Likewise, you’d see the trails because you don’t call GraphicsDevice.Clear
in the GameState.Start
state (you only do that in the
GameState.InGame
state).
The reason you’d still see your animated sprites is because the
SpriteManager
class isn’t affected by
the game state logic you just added. You only added that code to the
Game1
class; the SpriteManager
is a game component and is not
affected by the switch
statement you
just added.
To get all of this to work correctly, you’ll need to add some
logic to disable your SpriteManager
game component in certain game states and enable it in other
states.
By default, when you create an instance of a GameComponent
and add it to the list of
components in a game, the GameComponent
is wired into the game loop.
When the game’s Update
method is
called, so is the Update
method of
the GameComponent
, and so on.
There are two properties that can be used to enable and disable a
GameComponent
. The Enabled
property of a GameComponent
will
determine whether its Update
method
is called when the game’s own Update
method is called. Likewise, the Visible
property of a DrawableGameComponent
will determine whether
its Draw
method is called when the
game’s Draw
method is called. Both of
these properties are set to true
by
default. Go to the Initialize
method
in your Game1
class and set both
properties to false
immediately after
adding the component to your list of game components (added lines are in
bold):
spriteManager = new SpriteManager(this); Components.Add(spriteManager); spriteManager.Enabled = false; spriteManager.Visible = false;
Why start the SpriteManager
in a disabled state? Remember that the game starts in
the GameState.Start
state, which
will be used for a splash screen of some sort. You’re not going to
want sprites flying in and out of the screen at this point in the
game. Hence, you’ll start the game with a disabled SpriteManager
, and then, when the splash
screen closes, you’ll move to a game playing state and activate the
SpriteManager
.
Next, add some code to show some text when the game is in the
GameState.Start
state. This will
serve as your splash screen, and you can add graphics, text, and even
animations or other effects to it, just as you would during the game
itself. For now, you’ll just be adding some simple text that will tell
the user that he needs to avoid the blade objects. In your Draw
method, add to the GameState.Start
case of your switch
statement some code to display these
simple instructions to the user:
case GameState.Start: GraphicsDevice.Clear(Color.AliceBlue); // Draw text for intro splash screen spriteBatch.Begin( ); string text = "Avoid the blades or die!"; spriteBatch.DrawString(scoreFont, text, new Vector2((Window.ClientBounds.Width / 2) - (scoreFont.MeasureString(text).X / 2), (Window.ClientBounds.Height / 2) - (scoreFont.MeasureString(text).Y / 2)), Color.SaddleBrown); text = "(Press any key to begin)"; spriteBatch.DrawString(scoreFont, text, new Vector2((Window.ClientBounds.Width / 2) - (scoreFont.MeasureString(text).X / 2), (Window.ClientBounds.Height / 2) - (scoreFont.MeasureString(text).Y / 2) + 30), Color.SaddleBrown); spriteBatch.End( ); break;
This code should be pretty straightforward; there’s nothing you
haven’t seen before, other than the use of the SpriteFont
’s MeasureString
method. This method will return a Vector2
object indicating the size of the
string being measured. This is helpful when trying to center a
spritefont in the middle of the screen, which is exactly what is being
done in this code. To center the text exactly in the middle of the
screen, you divide the value of the Window.ClientBounds.Width
property by two to
find the horizontal middle of the screen, and then offset the text by
subtracting half the width of the spritefont text you’re about to draw.
You determine the width of the text you’re about to draw by using the
SpriteFont.MeasureString
method.
If you compile and run the code at this point, you’re going to be
somewhat disappointed. After all the work you’ve put into this game, it
doesn’t work! All you have now is a message telling you to avoid the
blades or die; worse yet, the game screen says to press any key to get
started, but no matter how hard you press those keys, nothing happens.
That’s because you haven’t yet added any functionality to move from the
GameState.Start
state to the GameState.InGame
state.
To move to the GameState.InGame
state, add some code to the
GameState.Start
case of the switch
statement in the Update
method of the Game1
class. The following code will detect
any key presses from the user and, when the player presses a key, change
the game to the GameState.InGame
state and activate your SpriteManager
, which will allow sprites to
start flying around the screen:
case GameState.Start: if (Keyboard.GetState().GetPressedKeys( ).Length > 0) { currentGameState = GameState.InGame; spriteManager.Enabled = true; spriteManager.Visible = true; } break;
If you wanted to, you could also add support here for the player clicking a mouse button or pressing a button on the gamepad to start the game. In this case, you’d probably want to instruct the player to press any key, click a mouse button, or press a button on the gamepad to continue. It’s always a good idea to let players know what controls they can use so they don’t have to guess. Making players guess will always lead to unsatisfied gamers.
Compile and run the application now, and you’ll see a very simple splash screen (shown in Figure 8-9) that disappears when you press any key, at which point the game begins. Great job!
Now that you have a fancy, schmancy splash screen, it’s time to add the same type of screen at the end of the game. Before you do that, however, you’ll need to add logic that will actually make the game end.
So, now you have to determine how your game will end. You already have an objective for the game: avoid the three- and four-blade sprites. But when is the game actually over? It seems a bit rough to end the game as soon as the user hits a single blade sprite. Instead, it might make the game a bit more enjoyable if the player had a certain number of lives to play with.
To accomplish this, first you’ll need to create a class-level
variable in your Game1
class to keep
track of the number of lives remaining, as well as a public property
with get
and set
accessors to allow the SpriteManager
to access and modify the
value:
int numberLivesRemaining = 3; public int NumberLivesRemaining { get { return numberLivesRemaining; } set { numberLivesRemaining = value; if (numberLivesRemaining == 0) { currentGameState = GameState.GameOver; spriteManager.Enabled = false; spriteManager.Visible = false; } } }
Notice that when the property is set, its value is assigned to the
numberLivesRemaining
variable, and
then that variable is checked to see whether its value is zero. If the
value is zero, the game state is changed to GameState.GameOver
and the SpriteManager
is
disabled and hidden. This allows you to decrement this value from the
Sprite
Manager
class and then,
when the player is out of lives, have the game automatically shut down
and enter a state in which you can display a game-over screen.
Now, not only do you want to keep track of the number of lives that a player has, but the player also needs to be able to see how many lives he has left at any given time.
Why show the number of lives remaining on the screen?
Again, this comes down to trying to make the game a more enjoyable experience for the player. If the player has to constantly keep track of the number of lives she has left on her own, it will detract from the gameplay experience. Anything you can do to help the player out by displaying important data (such as the score and the number of lives remaining) will go a long way toward letting her focus on the most important thing: having fun playing your game.
To display the number of lives remaining, you’ll draw one animated three rings sprite in the upper-left corner of the screen (below the score) for each life that the player has remaining.
To avoid confusion, you won’t want the sprites to be the same size
as the actual sprite being controlled by the player, so you’ll have to
add some code that will allow you to scale the sprites. Because these
sprites won’t move on their own and the player won’t interact with them,
you can use the AutomatedSprite
class
and specify a speed of (0, 0) to draw these objects.
In the Sprite
class, add a
class-level variable to represent the scale at which the sprite is
supposed to be drawn:
protected float scale = 1;
Specifying a scale value of 1
will cause the object to be drawn at the original size of the sprite, so
you should initialize it to that value. Next, you’ll need to change the
Draw
method in your Sprite
class to use your newly added Scale
variable for the scale parameter. Your Draw
method should look like this:
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch) { spriteBatch.Draw(textureImage, position, new Rectangle(currentFrame.X * frameSize.X, currentFrame.Y * frameSize.Y, frameSize.X, frameSize.Y), Color.White, 0, Vector2.Zero, scale, SpriteEffects.None, 0); }
Finally, you’ll need to add to the Sprite
class a new constructor that will
accept a scale value as a parameter:
public Sprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, int scoreValue, float scale) : this(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, defaultMillisecondsPerFrame, collisionCueName, scoreValue) { this.scale = scale; }
and add to the AutomatedSprite
class a new constructor that will accept a scale parameter and pass that
value on to the base class:
public AutomatedSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, int scoreValue, float scale) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName, scoreValue, scale) { }
Your AutomatedSprite
class is
now ready to be used to create the sprites that will display the number
of lives remaining for the player. In the SpriteManager
class, add a class-level
variable to keep track of the sprites used for player lives:
List<AutomatedSprite> livesList = new List<AutomatedSprite>( );
In the SpriteManager
’s LoadContent
method, you’ll need to fill the
livesList
list with a number of sprites equaling the number of lives a
player begins with. In each frame, you’ll draw the list of items in the
livesList
variable in the upper-left
corner of the screen. This will be a visual indicator to show the player
how many lives she has remaining. To fill the list, create a loop that
runs as many times as the player has lives, adding a new AutomatedSprite
object to the list each time
through the loop:
for (int i = 0; i < ((Game1)Game).NumberLivesRemaining; ++i) { int offset = 10 + i * 40; livesList.Add(new AutomatedSprite( Game.Content.Load<Texture2D>(@"images hreerings"), new Vector2(offset, 35), new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), Vector2.Zero, null, 0, .5f)); }
The only complex thing going on in this code is the second
parameter, which represents the position of the sprite. The parameter is
of the type Vector2
. Your goal in
this list is to create a set of sprites that do not move and that are
staggered in a row across the upper-left corner of the screen. The X
portion of the parameter is first offset by 10 (so that the leftmost
image is offset slightly from the edge of the screen) and then
multiplied by 40 (so each image is drawn 40 units to the right of the
previous image). The Y portion of the parameter is set to 35, to offset
it just below the score text.
Now all that’s left to do is update your livesList
objects each time Update
is called in the SpriteManager
class and draw your objects each
time Draw
is called.
To do this, add the following code at the end of the UpdateSprites
method in your SpriteManager
class:
foreach (Sprite sprite in livesList) sprite.Update(gameTime, Game.Window.ClientBounds);
and add the following code to the Draw
method, just above the call to spriteBatch.End
:
foreach (Sprite sprite in livesList) sprite.Draw(gameTime, spriteBatch);
Compile and run the game at this point, and you’ll see three sprites in the upper-left corner of the screen, indicating the number of lives the player has left (see Figure 8-10).
Good job! Now you just need to add some logic to remove a life when the player collides with a blade sprite and to display a game-over screen when the game ends.
Removing one of the life sprites is pretty straightforward. You
have code that detects collisions between the player and the moving
sprites on the screen. When such a collision occurs, you need to check
the type of the sprite that collided with the player: if the type is
AutomatedSprite
, you’ll remove a life
sprite from the end of the list of life sprites. Make sure you remove
the sprite from the end of the list, because you inserted them in order
from left to right.
In addition to removing a sprite from the list of sprites
representing lives remaining, you’ll need to decrement the value of the
numberLivesRemaining
variable from
the Game1
class by using its
accessor.
The code for the collision checks is located in your SpriteManager
’s UpdateSprites
method. In that method, you have
logic to remove sprites in two cases: when a sprite collides with the
player and when a sprite leaves the game screen. Both cases use the
spriteList.RemoveAt
method to remove
the sprite from the game. Search for the two instances of spriteList.RemoveAt
within the UpdateSprites
method of the Sprite
Manager
class, and find the one used for
collisions (you’ll see code used to play collision sounds nearby). Add
the following code to the method, just before the code to remove the
sprite when a collision occurs:
if (s is AutomatedSprite) { if (livesList.Count > 0) { livesList.RemoveAt(livesList.Count - 1); --((Game1)Game).NumberLivesRemaining; } }
For clarity, the entire UpdateSprites
method is posted here, with the
added code marked in bold so you can see exactly where to put that
code:
protected void UpdateSprites(GameTime gameTime) { // Update player player.Update(gameTime, Game.Window.ClientBounds); // Update all nonplayer sprites for (int i = 0; i < spriteList.Count; ++i) { Sprite s = spriteList[i]; s.Update(gameTime, Game.Window.ClientBounds); // Check for collisions if (s.collisionRect.Intersects(player.collisionRect)) { // Play collision sound if (s.collisionCueName != null) ((Game1)Game).PlayCue(s.collisionCueName); // If collided with AutomatedSprite, // remove a life from the player if (s is AutomatedSprite) { if (livesList.Count > 0) { livesList.RemoveAt(livesList.Count - 1); --((Game1)Game).NumberLivesRemaining; } } // Remove collided sprite from the game spriteList.RemoveAt(i); −−i; } // Remove object if it is out of bounds if (s.IsOutOfBounds(Game.Window.ClientBounds)) { ((Game1)Game).AddScore(spriteList[i].scoreValue); spriteList.RemoveAt(i); −−i; } } // Update lives-list sprites foreach (Sprite sprite in livesList) sprite.Update(gameTime, Game.Window.ClientBounds); }
If you run the game now, you’ll notice that a life is removed
every time you run into a three- or four-blade sprite. When all your
lives are used up, the game will appear to freeze. It actually isn’t
frozen, though; it’s simply entered a game state in which you aren’t
doing anything (GameState.GameOver
).
The last step in this section is to create a game-over screen, similar
to the splash screen you created earlier.
First, in the Update
method of
the Game1
class, add some code that
will allow the player to close the game window when the game is in the
game-over state. Here, you’ll close the game when the player presses the
Enter key. Add the following code to detect when the Enter key is
pressed and to call the Exit( )
method, which will shut down the game entirely (if you added support for
starting the game by pressing a mouse or gamepad button, you should
probably add similar input support here to close the game as
well):
case GameState.GameOver: if (Keyboard.GetState( ).IsKeyDown(Keys.Enter)) Exit( ); break;
To clarify, your Game1
class’s
Update
method should now look
something like this (changes are in bold):
protected override void Update(GameTime gameTime) { // Only perform certain actions based on // the current game state switch (currentGameState) { case GameState.Start: if (Keyboard.GetState().GetPressedKeys().Length > 0) { currentGameState = GameState.InGame; spriteManager.Enabled = true; spriteManager.Visible = true; } break; case GameState.InGame: break; case GameState.GameOver: if (Keyboard.GetState().IsKeyDown(Keys.Enter)) Exit(); break; } // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); //Update audio audioEngine.Update(); base.Update(gameTime); }
Now add some code that will draw a game-over message when the game
is in the game-over state. Of course, because you’re going to be
drawing, you’ll do this in the Draw
method of the Game1
class. In the
game-over case
of the switch
statement, add the following
code:
case GameState.GameOver: GraphicsDevice.Clear(Color.AliceBlue); spriteBatch.Begin( ); string gameover = "Game Over! The blades win again!"; spriteBatch.DrawString(scoreFont, gameover, new Vector2((Window.ClientBounds.Width / 2) - (scoreFont.MeasureString(gameover).X / 2), (Window.ClientBounds.Height / 2) - (scoreFont.MeasureString(gameover).Y / 2)), Color.SaddleBrown); gameover = "Your score: " + currentScore; spriteBatch.DrawString(scoreFont, gameover, new Vector2((Window.ClientBounds.Width / 2) - (scoreFont.MeasureString(gameover).X / 2), (Window.ClientBounds.Height / 2) - (scoreFont.MeasureString(gameover).Y / 2) + 30), Color.SaddleBrown); gameover = "(Press ENTER to exit)"; spriteBatch.DrawString(scoreFont, gameover, new Vector2((Window.ClientBounds.Width / 2) - (scoreFont.MeasureString(gameover).X / 2), (Window.ClientBounds.Height / 2) - (scoreFont.MeasureString(gameover).Y / 2) + 60), Color.SaddleBrown); spriteBatch.End( ); break;
This code will draw three lines of text on the screen: a message indicating that the game is over, a message indicating that to exit the player should press the Enter key, and a message showing the player’s final score. The game-over screen will look something like Figure 8-11.
With any game that you develop, you will want to tweak things during game testing to ensure that the game plays the way that you intend and is challenging but fun at the same time. The biggest factor is to make sure that the game is entertaining to play. If you’re just making the game for yourself, obviously that will be your call. If, however, you’re developing it for a wider audience, it’s important to get feedback from that user base sooner rather than later.
In this case, one thing you might want to tweak is related to the mouse movement that you’ve built into the game. You may have noticed that playing with the mouse is much easier than playing with the keyboard keys. To make the game more challenging and to force the user to use an input form that maintains a constant speed for the player sprite, try removing mouse support (I’d recommend leaving support for the gamepad and keyboard input in place).
To remove support for the mouse, comment out or delete the
mouse-movement code located in the Update
method of the UserControlledSprite
class:
// COMMENTED-OUT MOUSE SUPPORT // If the mouse moved, set the position of the sprite to the mouse position // MouseState currMouseState = Mouse.GetState( ); // if (currMouseState.X != prevMouseState.X || // currMouseState.Y != prevMouseState.Y) // { // position = new Vector2(currMouseState.X, currMouseState.Y); // } // prevMouseState = currMouseState;
You should also comment out the
class-level prevMouseState
variable
in the User
ControlledSprite
class:
// COMMENTED-OUT MOUSE SUPPORT // MouseState prevMouseState;
Prior to removing the mouse support for the game, the initial
player sprite position was set to the position of the mouse cursor. That
won’t work anymore, so you’ll want to start
the player in the middle of the screen. You create the player
object in the Load
Content
method of the SpriteManager
class, and in the constructor
for the player
object you pass in
Vector2.Zero
as the parameter for the
position of the object (the second parameter in the list). Change that
code so you pass in the middle of the screen as the initial position of
the player
object. Your
initialization code for the player
object in the LoadContent
method of
the SpriteManager
class should now
look like this:
player = new UserControlledSprite( Game.Content.Load<Texture2D>(@"Images/threerings"), new Vector2(Game.Window.ClientBounds.Width / 2, Game.Window.ClientBounds.Height / 2), new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), new Vector2(6, 6));
Another aspect of the gameplay experience that you’ll probably want to tweak is to make the game increasingly more difficult to play. As the game stands at this point, players can play virtually forever because the game just isn’t very challenging.
How do you make the game more difficult? Well, there are a lot of ways. You could make the blade sprites in the game move progressively faster, or you could spawn different types of sprites that are more and more difficult to avoid. Or you could use a combination of those approaches, or do something totally different. The key here is to be creative. This is video game development, and fresh and new ideas are what make great games. Feel free to play with the game and think about what you could do to make the experience more entertaining.
For the purposes of this book, we’re going to make the sprites
spawn more and more often in order to make the game progressively
harder. You already have two variables that determine a minimum and
maximum spawn time for each new sprite (enemySpawnMinMilliseconds
and enemySpawnMaxMilliseconds
in the Game1
class). These variables are set to 1,000
and 2,000 milliseconds, respectively (in other words, a new sprite is
spawned every 1 to 2 seconds).
You don’t want to decrease the spawn times every frame, because
with the game running at 60 frames per second, the rate of change would
be too quick to make things interesting. Instead, create a couple of new
class-level variables in the SpriteManager
class that you can use to
decrease the spawn time every so often (in this case, every
second):
int nextSpawnTimeChange = 5000; int timeSinceLastSpawnTimeChange = 0;
These variables may look familiar because this is the same concept
you used when experimenting with animation speeds. Basically, you’ll add
some code in the Update
method of the Game1
class that will add the elapsed time to
the timeSinceLastSpawn
TimeChange
variable. When
that variable’s value is greater than the value of the nextSpawnTimeChange
variable (which will occur after every 5 seconds of gameplay because nextSpawnTimeChange
is set to 5,000
milliseconds), you’ll decrease the values of both of the spawn timer variables
(enemySpawnMinMilliseconds
and
enemySpawn
Max
Milliseconds
).
However, you don’t want to decrease these values indefinitely. If the spawn time values reached zero, a new sprite would be generated every frame—that’s 60 sprites generated every second. There’s no way anybody could ever keep up with that. To avoid this scenario, you’ll cap off the spawn time at 500 milliseconds.
Create a new method in the SpriteManager
class that will adjust the
spawning frequency variables, making enemy sprites spawn more and more
frequently as the game progresses:
protected void AdjustSpawnTimes(GameTime gameTime) { // If the spawn max time is > 500 milliseconds, // decrease the spawn time if it is time to do // so based on the spawn-timer variables if (enemySpawnMaxMilliseconds > 500) { timeSinceLastSpawnTimeChange += gameTime.ElapsedGameTime.Milliseconds; if (timeSinceLastSpawnTimeChange > nextSpawnTimeChange) { timeSinceLastSpawnTimeChange −= nextSpawnTimeChange; if (enemySpawnMaxMilliseconds > 1000) { enemySpawnMaxMilliseconds −= 100; enemySpawnMinMilliseconds −= 100; } else { enemySpawnMaxMilliseconds −= 10; enemySpawnMinMilliseconds −= 10; } } } }
In this code, the interior if
/else
statement causes the spawning increments to be decreased rapidly
(subtracting 100 each time the spawn times are decremented) until the
max spawn time is less than 1 second (1,000 milliseconds). After that,
the spawn time continues to decrease until it reaches a max spawn time
of 500 milliseconds, but it decreases at a much slower rate (subtracting
only 10 each time the spawn times are decremented). Again, this is just
another part of tweaking the gameplay experience. This particular method
of changing the spawn time will cause the game to get tougher and more
interesting fairly quickly, and then slowly get harder and harder until
the game is tough enough to pose a challenge to most players.
You’ll need to add a call to the new AdjustSpawnTimes
method from within the
Update
method of your SpriteManager
class. Add the following line to
the Update
method immediately before
the call to base.Update
:
AdjustSpawnTimes(gameTime);
Compile and run the game at this point, and you’ll find that the longer you stay alive, the harder the game will become. At some point, you will no longer be able to keep up, and the game will end. See Figure 8-12 for a view of how convoluted the game can get once it really starts rolling.
We’ll add one last thing to the game in this chapter, and then it will be up to you to fine-tune and tweak it further. You have three sprites that don’t do anything at this point: the skull ball, the plus, and the bolt. These sprites are meant not to take away a player’s life when they collide with the player’s object, but rather to have some positive or negative effect on the player.
As in previous sections in this chapter, we’re really getting into the creative aspect of game development here, and if there’s something you feel would add a great deal of entertainment to the game, you should look at adding that functionality. As you consider implementing things like power-ups, there really is no limit to what you can do.
For the purposes of this book, the effects that these objects will have when they collide with the player object are as follows:
Essentially, a power-up (or power-down, if the effect is negative to the player) will run for a given number of seconds, and then the effect will wear off. In this game, the effects will stack, meaning that you can be influenced by more than one effect at a given time, and you can be influenced by the same effect multiple times (for instance, if you hit two skull balls back to back, you’ll be running at 25% of your normal speed).
Should the effects always stack? That’s completely your call. You’re the developer, and you own this virtual world. You want to make the game fun to play and challenging. Again, experiment with different effects and decide what you like and what works for you.
To create these effects, you’ll need to add some code to your
Sprite
base class. First, you’ll need
to add a variable that will hold the initial value of the scale
property. Currently, you’re initializing
scale
to 1
, but if the player collides with a plus
sprite, the value of scale
will
double. After that power-up expires, you’ll have to set scale
back to its original value. Rather than
hardcoding the value 1
in at that
point, it would be better to use a variable representing the original
value. Add the following class-level variable to the Sprite
class:
protected float originalScale = 1;
Next, you’ll need to add a method that will allow the SpriteManager
to increase or decrease the
value of the scale
variable, as well
as a method that the SpriteManager
can call to reset the scale
variable
to its original value. Add the following methods to the Sprite
class:
public void ModifyScale(float modifier) { scale *= modifier; } public void ResetScale( ) { scale = originalScale; }
Pause here for a second and answer this question: what effect will
changing the scale have on your collision detection? That’s right, it’s
going to get messed up. You’ll have changed the scale, but the collision
detection will still be using the frameSize
and collisionOffset
properties, which hold the
original values for the size of the frame and the collision offset. What
can you do about this? Luckily, your SpriteManager
uses an accessor called CollisionRect
that builds a Rectangle
for collision detection. You can
modify that accessor to use the scale
property to correctly build the Rectangle
when a different scale
factor is applied. Change the collisionRect
property in your Sprite
base class as follows:
public Rectangle collisionRect { get { return new Rectangle( (int)(position.X + (collisionOffset * scale)), (int)(position.Y + (collisionOffset * scale)), (int)((frameSize.X - (collisionOffset * 2)) * scale), (int)((frameSize.Y - (collisionOffset * 2)) * scale)); } }
Now you’ll need to do the same sort of thing for the Speed
variable. First, add to the Sprite
class a class-level variable that will
hold the original speed:
Vector2 originalSpeed;
Because you’re initializing speed
in the constructors via a parameter
rather than instantiating it to a constant value, you don’t need to
initialize the originalSpeed
variable
to anything specific. You will, however, need to initialize it in the
constructor that sets the speed
variable. Add the following code just below the line in the constructor
that reads this.speed = speed
:
originalSpeed = speed;
Next, add two methods (just as you did for the scale
variable) that will allow the SpriteManager
to modify and reset the Speed
variable of the Sprite
class:
public void ModifySpeed(float modifier) { speed *= modifier; } public void ResetSpeed( ) { speed = originalSpeed; }
Oh yeah, that’s good stuff so far. Now you’re ready to make the
final changes. Open the SpriteManager
class, as you’ll be making some changes in that class next.
Every time the player hits one of these special sprites, a
power-up timer needs to be set to
let that power-up run for 5 seconds. Add a class-level variable
to the Sprite
Manager
class to keep track of when
power-ups should expire:
int powerUpExpiration = 0;
Next, find the UpdateSprites
method. In that method, you check to see whether the player has collided with a sprite.
Previously, you added a check to see whether a collision with an AutomatedSprite
object had occurred and, if
so, you removed a life from the
player. Now you’re going to handle the other types of
collisions that are possible. The
easiest way to determine the type of sprite that was hit if it wasn’t an
AutomatedSprite
is by examining the CollisionCue
property of the sprite. Remember that a sprite’s CollisionCue
property determines
which sound is played in a collision with that object, and therefore
that sound property is unique for each different type of sprite.
Your code should have an if
statement that looks like the following:
if (s is AutomatedSprite) { if (livesList.Count > 0) { livesList.RemoveAt(livesList.Count - 1); --((Game1)Game).NumberLivesRemaining; } }
Add the following code after that first if
statement (the if(s is AutomatedSprite)
statement, not the
if(LivesList.Count > 0)
statement)
to create the power-up effect and set the power-up expiration timer to 5
seconds (5,000 milliseconds):
else if (s.collisionCueName == "pluscollision") { // Collided with plus - start plus power-up powerUpExpiration = 5000; player.ModifyScale(2); } else if (s.collisionCueName == "skullcollision") { // Collided with skull - start skull power-up powerUpExpiration = 5000; player.ModifySpeed(.5f); } else if (s.collisionCueName == "boltcollision") { // Collided with bolt - start bolt power-up powerUpExpiration = 5000; player.ModifySpeed(2); }
This code will perform some action that starts a power-up effect
on the player based on the collision cue of the colliding sprite. In
addition, it will set the powerUpExpiration
variable to 5000
to indicate that the power-up timer has
started and has 5 seconds remaining.
The last thing you’ll need to do is decrement the expiration timer
every time the Update
method of the
SpriteManager
class is called. Add
the following method to the SpriteManager
class:
protected void CheckPowerUpExpiration(GameTime gameTime) { // Is a power-up active? if (powerUpExpiration > 0) { // Decrement power-up timer powerUpExpiration −= gameTime.ElapsedGameTime.Milliseconds; if (powerUpExpiration <= 0) { // If power-up timer has expired, end all power-ups powerUpExpiration = 0; player.ResetScale( ); player.ResetSpeed( ); } } }
Then, call that method from within your Update
method, just before the base.Update
call:
CheckPowerUpExpiration(gameTime);
This new method will subtract the elapsed game time from the power-up timer at every frame, and when the power-up timer reaches zero, it will reset the scale and speed of the player.
Compile and run the game now, and you’ll find that you have a complete 2D game written in XNA. It’s basic, but pretty fun. Again, you can tweak gameplay issues such as speeds, spawn rates, power-ups, and so on to your liking and customize the game to make it play the way you feel it should.
While playing, notice how the power-up effects stack. For example, if you hit a plus sprite and then hit a skull sprite before the plus effect wears off, both effects are added to your player sprite. The same thing happens with similarly typed sprites; for instance, if you hit a plus and then another plus before the effect from the first plus wears off, your player sprite will now be at 400% of its normal size (normal size × 2 × 2).
The tricky thing is that even though you can stack power-up effects, there is only one power-up timer. This means that if you have two or three or more effects on your player sprite, they won’t expire at different times. Instead, every time you hit a power-up, the power-up timer will be reset to 5 seconds. Only if 5 seconds pass without you hitting any other power-ups will all the accumulated power-up effects expire.
This is by design, but again, this is a great place for you to examine how you think the game should be played. If you think a different rule for power-ups should be in place, make it happen! You’re the developer, and you’re the king of this virtual world, right? You might as well take advantage of that and make the game play exactly how you think it should.
Wow. Maybe this should say, “What didn’t you do?” This was a long chapter, but you did some great stuff. Let’s take a look:
You learned how to draw 2D text on the screen.
You randomly generated sprites of different types.
You added a background image.
You fleshed out a system to keep score.
You experimented with game states and implemented three states (start, in-game, and end).
You added splash and game-over screens using game states.
You added power-up effects and fine-tuning logic to your game.
2D fonts are drawn on the screen just the same as any Texture2D
object.
2D fonts are built using the SpriteFont
object.
Background images are added to games by using a sprite that covers the entire screen.
Game states are breaks in gameplay or transitions in gameplay from one state to another (for example, moving from one level to another, accomplishing a mission, losing the game, or something similar in concept).
Game development is a very creative business. While the mechanics are very scientific, as in all programming, the actual fine-tuning of the game is a very art-oriented craft that is heavily centered on how the gameplay feels to a user. As a developer, you should play with your game as early and often as possible so that you can tweak the experience into something that you enjoy and that you envision others will enjoy as well.
“Our mental discipline is matched only by our skill in XNA…I only hope these are enough to withstand this awful trial.” —Akara
What type of object is used to draw 2D text in XNA?
How is a background image different from an image used to represent a player or object in the game?
What are game states and how are they used?
In the Flight of the Conchords episode “Mugged,” what do the muggers steal from Jemaine?
18.116.60.62