Daniel Tian’s Professional Blog

Icon

Just another WordPress.com weblog

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

In Part 2 of the tutorial, we implemented proper text wrapping to display text inside the text box. Now we’ll make the text ‘type out’.

Typing the text
So now that we finally have the text wrapping down, we can remove the red debug box. Comment out the Draw() line in the Draw() method that draws the text box. The words will still wrap properly, the only thing we turned off was the drawing of the text box. Now we’re ready to type the text. First we need to create some new instance variables:

Instance variables:

String parsedText;
String typedText;
double typedTextLength;
int delayInMilliseconds;
bool isDoneDrawing;

And then we set the variables, put this code in the LoadContent() method at the end:

parsedText = parseText(text);
delayInMilliseconds = 50;
isDoneDrawing = false;

What this does is it first parses the text into a correctly-formatted string. This is what the text will look like once it’s completely done typing. typedText will hold the text that should be displayed on every Update() call, and delayInMilliseconds is the amount of time to wait between each character displayed. The boolean is set to true once the entire string is displayed and is used in case the game logic needs to know when the text is done drawing (for example, to display a message that says ‘Click to continue’).

Now we just need to add some code to the Update() method, after the TODO comment and before base.Update(gameTime):

if (!isDoneDrawing)
{
    if (delayInMilliseconds == 0)
    {
        typedText = parsedText;
        isDoneDrawing = true;
    }
    else if (typedTextLength < parsedText.Length)
    {
        typedTextLength = typedTextLength + gameTime.ElapsedGameTime.TotalMilliseconds / delayInMilliseconds;

        if (typedTextLength >= parsedText.Length)
        {
            typedTextLength = parsedText.Length;
            isDoneDrawing = true;
        }

        typedText = parsedText.Substring(0, (int)typedTextLength);
    }
}

This code block might seem a little complicated compared to what we’ve been doing so far, but it’s quite easy to understand. First, it checks to see if isDoneDrawing is set to true. If it’s false, it then checks to see if the delay is 0. If it is, it displays the complete text string all at once. Otherwise, we check typedTextLength with parsedText.Length. If typedTextLength is less, that means the text isn’t fully printed out yet. The next few lines might seem confusing, but let me explain.

We want to type the text out character by character. The easiest way to do this is to simply add a character on every Update() call. However, there are some problems with this method. If you use the default 60 FPS in XNA, the fastest speed is 1 letter every 17 milliseconds. You can’t go any faster than that speed because it’s limited by the number of Update() calls. If we remove the polling rate limiter, then the text will display too fast. On my computer, removing the limit gives me 6000 FPS. That means that there are theoretically 6000 Update() calls per second (in real life the number of calls is lower because each call takes some time to process), causing the text to display instantaneously.

So what’s a solution that’s framerate-independent? The answer is to use the elapsed game time. Because the game time flows at the same rate regardless of the FPS, we’ll use it as our counter. Going back into the code, you can see that we have the variable typedTextLength. We have to use it because we need to check to see if this length is longer than the length of parsedText. If you try to print out a substring that is longer than the actual string length, it will cause an exception, so this check prevents us from doing so. We divide gameTime.ElapsedGameTime.TotalMilliseconds by delayInMilliseconds to get the number of characters we need to add. Assuming a delay of 50 milliseconds, for every 50 milliseconds that’s passed, we want to add one more character, and the division gives us the correct number of characters to add. This method works especially well for games that don’t limit the polling rate because each update will add a small amount to typedTextLength, but eventually they will add up to 1 and and the next character is displayed.

Ok, now we just need to change one last bit of code to see the effects. Change the DrawString call in the Draw() method so that it looks like this:

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

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

And there you have it, typewriter text box with proper word wrapping! If you run the program now, it should look like this:

textbox7

Conclusion
This conclues Part 3 of the tutorial. We made the text type out letter by letter with a delay that we set, and it works properly regardless of framerate. The next step is to put all the text box code into a separate class so it can be reused, but I’ll leave that as an exercise for you. Thank you for reading through my tutorial, and please leave me feedback!

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;
        String parsedText;
        String typedText;
        double typedTextLength;
        int delayInMilliseconds;
        bool isDoneDrawing;

        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";
            parsedText = parseText(text);
            delayInMilliseconds = 50;
            isDoneDrawing = false;
        }

        /// <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
            if (!isDoneDrawing)
            {
                if (delayInMilliseconds == 0)
                {
                    typedText = parsedText;
                    isDoneDrawing = true;
                }
                else if (typedTextLength < parsedText.Length)
                {
                    typedTextLength = typedTextLength + gameTime.ElapsedGameTime.TotalMilliseconds / delayInMilliseconds;

                    if (typedTextLength >= parsedText.Length)
                    {
                        typedTextLength = parsedText.Length;
                        isDoneDrawing = true;
                    }

                    typedText = parsedText.Substring(0, (int)typedTextLength);
                }
            }

            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, typedText, 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;
        }

    }
}

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

11 Responses

  1. […] XNA Tutorial: typewriter text box with proper word wrapping – Part 1 This is Part 1 of 3 and covers how to create a text box for the text to go in. Go to Part 2 Go to Part 3 […]

  2. […] 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 […]

  3. simsmaster says:

    BUG: When a Word is longer then the Textbox it´s draw´n out of Screen! But I´ve no Idea about a fix…

    P.S. I´m German, so be sorry for my bad English 😉

  4. Daniel Tian says:

    I think what you mean is that if a single word is longer than the width of the text box, it’ll get drawn outside of the text box’s boundaries. In that case, you’ll have to write code to do auto-hyphenation, which shouldn’t be too hard. Just check the width of the word, if it’s longer than the width of the entire text box, go letter by letter until you reach a certain amount, add a hyphen, then start a new line and finish the rest of the letters.

  5. Chris Peel says:

    How do I make this code work for GS2.0? Currently throwing up plenty of namespace errors

    • Daniel Tian says:

      Chris, it sounds like you copied and pasted the source code. You want to make sure the name of the project is the same as the name of the namespace. In my case, I called it ‘TextBox’, you’ll want to change it to the name of your project.

  6. Alain says:

    nice! really helps a lot thanks.

  7. Ryan says:

    Thanks for this very helpful! I’m going to expand on this so I could have stuff like this in my game with profile pictures, names, and have timing on there so you could wait for input and stuff like that! Thanks again!

  8. Andy says:

    I tried to make my own text scroll using this tutorial, but for some reason, it wants to generate a method stub for the parseText in the load content for parsedtext. Any ideas on how to fix that?

  9. jolouharris says:

    Amazing tutorial, exactly what I was looking for 🙂 Thank you so much ❤

  10. Michael Bonner says:

    Hey Daniel,

    Amazing tutorial, im 4th year in college and this is easily the best step by step tutorial i have seen!

    Thank you very much for sharing this and i hope u have lots more to come!

    kind Regards,
    Michael

Leave a reply to simsmaster Cancel reply