One function most games need is the ability to display information to the user using text and numbers. It’s not uncommon for graphics libraries to have support for TTF fonts (see SFML and SDL). However, given the style of game I’m working on, what I would prefer is a bitmap font.
Just type “video game bitmap fonts” into Google image search and you can see how different they are compared to TTF. If you’re making a low resolution or pixel style game then bitmap fonts really fit the theme.
I’ve found libraries that deal with TTF fonts. I’ve also found programs that will supposedly turn TTF fonts into bitmap fonts. But the fonts I want are built from the ground up as a bitmap image, and I haven’t really found any way of using bitmap fonts (with SFML .NET specifically) within a project that satisfies that.
It seemed like a decent sized task to work on myself, so I took a stab at it.
Design
The requirements I wanted for a bitmap font are:
- Stored in PNG format
- The PNG file contains all the information needed for the font to work, no extra metadata/information from external files is needed
- ASCII (extended) based
- Variable-width text support
- Support for color masking the font
- All characters in the source image are on a single line
The second requirement may not be possible for all, but the information I needed was able to fit into the image. The values I needed were: How tall each character is, the delimiter color that separates the characters, the background/mask color, whether or not the background color should be masked, character spacing (pixels), and line spacing (pixels). All of the values are read in by my code, even though they are not all (currently) used.
My code expects a small rectangle of pixels in the top-left of the image to be used for those values. Here’s a zoomed in image of the corner pixels in paint.net:
The top-left pixel (0, 0) is the delimiter color, the color that separates the characters in the image from one another.
The pixel below the top-left one (0, 1) uses the RGB channels to specify separate options. R = character spacing in pixels. G = line spacing in pixels. B = whether or not to mask the background color (0 means do not mask, all other values mean mask).
The next pixel down (0, 2) is the mask color. This color is the “background” color of the image that will be turned transparent should the masking flag be set. (The background for the production font image has a black background, I made it transparent here so the value-containing pixels are easier to see)
Currently those are the only pixels that are used. I’m currently thinking that I’ll reserve the top-left 2×3 pixels for future use. I might even expand that area to allow for user-values.
While the character width is variable, the character height is uniform from a pixel-area viewpoint. This height of the characters is determined by the height of the image. This doesn’t mean that to the end-user a period will be as tall as a capital M though, see the screenshot of the font I created below for an example.
The red line going from (3, 0) to (3, 4) is mostly decorative. Only the single pixel at the top actually matters, as it marks the beginning of the portion of the image that contains the characters. That position is actually hard-coded in as the start of the characters, the rest of the delimiter positions are found programmatically. However, for all delimiters, the pixels directly below the delimiter are not looked at or included in any rendered text, so I use different heights to group my characters into 5s and 10s.
Example
Here’s a portion of how the image looks while editing it (I’m not a graphic artist, it shows):
I’d like to point out some things here. First, the indices of the characters are ASCII based, that’s why they’re in the order they’re in. That means that you’ll have several empty “characters” at the beginning before you can start drawing the actual characters you’ll use. I just made those empty ones one pixel wide with nothing between them.
Second, notice the empty space above/below the characters. Technically the capital X, lower case e, and lower case j all have the same height in pixels. It’s up to the font designer to design for that empty space.
Third, the gray area at the bottom of the image is meant to visually indicate the area of the characters that is reserved for descenders. Notice how only the lower case g and j have portions that go into that space. That gray area is on a separate layer in my paint.net file that gets turned off when I export the image as a PNG, otherwise that portion would not get masked. If you needed to include space above for diacritic marks (such as Ä), you can make room for that too.
Code
Above I listed all characters being on the same line as a requirement. That accomplishes two things: 1) makes the code that reads in and processes the font easier to write; 2) allows the height of the image to define the height of the font.
So the basic logic of the code is as follows:
- Open the image file and read its contents into memory
- Grab all the values defined in the top-left portion of the image.
- Scan through the rest of the image and store the location/size info of each character
The first two items are relatively trivial. Those occur in the constructor of the font (lines 3-9):
public BitmapFont(string filename) { this._sourceImage = new Image(filename); this._delimiterColor = this._sourceImage.GetPixel(0, 0); this._maskColor = this._sourceImage.GetPixel(0, 2); var options = this._sourceImage.GetPixel(0, 1); this._charSpacing = options.R; this._lineSpacing = options.G; this._isImageKeyed = options.B > 0; this.ProcessImage(); this._renderTexture = new RenderTexture(1000, this._sourceImage.Size.Y); this.StringSprite = new Sprite(this._renderTexture.Texture); }The “chopping up” portion is in a separate method called by the constructor:
private void ProcessImage() { uint left = StartPixel; uint count = 0; //used for indexing ascii values for (uint x = StartPixel; x < this._sourceImage.Size.X; x++) { var clr = this._sourceImage.GetPixel(x, 0); if (clr.Equals(this._delimiterColor) && count < NumAsciiChars) { var ir = new IntRect((int)left, 0, (int)(x - left), (int)this._sourceImage.Size.Y); this._originalLetterPositions[count] = ir; left = x + 1; count++; } if (count >= NumAsciiChars) break; } if (this._isImageKeyed) this._sourceImage.CreateMaskFromColor(this._maskColor); this._sourceTexture = new Texture(this._sourceImage); }The bulk of this method is for finding the rectangles that define the characters.
StartPixel
is a const value of 3. It’s the pixel to the right of the hard-coded “start” point of the characters.NumAsciiCharacters
is a const value of 256. That’s also the size of the_originalLetterPositions
array.Finally, if the constructor found that the image is supposed to have a transparent background, it applies the key.
As far as drawing text goes, the project I was making only required writing text onto a single line. No line-breaks, truncating text to fit within a certain area, etc. So That’s all my drawing function does. The plan for that was to have the
BitmapFont
class draw the text with a single method call which draws the string onto a texture that the user then displays where desired. Leaving out all of the framework overhead and just getting to the meat of how a string is drawn, it pretty much comes down to this loop:float xpos = 0; //horizontal position of current letter var letterSprite = new Sprite(this._sourceTexture); foreach (char c in str) { var rekt = this._originalLetterPositions; letterSprite.TextureRect = rekt; letterSprite.Position = new Vector2f(xpos, 0); xpos += rekt.Width + this._charSpacing; letterSprite.Draw(this._renderTexture, RenderStates.Default); }Naturally there is work that needs to be done to ensure that the
_renderTexture
is large enough to accommodate the string to be drawn, dispose things we don’t need any more, “finalize” the drawing process, etc. But I left those portions out as they seemed more like framework specific boiler-plate type code that would get in the way of the part of the code we’re interested in.Result
Here’s a screenshot of what the font looks like in action:
You can also change the color of the font:
I left scaling the font, setting the position, and setting the color/tint of the drawn string out of the
BitmapFont
class. To handle those you can just set theScale
,Position
, andColor
properties of theStringSprite
property/object within theBitmapFont
class. Some examples:private void SetScoreTint() { var pc = this._gameOver ? Color.White : this.Player.Color; //red tints a little darker than cyan, so adjust for that var alpha = (byte)(pc.Equals(Color.Red) ? 0x44 : 0x33); //this._bf is an instance of BitmapFont this._bf.StringSprite.Color = new Color(pc.R, pc.G, pc.B, alpha); }private void DrawHeader(RenderTarget target) { this._bf.RenderText("STATS"); this._bf.StringSprite.Scale = TitleScale; var x = (target.Size.X - this._bf.ScreenSize.X)/2; this._bf.StringSprite.Position = new Vector2f(x, 20); target.Draw(this._bf.StringSprite); }Notice that the second method references
ScreenSize
on theBitmapFont
instance. That’s a property that returns the rendered string size in pixels as aVector2f
. It takes care of doing the math involved when the scale of theStringSprite
is not 1.Also worth noting: you can replace any ASCII character with any small sprite you want. Say for example you want to use a small heart symbol to denote HP for your character, and you don’t expect to ever display the backtick (`) character to the user. You could simply replace that character in the font image with the sprite of your heart, like so (beware of tint/color issues if you do this though):
Then if you want to render a quantity of HP with the heart symbol next to it you would just write:
this._bf.RenderText("`100");
You can use the font being stored in PNG format to your advantage in other ways too. Say for example you don’t expect to use several of the characters like {, }, ~, ^, *, |, \, etc. You can just leave those blank. If you’re not going to use them, why bother filling them in? Likewise maybe you need a font just for the numbers 0-9, leave every space in the image blank except those 10 digits. Maybe you want a font that’s all caps but don’t want to rely on remembering to write code to do that, just copy the upper-case letters to the lower-case letter’s spots in the PNG. That way should you decide later you do actually want different upper/lower case letters, you just have to swap the PNG file out.
I have the full source code available in a gist here.