In g01a, printing
text was one call: tci_printf("£100"). The terminal received the
bytes, looked up each character in its own built-in font, and drew
the glyphs — that work happened invisibly, inside the terminal
emulator. You never thought about it.
An SDL2 window has no terminal. It is a canvas: a rectangle of pixels. Nothing interprets character codes. Nothing owns a font. If you want text on screen, you have to do what the terminal was doing for free: load a font file, ask the font library to rasterise the string into pixels, upload those pixels to the GPU, and blit them to the right position. That is the cost of a graphical interface — and it is also the freedom: the font, the size, the colour, and the position are now all yours to control.
SDL2_ttf[1] handles the rasterisation step. It loads a TrueType font
file and renders text into a surface — a block of pixels in memory.
SDL_CreateTextureFromSurface uploads that surface to the GPU as a
texture. SDL_RenderCopy blits the texture to the window.
draw_string combines those three steps into a single call. This page
implements font_load, font_free, and draw_string in font.c.
The call font_load(&g, "assets/font.ttf", 16) is already in main.c
from the Setup page — replacing the stub is the only change needed here.
font_load and font_free
TTF_OpenFont loads the font file from disk at the requested point
size. The returned TTF_Font * is the handle used by all subsequent
render calls:
#include "game.h"
int font_load(game_t *g, const char *path, int ptsize)
{
g->font = TTF_OpenFont(path, ptsize);
if (!g->font) {
SDL_Log("font_load: %s", TTF_GetError());
return (-1);
}
return (0);
}
void font_free(game_t *g)
{
if (g->font) {
TTF_CloseFont(g->font);
g->font = NULL; /* prevents double-free if called again */
}
}TTF_Init() and TTF_Quit() are handled in main.c — the same
pattern as SDL_Init and IMG_Init. font_load only opens the font
file; it does not initialise the subsystem.
TTF_OpenFont and TTF_CloseFont follow the same acquire-and-release
contract as malloc and free from
c01: one call
allocates a resource and returns a handle, the matching call frees it.
The discipline is identical — every TTF_OpenFont must have a
corresponding TTF_CloseFont, and setting the pointer to NULL after
closing is the same guard taught in c01 to make double-free safe.
draw_string
TTF_RenderUTF8_Solid renders a string to a new SDL_Surface — a
CPU-side pixel buffer. The surface must be uploaded to the GPU with
SDL_CreateTextureFromSurface, then blitted, then freed:
void draw_string(game_t *g, int x, int y, const char *s)
{
SDL_Color white;
SDL_Surface *surf;
SDL_Texture *tex;
SDL_Rect dst;
int w;
int h;
if (!s || !*s)
return;
white.r = 255;
white.g = 255;
white.b = 255;
white.a = 255;
surf = TTF_RenderUTF8_Solid(g->font, s, white); /* render to CPU surface */
if (!surf)
return;
tex = SDL_CreateTextureFromSurface(g->ren, surf); /* upload to GPU */
SDL_FreeSurface(surf); /* CPU copy no longer needed */
if (!tex)
return;
SDL_QueryTexture(tex, NULL, NULL, &w, &h); /* get rendered pixel dimensions */
dst.x = x;
dst.y = y;
dst.w = w;
dst.h = h;
SDL_RenderCopy(g->ren, tex, NULL, &dst);
SDL_DestroyTexture(tex); /* create, use, destroy — one texture per call */
}SDL_Color is a four-field struct: r, g, b, a. White is
{255, 255, 255, 255}. The fields must be set individually in C99 —
struct literal syntax = {255, 255, 255, 255} also works. The four
channels are the same ones from
c04/05
— there they were packed into a single uint32_t as 0xAARRGGBB;
here SDL2 keeps them as separate Uint8 fields and packs them
internally when it needs the combined value.
TTF_RenderUTF8_Solid produces a surface with a transparent
background and coloured text. Use the UTF-8 variant — not
TTF_RenderText_Solid, which treats the string as Latin-1. The prize
strings contain £, which is Unicode code point U+00A3 — a two-byte
UTF-8 sequence (encoding covered in
c01/04).
The Latin-1 function sees 0xC2 and 0xA3 as two separate characters
and renders both, producing £. The UTF-8 function recognises the
sequence, decodes it to a single code point, and renders one glyph.
SDL_QueryTexture fills w and h with the pixel dimensions of
the texture, which is the rendered size of the string at the chosen
point size. The destination rectangle uses these dimensions so the text
is drawn at its natural size.
Create, blit, destroy — the texture is created, used once, and
immediately destroyed. Creating and destroying a texture per
draw_string call has a measurable cost, but for a game that renders
a handful of strings per frame at 60 Hz the overhead is negligible.
A caching strategy (create once, redraw when text changes) is an
optimisation for later.
Test the font
Add a temporary test to render_frame:
void render_frame(game_t *g)
{
SDL_RenderClear(g->ren);
draw_string(g, 40, 40, "Who Wants to Be a Game Developer?");
SDL_RenderPresent(g->ren);
}Build and run:
make re
./game questions.txtWhite text should appear in the upper-left area of the window against
a black background. If the window is empty, confirm assets/font.ttf
is present and that font_load is being called before game_init.
If the text appears but is garbled or invisible, try a different point
size.
Remove the test string from render_frame before continuing.