(For more resources on Microsoft XNA 4.0, see here.)
I'm generally a big fan of having as few text fields in an application as possible, and this holds doubly true for a game but there are some occasions when receiving some sort of textual information from the player is required so in these regrettable occasions, a textbox or field may be an appropriate choice.
Unfortunately, a premade textbox isn't always available to us on any given gaming project, so sometimes we must create our own.
This recipe only relies upon the presence of a single SpriteFont
file referring to any font at any desired size.
To start adding textboxes to your own games:
SpriteFont
to the solution named Text
:
<?xml version="1.0" encoding="utf-8"?>
<XnaContent >
<FontName>Segoe UI Mono</FontName>
<Size>28</Size>
<Spacing>0</Spacing>
<UseKerning>true</UseKerning>
<Style>Regular</Style>
<CharacterRegions>
<CharacterRegion>
<Start> </Start>
<End>~</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>
class Textbox
{
private static Dictionary<Keys, char> characterByKey;
static Textbox()
{
characterByKey = new Dictionary<Keys, char>()
{
{Keys.A, 'a'},
{Keys.B, 'b'},
{Keys.C, 'c'},
{Keys.D, 'd'},
{Keys.E, 'e'},
{Keys.F, 'f'},
{Keys.G, 'g'},
{Keys.H, 'h'},
{Keys.I, 'i'},
{Keys.J, 'j'},
{Keys.K, 'k'},
{Keys.L, 'l'},
{Keys.M, 'm'},
{Keys.N, 'n'},
{Keys.O, 'o'},
{Keys.P, 'p'},
{Keys.Q, 'q'},
{Keys.R, 'r'},
{Keys.S, 's'},
{Keys.T, 't'},
{Keys.U, 'u'},
{Keys.V, 'v'},
{Keys.W, 'w'},
{Keys.X, 'x'},
{Keys.Y, 'y'},
{Keys.Z, 'z'},
{Keys.D0, '0'},
{Keys.D1, '1'},
{Keys.D2, '2'},
{Keys.D3, '3'},
{Keys.D4, '4'},
{Keys.D5, '5'},
{Keys.D6, '6'},
{Keys.D7, '7'},
{Keys.D8, '8'},
{Keys.D9, '9'},
{Keys.NumPad0, '0'},
{Keys.NumPad1, '1'},
{Keys.NumPad2, '2'},
{Keys.NumPad3, '3'},
{Keys.NumPad4, '4'},
{Keys.NumPad5, '5'},
{Keys.NumPad6, '6'},
{Keys.NumPad7, '7'},
{Keys.NumPad8, '8'},
{Keys.NumPad9, '9'},
{Keys.OemPeriod, '.'},
{Keys.OemMinus, '-'},
{Keys.Space, ' '}
};
}
public StringBuilder Text;
public Vector2 Position;
public Color ForegroundColor;
public Color BackgroundColor;
public bool HasFocus;
GraphicsDevice graphicsDevice;
SpriteFont font;
SpriteBatch spriteBatch;
RenderTarget2D renderTarget;
KeyboardState lastKeyboard;
bool renderIsDirty = true;
public Textbox(GraphicsDevice graphicsDevice, int width,
SpriteFont font)
{
this.font = font;
var fontMeasurements = font.MeasureString("dfgjlJL");
var height = (int)fontMeasurements.Y;
var pp = graphicsDevice.PresentationParameters;
renderTarget = new RenderTarget2D(graphicsDevice,
width,
height,
false, pp.BackBufferFormat, pp.DepthStencilFormat);
SpriteBatch
:
Text = new StringBuilder();
this.graphicsDevice = graphicsDevice;
spriteBatch = new SpriteBatch(graphicsDevice);
}
Update()
method by determining if we need to take any notice of the keyboard:
public void Update(GameTime gameTime)
{
if (!HasFocus)
{
return;
}
var keyboard = Keyboard.GetState();
foreach (var key in keyboard.GetPressedKeys())
{
if (!lastKeyboard.IsKeyUp(key))
{
continue;
}
if (key == Keys.Delete ||
key == Keys.Back)
{
if (Text.Length == 0)
{
continue;
}
Text.Length--;
renderIsDirty = true;
continue;
}
char character;
if (!characterByKey.TryGetValue(key, out character))
{
continue;
}
if (keyboard.IsKeyDown(Keys.LeftShift) ||
keyboard.IsKeyDown(Keys.RightShift))
{
character = Char.ToUpper(character);
}
Text.Append(character);
renderIsDirty = true;
}
lastKeyboard = keyboard;
}
RenderTarget
if it has changed:
public void PreDraw()
{
if (!renderIsDirty)
{
return;
}
renderIsDirty = false;
var existingRenderTargets = graphicsDevice.GetRenderTargets();
graphicsDevice.SetRenderTarget(renderTarget);
spriteBatch.Begin();
graphicsDevice.Clear(BackgroundColor);
spriteBatch.DrawString(
font, Text,
Vector2.Zero, ForegroundColor);
spriteBatch.End();
graphicsDevice.SetRenderTargets(existingRenderTargets);
}
RenderTarget
to the screen:
public void Draw()
{
spriteBatch.Begin();
spriteBatch.Draw(renderTarget, Position, Color.White);
spriteBatch.End();
}
LoadContent()
method, create a new instance of the text field:
Textbox textbox;
protected override void LoadContent()
{
textbox = new Textbox(
GraphicsDevice,
400,
Content.Load<SpriteFont>("Text"))
{
ForegroundColor = Color.YellowGreen,
BackgroundColor = Color.DarkGreen,
Position = new Vector2(100,100),
HasFocus = true
};
}
Update()
method:
protected override void Update(GameTime gameTime)
{
textbox.Update(gameTime);
base.Update(gameTime);
}
Draw()
method, let the text field perform its RenderTarget
updates prior to rendering the scene including the text fi eld:
protected override void Draw(GameTime gameTime)
{
textbox.PreDraw();
GraphicsDevice.Clear(Color.Black);
textbox.Draw();
base.Draw(gameTime);
}
Running the code should deliver a brand new textbox just waiting for some interesting text like the following:
In the Update()
method, we retrieve a list of all of the keys that are being depressed by the player at that particular moment in time.
Comparing this list to the list we captured in the previous update cycle allows us to determine which keys have only just been depressed.
Next, via a dictionary, we translate the newly depressed keys into characters and append them onto a StringBuilder
instance.
We could have just as easily used a regular string, but due to the nature of string handling in .NET, the StringBuilder
class is a lot more efficient in terms of memory use and garbage creation.
We could have also rendered our text directly to the screen, but it turns out that drawing text is a mildly expensive process, with each letter being placed on the screen as an individual image. So in order to minimize the cost and give the rest of our game as much processing power as possible, we render the text to RenderTarget
only when the text changes, and just keep on displaying RenderTarget
on screen during all those cycles when no changes occur.
If you're constructing a screen that has more than one text field on it, you'll find the HasFocus
state of the text field implementation to be a handy addition. This will allow you to restrict the keyboard input only to one text field at a time.
In the case of multiple text fields, I'd recommend taking a leaf from the operating system UI handbooks and adding some highlighting around the edges of a text field to clearly indicate which text field has focus.
Also, the addition of a visible text cursor at the end of any text within a text field with focus may help draw the player's eyes to the correct spot.
If you do have access to a built-in text field control such as the one provided in the "Windows Phone XNA and Silverlight" project type, but still wish to render the control yourself, I recommend experimenting with enabling the prebuilt control, making it invisible, and feeding the text from it into your own text field display.