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:
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, box, scrolling, text, textbox, tutorial, typewriter, typing, visual novel, word, wrap, wrapping, xna