thecodingidiot.com

Who Wants to Be a Game Developer? GraphicalThe Font

The Font

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.txt

White 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.

Footnotes

  1. SDL_ttf - SDL Wiki