
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
*/








