Using SpriteFonts in a Board-based Game with XNA 4.0

Exclusive offer: get 50% off this eBook here
XNA 4.0 Game Development by Example: Beginner's Guide

XNA 4.0 Game Development by Example: Beginner's Guide — Save 50%

Create your own exciting games with Microsoft XNA 4.0

$26.99    $13.50
by Kurt Jaegers | September 2010 | Beginner's Guides Microsoft Web Graphics & Video

Having built a puzzle game called Flood Control in XNA 4.0 and enhanced it with animation, in this article by Kurt Jaegers, author of XNA 4.0 Game Development by Example: Beginner's Guide, we will take it further by:

  • Adding a SpriteFont to the game and displaying the current level and score in their appropriate positions on the screen
  • Implementing the flooding of the dome and adding increasing difficulty levels

All of these enhancements will give the player a better game experience, as well as give us the opportunity to learn more about how the SpriteBatch class can be used for animation and text display.

 

XNA 4.0 Game Development by Example: Beginner's Guide

XNA 4.0 Game Development by Example: Beginner's Guide

Create your own exciting games with Microsoft XNA 4.0

  • Dive headfirst into game creation with XNA
  • Four different styles of games comprising a puzzler, a space shooter, a multi-axis shoot 'em up, and a jump-and-run platformer
  • Games that gradually increase in complexity to cover a wide variety of game development techniques
  • Focuses entirely on developing games with the free version of XNA
  • Packed with many suggestions for expanding your finished game that will make you think critically, technically, and creatively
  • Fresh writing filled with many fun examples that introduce you to game programming concepts and implementation with XNA 4.0
  • A practical beginner's guide with a fast-paced but friendly and engaging approach towards game development

Read more about this book

(For more resources on XNA 4.0, see here.)

SpriteFonts

Unlike a Windows Forms application, XNA cannot use the TrueType fonts that are installed on your computer. In order to use a font, it must first be converted into a SpriteFont, a bitmap based representation of the font in a particular size that can be drawn with the SpriteBatch.DrawString() command.

Technically, any Windows font can be turned into a SpriteFont, but licensing restrictions on most fonts will prevent you from using them in your XNA games. The redistributable font package is provided by Microsoft to address this problem and give XNA developers a range of usable fonts that can be included in XNA games. Following are samples of each of the fonts included in the font package:

Time for action – add SpriteFonts to Game1

  1. Right click on the Fonts folder in the Content project in Solution Explorer and select Add | New Item.
  2. From the Add New Item dialog, select Sprite Font.
  3. Name the font Pericles36.spritefont. After adding the font, the spritefont file will open in the editor window.
  4. In the spritefont file, change <Fontname>Kootenay</Fontname> to <Fontname>Pericles</Fontname>.
  5. Change <Size>14</Size> to <Size>36</Size>.
  6. Add the following declaration to the Game1 class:

    SpriteFont pericles36Font;

  7. Update the LoadContent() method of the Game1 class to load spritefont by adding:

    pericles36Font = Content.Load<SpriteFont>(@"Fonts\Pericles36");

What just happened?

Adding a SpriteFont to your game is very similar to adding a texture image. Since both are managed by the Content Pipeline, working with them is identical from a code standpoint. In fact, SpriteFonts are really just specialized sprite sheets, similar to what we used for our game pieces, and are drawn via the same SpriteBatch class we use to draw our sprites.

The .spritefont file that gets added to your project is actually an XML document containing information that the Content Pipeline uses to create the .XNB file that holds the bitmap information for the font when you compile your code. The .spritefont file is copied from a template, so no matter what you call it, the XML will always default to 14 point Kootenay. In steps 4 and 5, we will edit the XML to generate 36 point Pericles instead.

Just as with a Texture2D, we declare a variable (this time a SpriteFont) to hold the Pericles 36 point font. The Load() method of the Content object is used to load the font.

SpriteFonts and extended characters
When a SpriteFont is built by the Content Processor, it actually generates bitmap images for each of the characters in the font. The range of characters generated is controlled by the <CharacterRegions> section in the SpriteFont's XML description. If you attempt to output a character not covered by this range, your game will crash. You can avoid this by removing the HTML comment characters (<!--and -->) from around the <DefaultCharacter> definition in the XML file. Whenever an unknown character is output, the character defined in <DefaultCharacter> will be used in its place.

Score display

Displaying the player's score with our new SpriteFont is simply a matter of calling the SpriteBatch.DrawString() method.

Time for action – drawing the score

  1. Add a new Vector2 to the declarations area of Game1 to store the screen location where the score will be drawn:

    Vector2 scorePosition = new Vector2(605, 215);

  2. In the Draw() method, remove "this.Window.Title = playerScore.ToString();" and replace the line with:
  3. ToString();" and replace the line with:
    spriteBatch.DrawString(pericles36Font,
    playerScore.ToString(),
    scorePosition,
    Color.Black);

What just happened?

Using named vectors to store things like text positions, allows you to easily move them around later if you decide to modify the layout of your game screen. It also makes code more readable, as we have the name scorePosition instead of a hard-coded vector value in the spriteBatch.DrawString() call. Since our window size is set to 800 by 600 pixels, the location we have defined above will place the score into the pre-defined score box on our background image texture.

The DrawString() method accepts a font to draw with (pericles36Font), a string to output (playerScore.ToString()), a Vector2 specifying the upper left corner of the location to begin drawing (scorePosition), and a color for the text to be drawn in (Color.Black).

ScoreZooms

Simply drawing the player's score is not very exciting, so let's add another use for our SpriteFont. In some puzzle games, when the player scores, the number of points earned is displayed in the center of the screen, rapidly growing larger and expanding until it flies off of the screen toward the player.

We will implement this functionality with a class called ScoreZoom that will handle scaling the font.

Time for action – creating the ScoreZoom class

  1. Add a new class file called ScoreZoom.cs to the Game1 class.
  2. Add the following using directive to the top of the file:

    using Microsoft.Xna.Framework.Graphics;

  3. Add the following declarati ons to the ScoreZoom class:

    public string Text;
    public Color DrawColor;
    private int displayCounter;
    private int maxDisplayCount = 30;
    private float scale = 0.4f;
    private float lastScaleAmount = 0.0f;
    private float scaleAmount = 0.4f;

  4. Add the Scale read-only property to the ScoreZoom class:

    public float Scale
    {
    get { return scaleAmount * displayCounter; }
    }

  5. Add a Boolean property to indicate when the ScoreZoom has finished displaying:

    public bool IsCompleted
    {
    get { return (displayCounter > maxDisplayCount); }
    }

  6. Create a constructor for the ScoreZoom class:

    public ScoreZoom(string displayText, Color fontColor)
    {
    Text = displayText;
    DrawColor = fontColor;
    displayCounter = 0;
    }

  7. Add an Update() method to the ScoreZoom class:

    public void Update()
    {
    scale += lastScaleAmount + scaleAmount;
    lastScaleAmount += scaleAmount;
    displayCounter++;
    }

What just happened?

The ScoreZoom class holds some basic information about a piece of text and how it will be displayed to the screen. The number of frames the text will be drawn for are determined by displayCounter and maxDisplayCount.

To manage the scale, three variables are used: scale contains the actual scale size that will be used when drawing the text, lastScaleAmount holds the amount the scale was increased by during the previous frame, and scaleAmount determines the growth in the scale factor during each frame.

You can see how this is used in the Update() method. The current scale is increased by both the lastScaleAmount and scaleAmount. lastScaleAmount is then increased by the scale amount. This results in the scale growing in an exponential fashion instead of increasing linearly by a scaleAmount for each frame. This will give the text a zooming effect as it starts growing slowly and then speeds up rapidly to fill the screen.

Time for action – updating and displaying ScoreZooms

  1. Add a Queue object to the Game1 class to hold active ScoreZooms:

    Queue<ScoreZoom> ScoreZooms = new Queue<ScoreZoom>();

  2. Add a new helper method to the Game1 class to update the ScoreZooms queue:

    private void UpdateScoreZooms()
    {
    int dequeueCounter = 0;
    foreach (ScoreZoom zoom in ScoreZooms)
    {
    zoom.Update();
    if (zoom.IsCompleted)
    dequeueCounter++;
    }
    for (int d = 0; d < dequeueCounter; d++)
    ScoreZooms.Dequeue();
    }

  3. In the Update() method, inside the case section for GameState.Playing, add the call to update any active ScoreZooms. This can be placed right before the case's break; statement:

    UpdateScoreZooms();

  4. Add the following to the CheckScoringChain() method to create a ScoreZoom when the player scores. Add this right after the playerScore is increased:

    ScoreZooms.Enqueue(new ScoreZoom("+" +
    DetermineScore(WaterChain.Count).ToString(),
    new Color(1.0f, 0.0f, 0.0f, 0.4f)));

  5. Modify the Draw() method of the Game1 class by adding the following right before the SpriteBatch.DrawString() call which draws the player's score:

    foreach (ScoreZoom zoom in ScoreZooms)
    {
    spriteBatch.DrawString(pericles36Font, zoom.Text,
    new Vector2(this.Window.ClientBounds.Width / 2,
    this.Window.ClientBounds.Height / 2),
    zoom.DrawColor, 0.0f,
    new Vector2(pericles36Font.MeasureString(zoom.Text).X / 2,
    pericles36Font.MeasureString(zoom.Text).Y / 2),
    zoom.Scale, SpriteEffects.None, 0.0f);
    }

What just happened?

Since all ScoreZoom objects "live" for the same amount of time, we can always be certain that the first one we create will finish before any created during a later loop. This allows us to use a simple Queue to hold ScoreZooms since a Queue works in a first-in-first-out manner.

When UpdateScoreZooms() is executed, the dequeueCounter holds the number of ScoreZoom objects that have finished updating during this cycle. It starts at zero, and while the foreach loop runs, any ScoreZoom that has an IsCompleted property of true increments the counter. When the foreach has completed, ScoreZooms.Dequeue() is run a number of times equal to dequeueCounter.

Adding new ScoreZoom objects is accomplished in step 4, with the Enqueue() method. The method is passed a new ScoreZoom object, which is constructed with a plus sign (+) and the score being added, followed by a red color with the alpha value set to 0.4f, making it a little more than halfway transparent.

Just as the SpriteBatch.Draw() method has multiple overloads, so does the SpriteBatch.DrawString() method, and in fact, they follow much the same pattern. This form of the DrawString() method accepts the SpriteFont (pericles36Font), the text to display, a location vector, and a draw color just like the previous call.

For the draw location in this case, we use this.Window.ClientBounds to retrieve the width and height of the game window. By dividing each by two, we get the coordinates of the center of the screen.

The remaining parameters are the same as those of the extended Draw() call we used to draw rotated pieces. After the color value is rotation, which we have set to 0.0f, followed by the origin point for that rotation. We have used the MeasureString() method of the SpriteFont class to determine both the height and width of the text that will be displayed and divided the value by two to determine the center point of the text. Why do this when there is no rotation happening? Despite what the order of the parameters might indicate, this origin also impacts the next parameter: the scale.

When the scale is applied, it sizes the text around the origin point. If we were to leave the origin at the default (0, 0), the upper left corner of the text would remain in the center of the screen and it would grow towards the bottom right corner. By setting the origin to the center of the text, the scale is applied evenly in all directions:

Just as with the extended Draw() method earlier, we will use SpriteEffects.None for the spriteEffects parameter and 0.0f for the layer depth, indicating that the text should be drawn on top of whatever has been drawn already.

Adding the GameOver game state

Now that we can draw text, we can add a new game state in preparation for actually letting the game end when the facility floods.

XNA 4.0 Game Development by Example: Beginner's Guide Create your own exciting games with Microsoft XNA 4.0
Published: September 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

Read more about this book

(For more resources on XNA 4.0, see here.)

Time for action – game over

  1. Modify the declaration of the GameStates enum in the Game1 class to include the GameOver state:

    enum GameStates { TitleScreen, Playing, GameOver };

  2. Add the following declarations to the Game1 class:

    Vector2 gameOverLocation = new Vector2(200, 260);
    float gameOverTimer;

  3. Modify the Update() method of Game1 by adding a new case section for the GameState.GameOver state:

    case GameStates.GameOver:
    gameOverTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
    if (gameOverTimer <= 0)
    {
    gameState = GameStates.TitleScreen;
    }
    break;

  4. Modify the if statement in the Draw() method of Game1 for the GameState.Playing state from if (gameState == GameStates.Playing) to:

    if ((gameState == GameStates.Playing) ||
    (gameState == GameStates.GameOver))

  5. Add a new if statement for the GameState.GameOver state to the Draw() method, right before the call to Base.Draw(gameTime).

    if (gameState == GameStates.GameOver)
    {
    spriteBatch.Begin();
    spriteBatch.DrawString(pericles36Font,
    "G A M E O V E R!",
    gameOverLocation,
    Color.Yellow);
    spriteBatch.End();
    }

What just happened?

With the addition of GameOver, we now have a complete cycle of game states. When the program is started, the game begins in the TitleScreen state. Pressing the Space bar switches from TitleScreen to Playing state. When the game ends, the state moves to GameOver.

The Update() method handles the GameOver state by decreasing the gameOverTimer value until it reaches zero, at which point the state is set back to TitleScreen.

While the Update() method handles each of the game states in a mutually exclusive manner (the update code for Playing will never run when in the GameOver state), the Draw() method handles things differently.

When in the GameOver state, we want to display the text G A M E O V E R! on top of the game board. The location of the text, defined as (200, 260) in our declarations area, places it in the upper half of the screen, covering the center horizontally. We need to execute the drawing code for the Playing state in both the Playing and GameOver states, as well as an additional section of code only for GameOver.

The flood

The background story of the game centers on an underwater research laboratory that is slowly flooding, with the player trying to empty out the flood waters before the place fills up.

Up to this point we do not have a representation of that flood in the game, or any incentive for the player to think quickly to find scoring chains.

Time for action – tracking the flood

  1. Add the following declarations to the Game1 class:

    const float MaxFloodCounter = 100.0f;
    float floodCount = 0.0f;
    float timeSinceLastFloodIncrease = 0.0f;
    float timeBetweenFloodIncreases = 1.0f;
    float floodIncreaseAmount = 0.5f;

  2. In the Update() method of Game1.cs, add the following code to keep track of the increasing flood waters right after the timeSinceLastInput variable is updated in the GameState.Playing case section:

    timeSinceLastFloodIncrease +=
    (float)gameTime.ElapsedGameTime.TotalSeconds;
    if (timeSinceLastFloodIncrease >= timeBetweenFloodIncreases)
    {
    floodCount += floodIncreaseAmount;
    timeSinceLastFloodIncrease = 0.0f;
    if (floodCount >= MaxFloodCounter)
    {
    gameOverTimer = 8.0f;
    gameState = GameStates.GameOver;
    }
    }

  3. Update the CheckScoringChain() method of the Game1 class by adding the following to decrease the flood counter when the player scores. Place this code right after playerScore += DetermineScore(WaterChain.Count);

    floodCount = MathHelper.Clamp(floodCount -
    (DetermineScore(WaterChain.Count)/10), 0.0f, 100.0f);

What just happened?

The flood itself is represented as a percentage. When the floodCount reaches 100 (MaxFloodCounter), the laboratory has completely flooded and the game is over. In addition to these two declarations, we also need to track how rapidly the flood increases (timeSinceLastFloodIncrease and timeBetweenFloodIncreases), and the rate at which the water rises (floodIncreaseAmount).

The timing on the flood increases is handled the same way input pacing is handled: a timer is incremented based on the elapsed game time until it reaches a threshold value. When it does, the timer is reset, and the floodCount variable is increased by the floodIncreaseAmount value.

When this increase takes place, we check to see if the floodCount has reached MaxFloodCount, indicating that the facility is flooded. If it has, an eight second timer is set for gameOverTimer and the game state is set to GameOver. Recall that in the GameOver handler, the gameOverTimer determines how long the G A M E O V E R! text will be displayed before the game switches back to the title screen.

Finally, in step three, the floodCount variable needs to be decreased each time the player completes a scoring chain. MathHelper.Clamp() is used to subtract the score value (divided by 10) from the floodCount, while keeping the value between 0.0f and 100.0f.

Displaying the flood

If you open the Background.png file in an image viewer, you will see that there is a full water tank floating inside the space on the playfield where game pieces get displayed. Since we always draw opaque game piece backgrounds over this area, so far we have not seen this portion of the image during game play.

We can use SpriteBatch.Draw() to cut out pieces of this full water tank and superimpose it over the empty tank on the right side of the game screen as the facility fills with water. The deeper the water gets, the more of the hidden water tank image we transfer to the visible tank on the screen, working our way up from the bottom:

Time for action – displaying the flood

  1. Add the following declarations to the Game1 class:

    const int MaxWaterHeight = 244;
    const int WaterWidth = 297;

    Vector2 waterOverlayStart = new Vector2(85, 245);
    Vector2 waterPosition = new Vector2(478, 338);

  2. Modify the Draw() method of the Game1 class by adding the following right after the SpriteBatch.DrawString() call that displays the player's score:

    spriteBatch.Draw(backgroundScreen,
    new Rectangle(
    (int)waterPosition.X,
    (int)waterPosition.Y + (MaxWaterHeight - waterHeight),
    WaterWidth,
    waterHeight),
    new Rectangle(
    (int)waterOverlayStart.X,
    (int)waterOverlayStart.Y + (MaxWaterHeight - waterHeight),
    WaterWidth,
    waterHeight),
    new Color(255, 255, 255, 180));

  3. Try it out! You should now be able to watch the flood slowly increase in the flood tank. When it reaches the top the game should switch to the GameOver state and, after an 8 second delay, back to the title screen.

What just happened?

The two int values, MaxWaterHeight, and WaterWidth refer to the size of the water image hidden inside the game board. It is 297 pixels wide, and the full water image is 244 pixels high.

Two vectors are used to store the location of the filled water image (85, 245) and the location that it will be drawn to on the screen (478, 338).

In order to draw the water in the water tank, the waterHeight variable, the MaxWaterHeight is multiplied by the percentage of water currently in the tank. This results in the number of pixels of water that need to be drawn into the tank.

When determining the source and destination rectangles, the X coordinates are dependant only on the location of the overlay and the drawing position, since they will not change.

The Y coordinates must be modified to pull pixels from the bottom of the image and expand upwards. In order to accomplish this, the current waterHeight is subtracted from the MaxWaterHeight, and this value is added to the Y coordinate of both vectors.

Difficulty levels

Now that the game can end, we need some way to make the game more difficult the longer the player plays.

After the player has completed 10 scoring chains, the water tank will be emptied, a new set of game pieces will be generated, and the flood will increase faster.

Time for action – adding difficulty levels

  1. Add the following declarations to the Game1 class:

    int currentLevel = 0;
    int linesCompletedThisLevel = 0;

    const float floodAccelerationPerLevel = 0.5f;

    Vector2 levelTextPosition = new Vector2(512, 215);

  2. Add the StartNewLevel() method to the Game1 class:

    private void StartNewLevel()
    {
    currentLevel++;
    floodCount = 0.0f;
    linesCompletedThisLevel = 0;
    floodIncreaseAmount += floodAccelerationPerLevel;
    gameBoard.ClearBoard();
    gameBoard.GenerateNewPieces(false);
    }

  3. Modify the Update() method of the Game1 class by updating the case section for GameState.TitleScreen to include the following right before the game state is set to GameState.Playing:

    currentLevel = 0;

    floodIncreaseAmount = 0.0f;

    StartNewLevel();

  4. Modify the CheckScoringChain() method to increment the linesCompletedThisLevel variable right after playerScore +=DetermineScore(WaterChain.Count);

    linesCompletedThisLevel++;

  5. Still in the CheckScoringChain() method, add the following to call the StartNewLevel() method if necessary. Place this code directly after the foreach loop that fades out tiles on the board:

    if (linesCompletedThisLevel >= 10)
    {
    StartNewLevel();
    }

  6. Update the Draw() method to display the current level in the appropriate location on the screen. Place this code right after the spriteBatch.DrawString() call that displays the player's score:

    spriteBatch.DrawString(pericles36Font,
    currentLevel.ToString(),
    levelTextPosition,
    Color.Black);

  7. Play! Flood Control is now completed, so try it out!

What just happened?

The current game level and the number of lines the player has completed in the current level are tracked as integers (currentLevel and linesCompletedThisLevel). The two constants, baseFloodAmount and floodAccelerationPerLevel, determine how much water is added to the facility every time the flood is updated. Finally, the levelTextPosition vector points to the location on the screen where the level number will be displayed.

The StartNewLevel() method increases the currentLevel, and clears the floodCount and lineCompletedThisLevel variables. It increases the floodIncreaseAmount by the value of floodAccelerationPerLevel and then clears the game board. Finally, new pieces are generated for each square on the board.

When beginning a new game (the updates in Step 3) we can simply set currentLevel and floodIncreaseAmount to zero, and then call the StartNewLevel() method. Since both of these variables are increased by StartNewLevel() the first level of a new game will begin with the appropriate values.

Step 4 increases the counter that tracks the number of lines the player has completed on the current level every time a scoring chain results in points. Step 5 checks to see if the player has completed 10 or more lines. If they have, a new level is started.

Finally, drawing the level number is a call to the simple form of SpriteBatch. DrawString() just as we did for displaying the player's score.

Have a go hero

There are a number of different things you could do to spruce up Flood Control. Here are a few suggestions to try using the knowledge:

  • Basic—add a "Paused" game state that displays an indication that the game is paused and how to resume play. To prevent cheating, the game board should either not be visible or be obscured in some way while the game is paused.
  • Intermediate—the Game Over screen is not very exciting. Create a new bitmap image indicating the aftermath of the flooded facility and display that image instead of the simple Game Over! text. You will need to load the image via the LoadContent() method and display it when appropriate.
  • Advanced—create an additional "suffix" for pieces that are locked down and cannot be turned. You'll need to expand the Tile_Sheet.png file by adding an additional (fourth) column and then copying the first two columns to columns three and four. Draw bolts in the four corners of each of the twelve new piece images and modify the draw code to add an additional 40 pixels to the X value of the source Rectangle if the piece contains the locked suffix. Grant extra points for using locked pieces in a scoring chain.

Summary

In the above article we have covered:

  • Adding a SpriteFont to the game and displaying the current level and score in their appropriate positions on the screen
  • Implementing the flooding of the dome and adding increasing difficulty levels

Further resources on this subject:


XNA 4.0 Game Development by Example: Beginner's Guide Create your own exciting games with Microsoft XNA 4.0
Published: September 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

About the Author :


Kurt Jaegers

Kurt Jaegers is an Oracle Database Administrator and Windows Network Administrator, as well as a long-time hobbyist game developer. He has built games for everything from the Commodore 64 to the Xbox 360. He is the owner of xnaresources.com, and the author of XNA 4.0 Game Development by Example: Beginner's Guide (C# edition) and XNA 4.0 Game Development by Example: Beginner's Guide – Visual Basic Edition, both of which were published by Packt Publishing.

Books From Packt


Unity 3D Game Development by Example Beginner's Guide
Unity 3D Game Development by Example Beginner's Guide

3D Game Development with Microsoft Silverlight 3: Beginner's Guide
3D Game Development with Microsoft Silverlight 3: Beginner's Guide

Flash Multiplayer Virtual Worlds
Flash Multiplayer Virtual Worlds

Flash 10 Multiplayer Game Essentials
Flash 10 Multiplayer Game Essentials

Blender 3D 2.49 Architecture, Buildings, and Scenery
Blender 3D 2.49 Architecture, Buildings, and Scenery

WordPress and Flash 10x Cookbook
WordPress and Flash 10x Cookbook

Unity Game Development Essentials
Unity Game Development Essentials

Microsoft Silverlight 4 and SharePoint 2010 Integration
Microsoft Silverlight 4 and SharePoint 2010 Integration


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
X
M
H
F
k
U
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software