More Fun with SpriteFonts


A while ago, I posted about pre-rendering text to improve runtime performance when using XNA SpriteFonts. Since then, I’ve managed to complete a game (Robot Legions, currently up for peer review) using my XNA codebase and got to try out my text pre-rendering solution in a real project. It worked out pretty well, but there were a few features that I wished I had implemented ahead of time. Well, while Robot Legions was being play-tested, I went ahead and implemented some of those features. Now I’ll describe those features and share a bit of (hopefully) helpful code that will let you use them in your own games.

The core functionality of my text pre-rendering solution is drawing a given string to a texture using a given font. I’m then able to use that text texture anywhere in my game as I would any normal texture. This allows me to avoid the overhead of calling DrawString() for text in my game that doesn’t have to change often.

SpriteFont demoFont = Game.Content.Load<SpriteFont>("TextRenderingDemo/DemoFont");
Texture2D textTexture = textWriter.CreateTextSpriteTexture(demoFont, "The quick, brown fox jumped over Richter Belmont");

In addition to the core functionality, I added the following features:

Button Guides
The XBLIG Creator’s site provides images of the Xbox Controller’s buttons compatible with XNA’s SpriteFont for providing on-screen control instructions. While it’s possible to combine the button images with another sprite font image and use them as one SpriteFont, I prefer to combine the button images with fonts I choose at runtime. To do this, I prepared a version of my string rendering function with extra parameters for rendering the button image and positioning it relative to the text of my choice.

SpriteFont demoFont = Game.Content.Load<SpriteFont>("TextRenderingDemo/DemoFont");
SpriteFont buttonFont = Game.Content.Load<SpriteFont>("Fonts/xboxControllerSpriteFont");
Texture2D textTexture = textWriter.CreateButtonGuideTextSpriteTexture(demoFont, buttonFont, ": Do Something", "'", 0.5f, new Vector2(0.0f, 0.0f));

Drop Shadows
In a lot of cases, just rendering SpriteFonts as is tends to feel a little flat. Drawing a darkened version of the same text offset by a couple pixels before drawing the text in question goes a long way to helping it stand out. It’s a trick I use so often that I decided to bake drop shadows into my text pre-renderer. With this functionality, a call like this:

SpriteFont demoFont = Game.Content.Load<SpriteFont>("TextRenderingDemo/DemoFont");
List<TextWriterDropShadow> dropShadows = new List<TextWriterDropShadow>();
dropShadows.Add(new TextWriterDropShadow(new Vector2(2.0f, 2.0f), Color.Black));

Texture2D textTexture = textWriter.CreateTextSpriteTexture(demoFont, "The quick, brown fox jumped over Richter Belmont", Color.White, dropShadows);

Can give me text that looks like this:

Text Borders
You’ll notice that I pass lists of drop shadows to my sprite pre-renderer. It may seem like overkill, but being able to define a multiple drop shadows allows me to create various effects when rendering text. For example, I can define a list of drop-shadows like this:

SpriteFont demoFont = Game.Content.Load<SpriteFont>("TextRenderingDemo/DemoFont");
List<TextWriterDropShadow> dropShadows = new List<TextWriterDropShadow>();
dropShadows.Add(new TextWriterDropShadow(new Vector2(-1.0f, -1.0f), Color.Black));
dropShadows.Add(new TextWriterDropShadow(new Vector2(1.0f, -1.0f), Color.Black));
dropShadows.Add(new TextWriterDropShadow(new Vector2(-1.0f, 1.0f), Color.Black));
dropShadows.Add(new TextWriterDropShadow(new Vector2(1.0f, 1.0f), Color.Black));

dropShadows.Add(new TextWriterDropShadow(new Vector2(0.0f, -2.0f), Color.Black));
dropShadows.Add(new TextWriterDropShadow(new Vector2(0.0f, 2.0f), Color.Black));
dropShadows.Add(new TextWriterDropShadow(new Vector2(-2.0f, 0.0f), Color.Black));
dropShadows.Add(new TextWriterDropShadow(new Vector2(2.0f, 0.0f), Color.Black));

Texture2D textTexture = textWriter.CreateTextSpriteTexture(demoFont, "The quick, brown fox jumped over Richter Belmont", Color.White, dropShadows);

To give me fancy bordered text like this:

How’s that for pop?

Playing with the number of drop shadows, their offsets, and colors can allow you to produce a variety of decorations for your game’s text. Give it a try if you’re interested. You can find the code for my text pre-renderer below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace EstrellaLib.Sprite
{
    public class TextWriterDropShadow
    {
        public Vector2 Offset;
        public Color Color;

        public TextWriterDropShadow(Vector2 offset, Color color)
        {
            Offset = offset;
            Color = color;
        }
    }

    public class TextTextureWriter
    {
        GraphicsDevice _graphics;
        SpriteBatch _spriteBatch;

        public TextTextureWriter(GraphicsDevice graphics)
        {
            _graphics = graphics;
            _spriteBatch = new SpriteBatch(graphics);            
        }

        public Texture2D CreateTextSpriteTexture(SpriteFont spriteFont, string textString)
        {
            return CreateTextSpriteTexture(spriteFont, textString, Color.White, null);
        }

        public Texture2D CreateTextSpriteTexture(SpriteFont spriteFont, string textString, Color textColor, List<TextWriterDropShadow> dropShadows)
        {
            return RenderTextTexture(spriteFont, textString, textColor, null, "", 1.0f, Vector2.Zero, dropShadows);
        }

        public Texture2D CreateButtonGuideTextSpriteTexture(SpriteFont textSpriteFont, SpriteFont buttonGuideSpriteFont, string textString, string buttonGuideText, float buttonGuideScale, Vector2 buttonGuidePositionOffset)
        {
            return CreateButtonGuideTextSpriteTexture(textSpriteFont, buttonGuideSpriteFont, textString, buttonGuideText, buttonGuideScale, buttonGuidePositionOffset, Color.White, null);
        }

        public Texture2D CreateButtonGuideTextSpriteTexture(SpriteFont textSpriteFont, SpriteFont buttonGuideSpriteFont, string textString, string buttonGuideText, float buttonGuideScale, Vector2 buttonGuidePositionOffset, Color textColor, List<TextWriterDropShadow> dropShadows)
        {
            return RenderTextTexture(textSpriteFont, textString, textColor, buttonGuideSpriteFont, buttonGuideText, buttonGuideScale, buttonGuidePositionOffset, dropShadows);
        }

        private Texture2D RenderTextTexture(SpriteFont textSpriteFont, string textString, Color textColor, SpriteFont buttonGuideSpriteFont, string buttonGuideText, float buttonGuideScale, Vector2 buttonGuidePositionOffset, List<TextWriterDropShadow> dropShadows)
        {
            if (textSpriteFont != null)
            {
                Vector2 stringDimensions = textSpriteFont.MeasureString(textString);

                float buttonHorizontalShift = 0.0f;
                Vector2 buttonTextDimensions = Vector2.Zero;
                if (buttonGuideSpriteFont != null)
                {
                    buttonTextDimensions = buttonGuideSpriteFont.MeasureString(buttonGuideText);
                    buttonTextDimensions.X *= buttonGuideScale;

                    buttonTextDimensions.X += Math.Abs(buttonGuidePositionOffset.X);
                    if (buttonGuidePositionOffset.X < 0.0f)
                    {
                        buttonHorizontalShift = -buttonGuidePositionOffset.X;
                    }

                    //Eliminate potential non-integral values that could result in lost pixels on texture render
                    buttonTextDimensions.X = (float)Math.Ceiling(buttonTextDimensions.X);
                }

                Vector2 dropShadowPositionAdjust = Vector2.Zero;
                Vector2 dropShadowSizeAdjust = Vector2.Zero;
                if (dropShadows != null)
                {
                    Vector2 lowestVals = Vector2.Zero;
                    Vector2 hightestVals = Vector2.Zero;
                    foreach (TextWriterDropShadow dropShadow in dropShadows)
                    {
                        if( dropShadow.Offset.X < 0.0f )
                            lowestVals.X = Math.Min(lowestVals.X, dropShadow.Offset.X);

                        if (dropShadow.Offset.Y < 0.0f)
                            lowestVals.Y = Math.Min(lowestVals.Y, dropShadow.Offset.Y);

                        if (dropShadow.Offset.X > 0.0f)
                            hightestVals.X = Math.Max(hightestVals.X, dropShadow.Offset.X);

                        if (dropShadow.Offset.Y > 0.0f)
                            hightestVals.Y = Math.Max(hightestVals.Y, dropShadow.Offset.Y);
                    }

                    dropShadowPositionAdjust.X = Math.Abs(lowestVals.X);
                    dropShadowPositionAdjust.Y = Math.Abs(lowestVals.Y);

                    dropShadowSizeAdjust.X = Math.Abs(lowestVals.X) + Math.Abs(hightestVals.X);
                    dropShadowSizeAdjust.Y = Math.Abs(lowestVals.Y) + Math.Abs(hightestVals.Y);
                }

                RenderTarget2D renderTarget = new RenderTarget2D(_graphics, (int)stringDimensions.X + (int)buttonTextDimensions.X + (int)dropShadowSizeAdjust.X, (int)stringDimensions.Y + (int)dropShadowSizeAdjust.Y);

                _graphics.SetRenderTarget(renderTarget);
                _graphics.Clear(Color.Transparent);
                _spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
                
                if (buttonGuideSpriteFont != null)
                {
                    _spriteBatch.DrawString(buttonGuideSpriteFont, buttonGuideText, dropShadowPositionAdjust + new Vector2(buttonHorizontalShift, buttonTextDimensions.Y * -0.25f * buttonGuideScale) + buttonGuidePositionOffset, Color.White, 0.0f, Vector2.Zero, buttonGuideScale, SpriteEffects.None, 1.0f);
                }

                Vector2 textPosition = dropShadowPositionAdjust + new Vector2(buttonHorizontalShift + buttonTextDimensions.X, 0.0f);
                if (dropShadows != null)
                {
                    foreach (TextWriterDropShadow dropShadow in dropShadows)
                    {
                        _spriteBatch.DrawString(textSpriteFont, textString, textPosition + dropShadow.Offset, dropShadow.Color);
                    }
                }

                _spriteBatch.DrawString(textSpriteFont, textString, textPosition, textColor);
                
                _spriteBatch.End();
                _graphics.SetRenderTarget(null);

                return renderTarget;
            }
            return null;
        }
    }
}

/* Button Guide Mapping
Character	Image
Space       Left Thumbstick
!	        Directional Pad
"	        Right Thumbstick
#	        BACK
$	        Guide
%	        START
&	        X
'	        A
(	        Y
)	        B
*	        Right Shoulder
+	        Right Trigger
,	        Left Trigger
-	        Left Shoulder
*/
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print