• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Working with TTF fonts —
It would be nice if there was another way to draw our text strings, without needing to create a short-lived texture. In this tutorial, we'll do just that, by creating a single texture onto which we'll place all our glyphs. We'll then pick our the glyphs we want to use to draw our text. It'll be like creating the Sprite Atlas we looked at in a previous tutorial, except without the packing aspect; this will be a little simpler.
Extract the archive, run make, and then use ./ttf02 to run the code. You will see a window open like the one above, with a number of lines of text, including two at the bottom that are showing incrementing numbers. Close the window to exit.
Inspecting the code
The major changes in this tutorial have been done in text.c and demo.c. We'll start with text.c, as this deals with the actual setup and use of our font atlas. A quick note: we've added an enum to defs.h to define the fonts we'll be using.
Now onto text.c. One of the first things that has changed is that we're now supporting multiple fonts. As such, the TTF_FONT static variable is now an array. We've also introduced two new arrays, glyphs and fontTexture. These variables will be used to hold the glyph atlas positions, as well as the font texture we will be holding for each font.
Our glyphs array is 128 entries wide (according to the NUM_GLYPHS define in the header), enough to hold the entire ASCII table. We won't be working with all of these, however, just a subset of the printable characters that we'll be working with. The initFonts now creates the font atlas for both our fonts, by making a call to initFont:
The initFont function takes the font type (the enum value, which in itself can be expressed as an int), and the filename we want to use. The initFont function itself is quite meaty, but is the most important part of this tutorial, as it is where we will set up our font atlas. Take a look at the listing below and we'll then go through it.
Working our way down from the top. We're first setting up a few variables to work with - two SDL_Surfaces, two SDL_Rects, and a char array. Notice how the char array is only 2 items wide. We'll come to this in a moment.
The next thing we want to do is zero the memory for our array of glyths, using memset, for the font we're working with. We do this with the value of NUM_GLYPHS, to marry up with the size of the array. With that done, we open our file, once again at the index for the font we want to work with, and for the filename argument specified, and our fixed FONT_SIZE define.
Now for an important bit. We create an SDL_Surface of the size of our defined FONT_TEXTURE_SIZE. In this case, FONT_TEXTURE_SIZE is 512, resulting in a square power-of-two texture. The arguments and SDL_SetColorKey will help to ensure that our surface is transparent, which will be important when using the texture. It will be no good if our text always had an opaque background.
With our prep down, we're ready to start drawing our glyphs on the atlas. Our dest SDL_Rect will be used to determine the rectangular region we will be drawing the glyph at. We set the dest's x and y to 0,0 to start with, then begin iterating through a subset of the ASCII table. Working from ' ' (a space) to 'z' will see us starting from 32 to 122 in integer values and present all the alphanumeric values we want, along with some symbols and punctuation. For each iteration of the loop, we set the first index of c to the value of i, and the second index to 0. In effect, this will create a char array holding a single ASCII value, followed by a NULL terminator. The NULL terminator is important for the next step.
We next call TTF_RenderUTF8_Blended, passing in our one character string (c), the font we're working with, and our pre-defined white colour. A short SDL_TTF string, composed of a single character. Now for another important bit - we want to find out the dimensions of the glyph we've just created, so we can know if we can place it on the atlas at the desired coordinates. TTF_SizeText does this for us. It takes as its arguments the font, the character string (in our case, this will be the single character), and variables to hold the width and height. Notice how we're passing the dest's w and h variables by reference, so the values can be set.
Now that we know the width and height of the glyph, we can test whether it will bit onto the atlas. When creating this font atlas, we will be adding the glyphs from left to right, top to bottom. To begin with, we ask whether the current dest.x plus the dest.w is greater than the defined FONT_TEXTURE_SIZE. What this is asking if whether the rectangular region of the glyph will fit cleanly onto the atlas. Assuming it does, we'll be skipping the down to the actual blitting operation. Using SDL_BlitSurface, we render our glyph to our atlas, and then store the position in our glyphs array, for the font. We do this by grabbing a reference to the item (using i as the index) and setting its x, y, w, and h values to the same as our dest's. Finally, we free the surface, since we no longer need it, and then increment the value of dest.x by dest.w, effectively moving it along the line.
But what if the previous test of dest.x + dest.y is greater than FONT_TEXTURE_SIZE? Returning to the condition test, we see that we set dest.x to 0. This is basically positoning it at the left-hand side of the texture once again. We then increment the value of dest.y by dest.h. We've moved down a row! Think of word-wrapping in a word processor (or other editor) of how if a word does not fit onto a line, it jumps down to the start of the next line. We're doing the same thing here. But what if we run out of space on the texture? We test to see if dest.y plus dest.h also exceeds FONT_TEXTURE_SIZE. If so, we're out of luck. Our texture is full. You can try this yourself by setting FONT_TEXTURE_SIZE in text.h to a larger value. At this point, we're just going to exit the program, since things didn't go as planned.
Once our loop is done, we at last have our font atlas. We feed the surface into our toTexture function, and assign it to the fontTextures entry for the fontType.
Phew! That was indeed a meaty function, although I'm sure you'll see now that it's quite logical and straightforward.
That's our font atlas created, but how can we actually use it for rendering text? That's actually rather simple, as we can see below:
This drawText function will allow us to render text at a specific location, for a colour we desire, using one of our fonts. Again, let's go through it step by step. One of the first things we do is apply the desired colour to the font atlas, for the specified font (fontType). SDL_SetTextureColorMod will use our r, g, and b values to set the colour. After that, we will step through each character in the text string that we have passed into the function. Notice how when indexing text, we're grabbing the value (character) as an int, not a char. This is so we can match the character value to the glyph entry, for the designed font.
With our glyph selected, we set up our dest rectangle to blit the area. We base dest's x and y variables on the x and y that were passed into the function, but use the glyph's width and height (w and h) for the dest's w and h. This will be telling SDL that we want to render a rectangle the size of the glyph. We then call SDL_RenderCopy, telling SDL to use the appropriate font texture, and using the glyph's rectangular region as the source, but our dest rectangle as the target; copying an area from the font atlas to our target area.
Note: we're not error checking the character in the above, so it's quite easy for us to crash the program if we pass in a character that exceeds an int value of 127. However, for ASCII text, it will work just fine.
Before we move on, you might want to see that we've updated our getTextTexture to conform to the next multi-font setup we've got. A small change, but one to take note of:
The function now takes the type of font as one of the arguments, and uses the appropriate font when calling TTF_RenderUTF8_Blended.
That's all for text.c, so now let's look at demo.c where we're actually making use of our changes. Our initDemo function now has a counter for the number of logic calls we're making:
Our logic call merely increments the numLogicCalls variable, so we'll skip that. Our main draw function is equally as simple. It makes calls to three functions, drawNormalText, drawColouredText, and drawStatText.
The drawNormalText function is, again, simple:
We're calling our drawText function that we defined in text.c, passing over the line of text that we want to render, the x and y coordinates, the RGB values, and finally the type of font that we want to use.
The drawColouredText function is also pretty simple:
It's just different coordinates and RGB values, as well as selecting different fonts to use. Our final function, drawStatText, will throw up no real surprises either:
We're creating a character buffer to hold our text, and then using sprintf to format the string we want to use. This function is making use to the numLogicCalls variable we set up and increment in the logic function. We're also tracking the number of seconds the application has been running by taking the value of SDL_GetTicks and dividing it by 1000 (SDL_GetTicks returns the number of milliseconds that have elapsed since the SDL was initialized).
As you can see, the text that is being rendered by the drawStatText function changes quite often, especially the one describing the number of logic calls. Imagine if we were trying to use our regular getTextTexture for this operation. We'd be creating a throwing away a lot of textures constantly. Of course, we could create lots of individual textures and use them as needed, but we would then lose the benefit of SDL's draw batching feature (see the sprite atlas tutorial for more on this).
A pretty long article for drawing some text, I'm sure you'll agree, but we've finished this part of the tutorial with a system that is far more flexible and comes with added benefits. But what's with that long line of text? It doesn't fit onto the screen? Indeed, the text actually says so. Wouldn't it be nice if we could make our text fit into the constraints of an area, so that it wraps neatly when approaching an edge? In our next part, we'll look at how to wrap our text, as well as performing some other little text effects.
The source code for all parts of this tutorial (including assets) is available here:
It is also available as part of the SDL2 tutorial bundle (with on-going updates):
If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.
Share your comments and thoughts below. All comments are anonymous and cannot be edited.