10 min read

(For more resources on Microsoft XNA 4.0, see here.)

Adding text fields

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.

Getting ready

This recipe only relies upon the presence of a single SpriteFont file referring to any font at any desired size.

How to do it…

To start adding textboxes to your own games:

  1. Add a 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>&#32;</Start>
            <End>&#126;</End>
          </CharacterRegion>
        </CharacterRegions>
      </Asset>
    </XnaContent>

  2. Begin a new class to contain the text field logic, and embed a static mapping of the keyboard keys to their respective text characters:

    class Textbox
    {
           
        private static Dictionary<Keys, char> characterByKey;

  3. Create a static constructor to load the mapping of keys to characters:

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

  4. Add the public instance fields that will determine the look and content of the display:

    public StringBuilder Text;
    public Vector2 Position;
    public Color ForegroundColor;
    public Color BackgroundColor;
    public bool HasFocus;

  5. Include instance-level references to the objects used in the rendering:

    GraphicsDevice graphicsDevice;
    SpriteFont font;
    SpriteBatch spriteBatch;        
    RenderTarget2D renderTarget;
    KeyboardState lastKeyboard;
    bool renderIsDirty = true;

  6. Begin the instance constructor by measuring the overall height of some key characters to determine the required height of the display, and create a render target to match:

    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);

  7. Complete the constructor by instantiating the text container and SpriteBatch:

        Text = new StringBuilder();
        this.graphicsDevice = graphicsDevice;
        spriteBatch = new SpriteBatch(graphicsDevice);
    }

  8. Begin the Update() method by determining if we need to take any notice of the keyboard:

    public void Update(GameTime gameTime)
    {
        if (!HasFocus)
        {
            return;
        }

  9. Retrieve all of the keys that are currently being depressed by the player and iterate through them, ignoring any that have been held down since the last update:

    var keyboard = Keyboard.GetState();
    foreach (var key in keyboard.GetPressedKeys())
    {
        if (!lastKeyboard.IsKeyUp(key))
        {
            continue;
        }

  10. Add the logic to remove a character from the end of the text, if either the Backspace or Delete key has been pressed:

    if (key == Keys.Delete ||
        key == Keys.Back)
    {
        if (Text.Length == 0)
        {
            continue;
        }
        Text.Length--;
        renderIsDirty = true;
        continue;
    }

  11. Complete the loop and the method by adding the corresponding character for any keys we recognize, taking note of the case as we do so:

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

  12. Add a new method to render the contents of the text field to 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);
    }

  13. Complete the class by adding a method to render the image of RenderTarget to the screen:

    public void Draw()
    {
        spriteBatch.Begin();
        spriteBatch.Draw(renderTarget, Position, Color.White);
        spriteBatch.End();
    }

  14. In your game’s 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
        };
    }

  15. Ensure that the text field is updated regularly via your game’s Update() method:

    protected override void Update(GameTime gameTime)
    {
        textbox.Update(gameTime);
        base.Update(gameTime);
    }

  16. In your game’s 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:

How it works…

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.

There’s more…

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.

On the phone

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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here