Daniel Tian’s Professional Blog

Icon

Just another WordPress.com weblog

XNA Tutorial: typewriter text box with proper word wrapping – Part 2

This is Part 2 of 3 and covers how to display text inside the text box created in Part 1.
Go to Part 1
Go to Part 3

In Part 1 of this tutorial, we created the text box using a Rectangle object and drew it to the screen. Now we’ll put some text into the text box.

Drawing the text
Drawing text in XNA is incredibly easy. First we need to create a SpriteFont object. A SpriteFont is a class provided by XNA that greatly simplifies displaying text by treating fonts as sprites.

To use a SpriteFont, go to Visual C# and right click on Content in the Solution Explorer, then pick Add -> New Item, pick Sprite Font and change the name to font.spritefont.

Now let’s set up the font. Double-click on font.spritefont in the Solution Explorer. You’ll see that it’s actually a XML file that defines the font properties. For now let’s change the FontName to Arial, then save the file. Now that we have the font set up, we can treat it almost exactly like a sprite. Add the following code into Game1.cs:

Instance variable:

SpriteFont font;
String text;

In LoadContent(), under debugColor:

font = Content.Load<SpriteFont>("font");
text = "Hello world!";

In Draw(), under the first spriteBatch.Draw() but before spriteBatch.End():

spriteBatch.DrawString(font, text, new Vector2(textBox.X, textBox.Y), Color.White);

As you can see, it’s almost exactly the same as drawing a sprite. This is where using a Rectangle for the text box shows its second usefulness. Rather than providing a concrete Vector2 position, we just tell it to use the X and Y position of the text box. If we want to change the text box position later on, all we have to do is change the position of the Rectangle. If you run the program now, it should look like this:

textbox2

Not too bad-looking, as long as you’re only displaying one line of text. Let’s see what happens though if we change the string to something longer:

textbox3

Aha, what happened here? We can see that the text isn’t word-wrapping to the text box. If you’ve understood the tutorial up till now, it should be obvious why this is the case. The text box and the sprite font are two different sprites and therefore have no effect on each other’s boundaries. Unfortunately, unlike regular sprites, we can’t define a Rectangle boundary for the sprite font, so we’ll have to manually wrap the text ourselves.

Wrapping the text – Brainstorming
Before we start wrapping the text, let’s think for a moment about how we can do it. The first thing you might think of is to print out the characters until the number of characters exceeds a certain amount, then insert a newline. Unfortunately this doesn’t work because it will start wrapping in the middle of words:

textbox4

The next solution you might come up with is to wrap on the words rather than the characters. In other words, count the number of characters in the word and add it to the number of characters on the current line. If it exceeds a certain amount, then put the word on the next line. This works fine for monospaced fonts (fonts where all the characters are the same width, like Courier New) but doesn’t work very well for non-monospaced fonts. Take this example:

textbox53

You can clearly see that the word ‘inside’ is a lot narrower than the word ‘window’, but they are both 6 characters each. We can’t tell the width of the string simply based on the number of characters, meaning that we can’t word wrap this way or else we run the risk of certain lines going out of bounds and other lines wrapping too early.

So how do we fix this problem? By now you’re probably out of ideas, decided to stick with only monospaced fonts, or came up with unorthodox solutions such as using an associative array with each character as the key and the width as the value (grossly inefficient, by the way). Fortunately, XNA provides us with the tools we need to make this process a cinch.

Wrapping the text – Implementation
Fortunately for us, XNA provides a function to easily measure the length of a string in pixels. Instead of counting the number of characters in each word, we get the pixel width of each word and see if the current line width plus the word with exceeds the width of the text box. To do this, let’s create a new method called ParseText. Put this new method right after the Draw() method:

private String parseText(String text)
{
    String line = String.Empty;
    String returnString = String.Empty;
    String[] wordArray = text.Split(' ');

    foreach (String word in wordArray)
    {
        if (font.MeasureString(line + word).Length() > textBox.Width)
        {
            returnString = returnString + line + '\n';
            line = String.Empty;
        }
        
        line = line + word + ' ';
    }

    return returnString + line;
}

What this code will do is insert a newline once the length of the current line plus the length of the current word is longer than the width of the text box, and it repeats until there are no more words to process. Here we see the third advantage of using a Rectangle for the text box. We can use the text box’s width as our word wrap point, meaning that we can easily change the text box size and still have the word wrap behave correctly.

Now we just need to change our spriteBatch.DrawString call in the Draw() method so that it uses our new parseText method:

From:
spriteBatch.DrawString(font, text, new Vector2(textBox.X, textBox.Y), Color.White);

To:
spriteBatch.DrawString(font, parseText(text), new Vector2(textBox.X, textBox.Y), Color.White);

If you run the program now, you should see this (feel free to change the text to whatever you want):

textbox6

The text is now properly wrapped!

Conclusion
This concludes Part 2 of the tutorial. We displayed some text with proper word wrapping inside the text box and the text box position and size can be changed without messing up the wrapping. Part 3 will cover how to make the text ‘type out’ letter by letter.
Go to Part 3

Source code
This is what your Game1.cs should look like at the end of this tutorial.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace TextBox
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Rectangle textBox;
        Texture2D debugColor;
        SpriteFont font;
        String text;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            textBox = new Rectangle(10, 10, 300, 300);

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            debugColor = Content.Load<Texture2D>("solidred");
            font = Content.Load<SpriteFont>("font");
            text = "This is a properly implemented word wrapped sentence in XNA using SpriteFont.MeasureString. A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z";
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here
            spriteBatch.Begin();
            spriteBatch.Draw(debugColor, textBox, Color.White);
            spriteBatch.DrawString(font, parseText(text), new Vector2(textBox.X, textBox.Y), Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }

        private String parseText(String text)
        {
            String line = String.Empty;
            String returnString = String.Empty;
            String[] wordArray = text.Split(' ');

            foreach (String word in wordArray)
            {
                if (font.MeasureString(line + word).Length() > textBox.Width)
                {
                    returnString = returnString + line + '\n';
                    line = String.Empty;
                }

                line = line + word + ' ';
            }

            return returnString + line;
        }

    }
}
Advertisements

Filed under: XNA Tutorials, , , , , , , , , , , ,

5 Responses

  1. […] – Part 1 This is Part 1 of 3 and covers how to create a text box for the text to go in. Part 2 Part […]

  2. […] XNA Tutorial: typewriter text box with proper word wrapping – Part 3 This is Part 3 of 3 and covers how to make the text ‘type out’ letter by letter. Go to Part 1 Go to Part 2 […]

  3. Dan Colasanti says:

    Thanks for the tutorial. While this works fine for a Windows build, it doesn’t display properly on Windows Phone 7 (the CornflowerBlue background is displayed, but the text does not display even though I have verified that the DrawString calls are being made with the correct typedText strings). Do you have any idea why this might be happening?

    Thanks,
    Dan

    • Daniel Tian says:

      Sorry, I’m not sure what the problem is. My best guess is that you’re using XNA 4.0 and that DrawString()’s implementation has been changed for 4.0. This tutorial was written for 3.x.

  4. Des says:

    Just a small point, you use (font.MeasureString(line + word).Length(). This finds the diagonal length of a string (ie, from the top left to the bottom right) which is larger than its width. This would only cause a noticeable error if a large font size was used. You should use .X rather than .Length.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: