Using SpriteFonts in a Board-based Game with XNA 4.0

0
88
10 min read

 

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>(@"FontsPericles36");

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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here