Text Handling Class for XNA 3.0

Applies to: Windows Games, XBox 360 Games, Zune Games

A Quick Note: As with all of my tutorials, this is only one way to do things. The TextHandler class we will be building here may not be appropriate for all game implementations. Depending on how you use it, it shouldn't have any negative speed impact on your code over the normal SpriteBatch.DrawString() method, but if you start using a lot of formatting and word-wrapping, the extra processing time can start adding up, so I suggest using the simplest calls that will get the job done. For example, if you aren't using embedded codes in a particular string, don't use DrawTextParsed() to display it.

Introduction

As I said in the introduction to my Star Defense tutorial series, XNA 2.0 really improved the way we handle drawing text to the screen in XNA.  Prior to 2.0, we had to create our own bitmapped fonts and use them to draw one letter at a time to the screen with SpriteBatch.Draw.  With the introduction of SpriteFonts and SpriteBatch.DrawString() in XNA 2.0, things got much simpler, but there is still some room for improvement. 
This article describes a TextHandler static class that I put together for one of the projects I’ve been working on.  It is fairly straightforward, and provides some nice shortcuts to make handling text in XNA even simpler than it is now.

Lets start by defining what we want to accomplish with the TextHandler class:

  • Easy to use – No point in creating a “helper” class that is harder to work with than the default SpriteBatch.DrawString system.
  • Automatic handling of SpriteFonts
  • A “Console-Like” mode (I don’t mean console like an Xbox game console, but a computer console, where you print to the screen and a “cursor” advances after each print so your subsequent prints end up starting after the previous one ended).
  • Basic text wrapping

While we are at it, lets also incorporate something fun as well.  We can build on the “console” discussed above to create our own “markup” language for text.  We can use this to change the color and font of the text we are drawing mid-string.

Getting started

Before you can work with text in XNA, you’ll need some fonts.  While you can technically create SpriteFonts from any font you have installed in Windows, you most likely can’t redistribute practically any of them legally.  For this reason, Microsoft provides a pack of redistributable fonts for XNA developers:

http://go.microsoft.com/fwlink/?LinkId=104778&clcid=0x409

Download the redistributable font package and install it.  You need to do this before launching Visual Studio so that they will be “known” to XNA when you try to use them.

After that is finished, fire up XNA Game Studo Express and start up a new project.  We’ll start with a little prep work for our project testing later, so right click on “Content” in solution explorer and click “Add->New Folder” and create a folder called “Fonts”.  We will store all of our SpriteFont objects here.

Now, lets go ahead and create a few SpriteFonts.  Right Click on the Fonts folder and click “Add->New Item”.  This will open the Add New Item dialog box.  Select SpriteFont as the item type, and enter the name “Lindsey20.spritefont” for the item name.

Click OK, and Visual Studio will create the .spritefont file and open it in the editor.  This file contains a LOT of comments, and just a few lines that actually control the output.  Near the top, you will see that there is a “FontName” field with “Kootenay” listed as the font name.  This is simply the default that is included in the SpriteFont template.  Change the FontName tag to “Lindsey”. 

Right below that (after a comment) is the Size tag.  The default here is 14, but lets set it to 20 for our purposes.

Now, we are going to need a few more SpriteFonts to play with, so I’ll just provide a list for you.  Add a new SpriteFont file for each, using the name give.  Set the FontName and Size appropriately for each .spritefont file:

  • Name: Pericles8.spritefont – Font: Pericles – Size 8
  • Name: Pericles10.spritefont – Font: Pericles – Size 10
  • Name: Pericles12.spritefont – Font: Pericles – Size 12
  • Name: Pericles14.spritefont –Font : Pericles – Size 14
  • Name: Pericles14Bold.spritefont – Font: Pericles – Size 14

For the last one (Pericles14Bold), scroll down a little in the .spritefont file and find the “Style” tag.  Change this from “Regular” to “Bold”.

Now we have some resources to work with later, so lets get on to putting the TextHandler together.

We will be creating a “static” class, meaning that you won’t have to create instances of the TextHandler class.  All of it’s functions will simply be available to any project it is included in.  There is nothing really complex or magical about a static class.  You use them all the time in C#/XNA development, as C# provides a number of them for you, such as MathHelper, which is a library of math functions.  You never create an object of the MathHelper class.  It is just there and usable.

In order to create our class, right click on the name of your project in Project Explorer and click “New -> Class”.  Name the class “TextHandler.cs” and click ok.

As usual, the first thing we need to do is add some “using” statements so that we have access to the XNA Framework.  At the top of the class file, add:

using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Storage;


This will give us the references we will need.  Next, we need to change our class from a normal class to a static class. Don’t worry!  It’s pretty easy.  Find the line that says:

class TextHandler
and change it to:

static class TextHandler

Shocking, I know.  There is, however, a special rule about static classes.  All of their members must also be static, so everything we declare inside this class will also be declared with “static” in front of it.

Declarations

One of our goals is to bring some simple automation to the way we handle fonts.  We have a number of fonts in our project, and conveniently each one has a distinct name that identifies it.  We can use a Dictionary object to map these names (strings) to the actual fonts (SpriteFonts).


Lets add the following declaration to our class:

// A dictionary to hold all of the SpriteFont objects we may use to draw text
private static Dictionary<string, SpriteFont> Fonts = new Dictionary<string, SpriteFont>();

Nothing out of the ordinary there.  We will add SpriteFonts to our dictionary and reference them later when we call SpriteBatch.DrawString.

We will also keep track of the font and color we are currently drawing in.  This will make things easier on us when using the class because we can specify a color and font and then just draw text without worrying about respecifying them in each draw call.  We’ll have two internal variables to keep track of this, and two properties to access them:

// The current font we are drawing in when the call to DrawText does not include a font
// specification (Can be overridden by specifying a font in the DrawText call.)
private static string sCurrentFont = "";

// The current color to use when drawing text (Can be overridden by specifying a color in the
// DrawText call.
private static Color cCurrentColor = Color.White;

// Whenever we draw text, we will use the CurrentFont if we haven't specified otherwise.
static public string CurrentFont
{
    get { return sCurrentFont; }
    set
    {
        if (Fonts.ContainsKey(value))
            sCurrentFont = value;
    }
}

// Whenever we draw text, we will use the CurrentColor if we haven't specified otherwise.
static public Color CurrentColor
{
    get { return cCurrentColor; }
    set { cCurrentColor = value; }
}

You may note that we default to drawing in white.  We don’t do anything special when setting a color, but when we set a font, we check to make sure that it exists in our font dictionary before actually setting it.
Remember  that we wanted to create a “console” type effect, so we will need to keep track of the position of our make-believe cursor.

// When using the "Print" function, CursorLocation tracks the current drawing position for future
// calls to the Print command.
public static Vector2 CursorLocation { get; set; } 
And finally, in order to use SpriteBatch and to load fonts, we need SpriteBatch and ContentManager objects.  We’ll initialize these from our game so that the TextHandler can use them later:
// We need a SpriteBatch object to do any drawing.
static public SpriteBatch spriteBatch { get; set; } // We will use a ContentManager object to load our SpriteFonts.
static public ContentManager contentManager { get; set; }

Loading Fonts

One of the things we want to simplify is the loading of fonts.  Since we have a font dictonary, there is no need to load them all by hand in our game’s LoadContent method.  We will create a few functions that will handle all of our fonts automatically.

Lets start with the most basic way to load a font:

// Load a SpriteFont given the FontName and the ResourceName (Usually identical)
static public void LoadFont(string FontName, string ResourceName)
{
    // Throw an exception if we don't have a content manager.
    if (contentManager == null)
        throw new Exception("TextHandler ContentManager Not Initialized");

    Fonts.Add(FontName, contentManager.Load<SpriteFont>(ResourceName));
    if (CurrentFont == "")
        CurrentFont = FontName;
}

The first thing we do is check to see if we have a ContentManager to use.  If not, we throw an exception.  Assuming we do, however, we just use the Dictionary’s Add() method to add a font (referenced by the string FontName) and the ContentManager to load the file by it’s resource name.  This is the same way you would load a font (or a texture, or a model, or anything else) with ContentManager.

Finally, we check to see if there is a CurrentFont.  If there isn’t, we set the current font to the font we just loaded.  This will make the first font we load the default font if we never set it anywhere else.
Lets add another way to load fonts:

// Load multiple fonts by passing a folder and a semi-colon delimited list of fonts to load.
// For example, if your content folder is "Content\Fonts" and you want to load Pericles10 and Pericles12,
// Pass @"Fonts\" as the FontsFolderPrefix and "Pericles10;Pericles12" as the FontList
static public void LoadFonts(string FontsFolderPrefix, string FontList)
{
    // Throw an exception if we don't have a content manager.
    if (contentManager == null)
        throw new Exception("TextHandler ContentManager Not Initialized");

    string[] FontsToLoad = FontList.Split(';');
    foreach (string s in FontsToLoad)
        LoadFont(s.Trim(), FontsFolderPrefix + s.Trim());
}

You’ll notice that this method actually uses our previous LoadFont method.  You would call this with something like:

TextHandler.LoadFonts(@”Fonts\”, “Pericles8;Pericles10;Pericles12”);

Which would load Pericles8, Pericles10, and Pericles12 from the “Fonts” folder of ContentManager.  This method is actually here for flexability though, since we won’t be using it.  We’re going to automate even more:

// Automatically loads all SpriteFonts from the passed folder (as a subfolder of the
// content manager root folder (ie, if your fonts are in "Content\Fonts" and your ContentManager's
// base folder is "Content", pass "Fonts" as the ContentFontsFolder.
static public void AutoLoadFonts(string ContentFontsFolder)
{
    // Throw an exception if we don't have a content manager.
    if (contentManager == null)
        throw new Exception("TextHandler ContentManager Not Initialized");

    // Get all of the files in the passed folder (they will be .XNB files)
    // Note that it will try to load any .XNB files in this folde as SpriteFonts, so that
    // should be the only type of content located there.
    DirectoryInfo info = new DirectoryInfo(contentManager.RootDirectory + @"\" + ContentFontsFolder);
    foreach (FileInfo file in info.GetFiles("*.XNB"))
    {
        // Trim off the .XNB extension
        string sFontName = file.Name.Substring(0, file.Name.Length - 4);

        // Load the font, giving it a dictionary name equal to the .spritefont asset name.
        LoadFont(sFontName, ContentFontsFolder + @"\" + sFontName);
    }
}
Here is our ultimate way to load fonts.  If we pass AutoLoadFonts the name of our fonts folder, it will use the DirectoryInfo and FileInfo objects to examine the folder and load each file in the folder as a SpriteFont.  Again, we are actually using the LoadFont() method we created earlier and just supplying the needed parameters based on what we find in the directory.

Initialization

You will have noticed that the first thing all of the font loading methods do is check to see if we have a ContentManager object, and bomb out if we don’t.  We’ll need some way to specify this object, and while we are at it we might as well specify our SpriteBatch too:

// Initializes the TextHandler class for usage.  Call this in your game's LoadContent.  If you pass
// a folder prefix (ie, @"Fonts\") the class will automatically load all spritefonts in that content folder.
static public void Initialize(SpriteBatch spriteBatch, ContentManager contentManager, string FontsFolderPrefix)
{
    TextHandler.spriteBatch = spriteBatch;
    TextHandler.contentManager = contentManager;
    if (FontsFolderPrefix != "")
        AutoLoadFonts(FontsFolderPrefix);
}

We’ll call initialize from our game’s LoadContent() method.  IF we pass it a SpriteBatch, a ContentManager, and the name of our fonts folder, it will handle all of the setup we need and load all of the fonts automatically.  If we leave the FontsFolderPrefix blank, we won’t load fonts automatically, and you will need to use LoadFont() or LoadFonts() from your game to load them yourself.

Lets Draw Something

So how are we going to draw text? We’re going to use SpriteBatch.DrawString() of course.  In fact, the entire TextHandler class boils down to two things : Handling Fonts and calling SpriteBath.DrawString().
We’ll start off by creating a “DrawText()” method for which we will specify all of the parameters of SpriteBatch.DrawString():

// Draw a text string.  This is our full method, with all parameters specifiable.  All of the other
// DrawText methods will be overloads of this function.
static public void DrawText(string Text, int X, int Y, float Angle, 
    string Font, Color color, Vector2 Origin, float Scale, 
    SpriteEffects spriteEffects, float LayerDepth)
{
    // Throw an exception if we don't have a spritebatch.
    if (spriteBatch == null)
        throw new Exception("TextHandler SpriteBatch Not Initialized");

    // Make sure the font exists in our font dictionary.  Otherwise, default to the CurrentFont
    string FontToUse = Font;
    if (!Fonts.ContainsKey(FontToUse))
        FontToUse = CurrentFont;

    // Use SpriteBatch to draw the text string.
    spriteBatch.DrawString(Fonts[FontToUse],
        Text,
        new Vector2(X, Y),
        color,
        Angle,
        Vector2.Zero,
        Scale,
        spriteEffects,
        LayerDepth);
}

As before, we are checking to see if we can proceed (by having a SpriteBatch object) first.  Assuming we can, we check to see if the font name passed in is in our font dictionary.  If it is, we’ll use it.  If not, we’ll use the default font.
Next, we just pass the parameters along to SpriteBatch.DrawString().

So I can hear you say “if we have to specify all of those parameters, why am I even doing this?” Good question!  But we’ll create a bunch of overloads of DrawText() which default things for us so we don’t have to specify them all.
If you aren’t familiar with overloads (you’ve used them, but you may now know what they are) they are simply multiple methods with the same name but different “method signatuers”.  In this case, a method signature is the list of parameters it takes.  Very often, overloaded methods leave out some parameters in the method signature and simply pass along defaults to the full method.  That’s what we are going to be doing in this case.

We’ll create a few DrawText() overloads.  I’m going to paste them in here, because they are all small and straightforward:

// ***
// All of these overloads simply pass defaults along to DrawText above, allowing you to specify
// different parameters when calling DrawText

static public void DrawText(string Text, int X, int Y)
{
    DrawText(Text, X, Y, 0, CurrentFont, CurrentColor, Vector2.Zero, 1.0f, SpriteEffects.None, 0);
}

static public void DrawText(string Text, int X, int Y, string Font)
{
    DrawText(Text, X, Y, 0, Font, CurrentColor, Vector2.Zero, 1.0f, SpriteEffects.None, 0);
}

static public void DrawText(string Text, int X, int Y, Color color)
{
    DrawText(Text, X, Y, 0, CurrentFont, color, Vector2.Zero, 1.0f, SpriteEffects.None, 0);
}

static public void DrawText(string Text, int X, int Y, string Font, Color color)
{
    DrawText(Text, X, Y, 0, Font, color, Vector2.Zero, 1.0f, SpriteEffects.None, 0);
}

So here we have four overloads of DrawText.  The simplest coming first.  If we are happy with our CurrentFont and CurrentColor, we can simply call the first overload, specifying the text to draw, and an x/y location.  The other parameters are filled in for us.

The other three overloads allow us to specify other combinations of parameters while keeping the defaults for everything else.

If you want to see things in action at this point, open your “Game1.cs “ file and locate your LoadContent method.  Add the following line after the SpriteBatch is set up:

TextHandler.Initialize(spriteBatch, Content, "Fonts");

That’s all we need to do to initialize our TextHandler. Notice that we didn’t create a TextHandler object.  Since we are using a static class, we don’t have to (and in fact, we can’t).

Next, scroll down to your Draw() method and add the following right after the GraphicsDevice.Clear:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
TextHandler.DrawText("Test Pericles8", 10, 10, "Pericles8");
spriteBatch.End();

Whenever we use TextHandler, we need an active SpriteBatch.Begin call. We don’t do that inside TextHandler itself because it is highly inefficient to keep doing Begin/Ends on SpriteBatch, so we just make it a requirement that you have to have an active SpriteBatch going on when you call DrawText().

Fire up your project and you should have “Test Pericles8” sitting in the upper left corner of your screen.

Implementing the Console

Now that we have the basics set up, lets start getting a little fancier.  We already have a declaration for the CursorLocation, so lets create a method to use and update it called “print”:

static public void Print(string Text)
{
    DrawText(Text, (int)CursorLocation.X, (int)CursorLocation.Y);
    CursorLocation += new Vector2(0, Fonts[CurrentFont].MeasureString(Text).Y);
}

For the simple implementation we are doing here, that’s all we need.  We simply call our DrawText() method pointing it at the current location and then advance the Y location of the cursor by the height of the text we drew.  Since the X location doesn’t change, it will seem like we advacned to the “next line”.

Lets try it out.  In your Game1.cs file, in the Draw() method right before the SpriteBatch.End, add:

TextHandler.CurrentFont = "Pericles14";
TextHandler.CursorLocation = new Vector2(300, 10);
TextHandler.Print("Hello, world!");
TextHandler.Print("This text demonstrates automatic");
TextHandler.Print("Text console handling!");

We set the cursor location to 300,10 and then to three consecutive print statements.  Each one will advance the cursor after it is done.  Fire it up and you should see three lines of text on the right half of your screen in Pericles14.

Text Formatting

The next thing we are going to talk about is our “markup language” for text strings.  Actually, markup language makes it sound a lot more complex than it actually is.  In reality, all we are doing is inserting some special sequences into our text strings that our TextHandler will identify and interpret when printing text.  They won’t be displayed to the screen.

First ,we need to come up with an unlikely text sequence to start our markup string.  I’ve decided that we’ll use a exclamation point (!) and a square bracket ([) to start our markup string, and end it with a square bracket (]).
This means that we won’t be able to use the sequence “![“ in what we draw with the parsed text code (though it won’t impact calls to DrawText itself).

Next, we’ll need to define what “codes” we will use.  I’m going to keep this fairly simple.  We will define the following codes:

  • ![#FFFFFF] – Specify a color in “web-like” format.  A # sign precedes a six-digit hexadecimal representation of the RGB values of the color.  Any subsequent text in this string will be drawn in this color unless another color is specified later or the end of the string is reached.
  • ![F:FontName] – Specify a font to switch to.  Any subsequent text will be drawn in this font unless another font is specified later or the end of the string is reached.
  • ![\n] – A new line.  We won’t actually insert these, but out word wrapping code will put them in and we will handle them.

Lets start, strangely enough, by adding a function to strip these codes out of a text string:

// Returns a string with all of our "special" formatting codes ( ![CODE] ) stripped out.
// We will use this when measureing text for wrapping purposes to make sure our codes don't
// impact our line lengths.
static public string StripTextCodes(string Text)
{
    string ResultText = "";
    string WorkText = Text;
    // loop until there is not an instance of "![" in the string
    while (WorkText.Contains("!["))
    {
        // pull apart the string, removing the ![, the ], and the code between them from the text
        string PreText = WorkText.Substring(0, WorkText.IndexOf("!["));
        string MidText = WorkText.Substring(WorkText.IndexOf("![") + 2);
        string PostText = MidText.Substring(MidText.IndexOf("]") + 1);
        MidText = MidText.Substring(0, (MidText.Length - PostText.Length) - 1);
        WorkText = PostText;
        // Append the actual text to our result
        ResultText += PreText;
    }
    ResultText += WorkText;
    // return the string with the codes stripped out.
    return ResultText;
}

We’ll need this later.  All we are doing here is taking the text string and then using Substring() to pull it apart.  We get the text before, in between, and after the ![ and ] characters and throw out what is between them (along with the delimiters themselves).  The string we return will have the text free of the markups.

Ok, it’s “wall of text” time.  The next method isn’t really that complicated, but it looks big because there are a lot of comments in it.  This method, called DrawTextParsed, will take a text string containing our markup characters, interpret them, and draw the results:

// Allows special codes to be embedded in a text string to allow for color and font changes.
// The currently implemented codes are:
//
// ![#FFFFFF] - Specifies a color in hex (html-style) notation.  (ie, #FFFFFF=white, #000000=black)
// ![F:FontName] - Change to the specified font
// ![\n] - Created manually or automatically... specifies a new line
static public void DrawTextParsed(string Text, int X, int Y, string Font, int WrapLength)
{
    // We will use the CursorLocaion to draw our string correctly, so we need to preserve the
    // location so we can restore it after we are done.
    Vector2 SaveCursor = CursorLocation;

    // Preserve the current DefaultColor
    Color SaveColor = CurrentColor;

    // Preserve the current Font
    string SaveFont = CurrentFont;

    // Set the cursor to our drawing location
    CursorLocation = new Vector2(X, Y);

    string PreText, MidText, PostText;

    string WorkText = Text;

    // If a Wrap Length has been passed, call our WrapText function and replace all of the
    // newline characters (\n) with ![\n] so they are "coded" with our standard code format.
    if (WrapLength > 0)
        WorkText = WrapText(WorkText, CurrentFont, WrapLength, true).Replace("\n", "![\n]");

    // Loop while the working text string contains a formatting code.
    while (WorkText.Contains("!["))
    {
        // PreText holds the portion of the string before the code.
        PreText = WorkText.Substring(0, WorkText.IndexOf("!["));

        MidText = WorkText.Substring(WorkText.IndexOf("![") + 2);

        // PostText holds the portion of the string after the code.
        PostText = MidText.Substring(MidText.IndexOf("]") + 1);

        // MidText holds the code itself
        MidText = MidText.Substring(0, (MidText.Length - PostText.Length) - 1);

        // Reset WorkText to contain everything after the code.
        WorkText = PostText;

        // Draw PreText at the current location, in the current font and color.
        DrawText(PreText, (int)CursorLocation.X, (int)CursorLocation.Y);

        // Move the cursor horizontally to the end of what we just drew.
        CursorLocation += new Vector2(Fonts[CurrentFont].MeasureString(PreText).X, 0);

        // If we have a formatting code...
        if (MidText.Length > 0)
        {
            // The # sign signifies an HTML-like color code (ie, #FFFFFF = White)
            if (MidText.StartsWith("#"))
            {
                // Pull out the characters
                byte r = byte.Parse(MidText.Substring(1, 2), System.Globalization.NumberStyles.HexNumber);
                byte g = byte.Parse(MidText.Substring(3, 2), System.Globalization.NumberStyles.HexNumber);
                byte b = byte.Parse(MidText.Substring(5, 2), System.Globalization.NumberStyles.HexNumber);

                // Set current color to the desired color.
                CurrentColor = new Color(r, g, b);
            }
            // A code beginning with "F:" designates a font change.
            if (MidText.StartsWith("F:"))
            {
                // Set the current font to the desired font.
                CurrentFont = MidText.Substring(2);
            }
            // The newline codes we wrapped earlier:
            if (MidText == "\n")
            {
                // Reset the horizontal position of the cursor, and bring the vertical position down
                // by the size of the PreText line.
                CursorLocation = new Vector2(X, CursorLocation.Y + Fonts[CurrentFont].MeasureString(PreText).Y);
            }
        }
    }

    // We have no codes left in WorkText, so simply draw whatever portion of it is left.
    DrawText(WorkText, (int)CursorLocation.X, (int)CursorLocation.Y);

    // Restore the saved positions
    CursorLocation = SaveCursor;
    CurrentColor = SaveColor;
    CurrentFont = SaveFont;
}

SpriteBatch.DrawString() only handles one font and color at a time, so we will leverage our “console” concept to make DrawTextParsed work.

The first thing we do is store the current cursor location, color, and font off to local variables so that we can put them back when we are done.  We want to leave them unchanged after calling DrawTextParsed but we are going to use them during the process so we have to “back them up”.

Next, we check to see if a WrapLength of anything but 0 was specified.  If it was, we are going to need to use our word wrapping function (coming up) to wrap the text.  The word wrapper will add newline characters (\n) to our string, but since we are using the “console” to print, we need to handle these newline characters differently from the normal SpriteBatch.DrawString() handling.  For this reason, after we get the text back from the word wrapper, we replace the \n characters with ![\n], essentially wrapping them in our markup code.

Then, just as our code remover method above does, we start pulling the string apart, looking for codes.  Every time we find a code, we print the text before it, and advance the X location of the cursor by the width of what we just printed.  We leave the Y position alone for now.

Then, we check to see what formatting code we have.  If our code starts with a # sign, we use System.Globalizaton.NumberStyles.HexNumber to convert the three 2-digit hex values into the red, green, and blue bytes that we will use to create a color, setting CurrentColor to that color.

Next, if our code starts with “F:”, we know we are trying to change fonts, so we simply set CurrentFont to whatever the rest of our code is.  Note that even if we specify an invalid font we won’t actually crash, because the CurrentFont property checks to make sure what we are trying to do is valid.  Otherwise it ignores the request.

Next, we check to see if the code is a newline character (\n).  If it is, we update the cursor location by resetting the X coordinate back to our original X value, and advancing the Y coordinate by the height of the text we just drew.

Finally, we will reach a point where we have no codes left to handle, so whatever text is left over we have to draw out (to avoid truncating the string at the end of the final code).

Right before we exit the method, we reset the CurrentColor, CurrentFont, and CursorLocation variables to what they were before we started.

Because of the call to WrapText inside the DrawTextParsed function, we can’t run our code yet.  We need WrapText to exist first, so lets go ahead and add it:

// I've seen the code that was the basis for this method floating around several places
// on the web.  As best I can tell it may have originally come from Nick Gravelyn (http://nickgravelyn.com/)
// but I'm not sure if that is the case.
//
// In any case, it returns a string that has newline characters (\n) inserted into it at the points where
// they would need to go to wrap a text string to a certain width.
static public string WrapText(string Text, string FontName, float MaxLineWidth, bool StripCodes)
{
    // Create an array (words) with one entry for each word in the passed text string
    string[] words = Text.Split(' ');

    // A StringBuilder lets us add to a string and finally return the result
    StringBuilder sb = new StringBuilder();

    // How long is the line we are currently working on so far
    float lineWidth = 0.0f;

    // Store a measurement of the size of a space in the font we are using.
    float spaceWidth = Fonts[FontName].MeasureString(" ").X;

    // Loop through each word in the string
    foreach (string word in words)
    {
        Vector2 size;
        // Strip out formatting codes if necessary
        if (StripCodes)
            size = Fonts[FontName].MeasureString(StripTextCodes(word));
        else
            size = Fonts[FontName].MeasureString(word);

        // If this word will fit on the current line, add it and keep track
        // of how long the line has gotten.
        if (lineWidth + size.X < MaxLineWidth)
        {
            sb.Append(word + " ");
            lineWidth += size.X + spaceWidth;
        }
        else
        // otherwise, append a newline character to start a new line.  Add the
        // word and a space, and set the size of the new line.
        {
            sb.Append("\n" + word + " ");
            lineWidth = size.X + spaceWidth;
        }
    }
    // return the resultant string
    return sb.ToString();
}

As the comments to this code mention, I’ve seen the basics of this code floating around the web, so it’s based on stuff that is already out there.  I simply made it a bit more generic (supporting multiple fonts) and added the code to strip out our markup language.

The result of WrapText will be a string that contains newline characters at the necessary points to keep the lines within the width limit.

Finally, lets add a couple of overloads for WrapText since we want to be able to use it outside of our DrawTextParsed():

// Overloads for WrapText that supply defaults for some parameters
static public string WrapText(string Text, float MaxLineWidth)
{
    return WrapText(Text, CurrentFont, MaxLineWidth, false);
}

static public string WrapText(string Text, string FontName, float MaxLineWidth)
{
    return WrapText(Text, FontName, MaxLineWidth, false);
}

All these do, again, is specify some default parameters to simplify calling the method.

More Testing

That’s it for the TextHandler class!  Lets go back to Game1.cs and add a few more tests.  Here is everything between my SpriteBatch.Begin and SpriteBatch.End calls:

// Specify our defaults
TextHandler.CurrentFont = "Pericles10";
TextHandler.CurrentColor = Color.White;

// Draw some text in each of our fonts
TextHandler.DrawText("Test Pericles8", 10, 10, "Pericles8");
TextHandler.DrawText("Pericles10", 10, 30, "Pericles10");
TextHandler.DrawText("Pericles12", 10, 50, "Pericles12");
TextHandler.DrawText("Pericles14", 10, 70, "Pericles14");
TextHandler.DrawText("Lindsey20", 10, 110, "Lindsey20");

// Demonstrate the "console" text handling
TextHandler.CursorLocation = new Vector2(300, 10);
TextHandler.Print("Hello, world!");
TextHandler.Print("This text demonstrates automatic");
TextHandler.Print("Text console handling!");

// Draw some word-wrapped text in yellow

TextHandler.CurrentColor = Color.Yellow;
TextHandler.Print(TextHandler.WrapText("This Line of Text is Wrapped " +
  "to be no more than 75 pixels wide!", 75));
TextHandler.Print("And this one isn't!");

// Draw some parsed text, wordwrapping it to 250 pixels wide.
TextHandler.CurrentColor = Color.White;
TextHandler.DrawTextParsed("This is ![#FFFF00]Yellow ![#FFFFFF] and " +
  "![#0000FF]Blue ![#FFFFFF] " +
  "and ![#FF0000]![F:Pericles14Bold] RED! " +
  "![F:Pericles14]![#FFFFFF]text " +
  "![F:Pericles14Bold]![#0000FF]Blue Bold " +
  "![F:Pericles14]![#FFFFFF]with " +
  "[brackets!] and lots of text and the " +
  "end to make sure wrapping is working ok.",
 10, 250, "Pericles14", 250);

As you can see, we run down each of our methods of drawing text. We start out by setting some defaults for font and color (instead of leaving the default font up to the LoadFont method).

Next, we use one of our DrawText overloads to display samples of each of our fonts to the screen at specific locations.

After that, we demonstrate using the "Console" mode, printing "Hello World" and a couple of other phrases. We also use the "Console" mode to display some wrapped text, noticing that our console cursor accounts for line wrapping just fine.

Finally, we demonstrate our ability to embed formatting codes into our text string.

Ideas for Expansion

There are lots of other things we could add to our TextHandler method, and I'll probably do an update down the road to include a few that I have in mind. Besides the ones I'm toying with, you could also do things like:

  • Add tags for things like Italics and Bold that would automatically select a font if it was named correctly (ie, append _B to the font name for bold, _I for italics, and _BI for bold italics or something along those lines). Of course, these fonts would need to be included in your project, but the TextHandler wouldn't break if they weren't, it would simply revert to using the default font since we check for non-existant fonts in DrawText.
  • I've also considered creating text "Components" that would utilize the TextHandler class and keep themselves updated. I can imagine, say, a Score Display component that would draw the player's current score as a DrawableGameComponent. All you would need to do would be to set it up with a location, point it at what variable you keep your game's score in, and enable it.

Download Source

































 
 
 
Site Contents Copyright © 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED