thecodingidiot.com

Who Wants to Be a Game Developer? GraphicalSetup

Setup

g01b builds on g01a. Clone the companion repo — it carries everything needed, including the libtci/ subdirectory with all library source:

git clone https://github.com/thecodingidiot-com/g01b-the-developer-graphical.git g01b-practice
cd g01b-practice

load.c and questions.txt are already present. Every other source file is new or rewritten from g01a.

Why libtci ships as source

A compiled static archive (libtci.a) is architecture-specific — an x86_64 Linux build cannot be used on ARM or macOS. Committing a binary to a source repository also makes it opaque: there is no way to read, audit, or rebuild it from the repo alone.

Shipping the source files in libtci/ solves both problems. make re at the top level descends into libtci/ and builds libtci.a from scratch using your own compiler and flags. The result is always correct for your machine, and every function is readable alongside the game code that calls it.

The limitation is worth naming: thecodingidiot.com declares environment: "linux" for all c-tier and games chapters. The curriculum does not support macOS, Windows, or ARM targets. For a single-platform curriculum, architecture portability is not a concern — but the principle of shipping source over binaries remains the right default regardless.

Build the library once before anything else:

make -C libtci re

libtci.a appears inside libtci/ with no warnings. Subsequent make re calls at the top level rebuild the library only if its sources have changed.


Install SDL2_image and SDL2_ttf

c04 installed SDL2. g01b adds two extensions: SDL2_image for loading PNG background files, and SDL2_ttf for rendering text from a TrueType font:

sudo apt install libsdl2-image-dev libsdl2-ttf-dev

Verify both are installed:

dpkg -l libsdl2-image-dev libsdl2-ttf-dev

Both lines should start with ii.


Generate the assets

The game needs three 800×600 background PNGs and the Px437 IBM EGA 8×14 TrueType font (CC BY-SA 4.0, VileR / int10h.org). Both are in the companion repo. Clone it alongside your practice directory:

git clone https://github.com/thecodingidiot-com/g01b-the-developer-graphical.git g01b-companion

Generate the backgrounds from your practice directory:

cd ~/g01b-practice
bash ~/g01b-companion/gen_assets.sh

The script creates assets/ in the current directory and writes three PNGs into it — one per background colour, each divided into four zones: question (top-left), answer (middle-left), help (bottom-left), and ladder panel (right). Running the script from the practice directory ensures the files land there, not inside the companion repo clone.

g01b background — gameplay (dark navy)
g01b background — correct answer (dark green)
g01b background — wrong answer (dark red)

Copy the font:

cp ~/g01b-companion/assets/font.ttf assets/

The Makefile

CC      = gcc
CFLAGS  = -Wall -Wextra -Werror -D_REENTRANT -I libtci
LDFLAGS = $(shell sdl2-config --libs) -lSDL2_image -lSDL2_ttf
SRCS    = main.c load.c game.c render.c font.c
OBJS    = $(SRCS:.c=.o)
LIBS    = libtci/libtci.a
NAME    = game
 
.PHONY: all clean fclean re
 
all: $(NAME)
 
$(NAME): $(OBJS) $(LIBS)
	$(CC) $(OBJS) $(LIBS) $(LDFLAGS) -o $(NAME)
 
$(LIBS):
	$(MAKE) -C libtci
 
%.o: %.c game.h
	$(CC) $(CFLAGS) -c $< -o $@
 
clean:
	$(MAKE) -C libtci clean
	rm -f $(OBJS)
 
fclean: clean
	$(MAKE) -C libtci fclean
	rm -f $(NAME)
 
re: fclean all

-D_REENTRANT is the flag SDL2 requires — it enables thread-safe versions of certain library functions (what threads are and why they need safe library functions is covered in c08).

sdl2-config --cflags would produce the same flag, but it also injects -I/usr/include/SDL2 into the include path. Combined with #include <SDL2/SDL.h> in the source that becomes a double-nested lookup /usr/include/SDL2/SDL2/SDL.h which does not exist. Using -D_REENTRANT directly avoids the conflict. sdl2-config --libs on the linker side has no such problem and is kept.

sdl2-config is installed with libsdl2-dev. LDFLAGS adds -lSDL2_image and -lSDL2_ttf after the SDL2 base libraries. The pattern rule %.o: %.c game.h rebuilds every object when the header changes.


game.h

game.h is the contract between all five source files. Replace your g01a version with this:

#ifndef GAME_H
# define GAME_H
 
# include <SDL2/SDL.h>
# include <SDL2/SDL_image.h>
# include <SDL2/SDL_ttf.h>
# include <fcntl.h>
# include <unistd.h>
# include <stdlib.h>
# include <time.h>
# include "libtci.h"
 
# define LEVELS 15
# define WIN_W  800
# define WIN_H  600
 
typedef struct s_question
{
    char    *text;
    char    *opts[4];
    int      answer;
    char    *hint;
} question_t;
 
typedef enum e_state
{
    STATE_TITLE,
    STATE_QUESTION,
    STATE_CONFIRM,
    STATE_CORRECT,
    STATE_WRONG,
    STATE_WIN,
    STATE_GAMEOVER
} game_state_t;
 
typedef struct s_game
{
    SDL_Window      *win;
    SDL_Renderer    *ren;
    SDL_Texture     *bg_studio;
    SDL_Texture     *bg_correct;
    SDL_Texture     *bg_wrong;
    TTF_Font        *font;
    game_state_t     state;
    question_t     **questions;
    int              count;
    int              level;
    int              safe_level;
    int              lifelines;
    int              phone_active;
    int              hidden[4];
    int              audience[4];
    char             pending;
} game_t;
 
extern const char  *PRIZES[LEVELS];
extern const int    SAFE[LEVELS];
 
/* load.c */
question_t  **load_questions(const char *path, int *count);
void          free_questions(question_t **questions, int count);
 
/* game.c */
void    game_init(game_t *g, question_t **questions, int count);
void    game_free(game_t *g);
void    evaluate_answer(game_t *g);
void    handle_lifeline(game_t *g, int lifeline);
void    next_question(game_t *g);
 
/* font.c */
int     font_load(game_t *g, const char *path, int ptsize);
void    font_free(game_t *g);
void    draw_string(game_t *g, int x, int y, const char *s);
 
/* render.c */
int     render_init(game_t *g);
void    render_free(game_t *g);
void    render_frame(game_t *g);
 
/* main.c */
void    handle_event(game_t *g, SDL_Event *ev, int *running);
 
#endif

The SDL2 headers are included once here. Every file that includes game.h sees the SDL2 types — SDL_Window *, SDL_Renderer *, TTF_Font * — without including the headers again. load.c and game.c include game.h but call no SDL2 functions. Only render.c, font.c, and main.c call into the SDL2 API. The platform separation rule is about what each file calls, not what it sees.

phone_active is a flag set when the Phone-a-Friend lifeline is used and cleared when the next question begins. It controls whether the hint is displayed. Without it, the hint from question N would persist to question N+1 because the lifeline bit stays cleared for the rest of the game.

PRIZES and SAFE are declared extern here and defined in game.c. They are game data — the prize ladder and the safe-level markers — not display data.


game.c

game.c carries the logic from g01a forward with three changes: PRIZES and SAFE become non-static globals (they are declared extern in game.h), phone_active is initialised and reset, and next_question no longer switches to a dedicated ladder state — the ladder is always visible as a panel in the render layer.

#include "game.h"
 
const char  *PRIZES[LEVELS] = {
    "£100", "£200", "£300", "£500", "£1,000",
    "£2,000", "£4,000", "£8,000", "£16,000", "£32,000",
    "£64,000", "£125,000", "£250,000", "£500,000", "£1,000,000"
};
 
const int   SAFE[LEVELS] = {
    0, 0, 0, 0, 1,
    0, 0, 0, 0, 1,
    0, 0, 0, 0, 0
};
 
void    game_init(game_t *g, question_t **questions, int count)
{
    tci_bzero(g->hidden, sizeof(g->hidden));
    tci_bzero(g->audience, sizeof(g->audience));
    g->state       = STATE_TITLE;
    g->questions   = questions;
    g->count       = count;
    g->level       = 0;
    g->safe_level  = -1;
    g->lifelines   = 7;
    g->phone_active = 0;
    g->pending     = 0;
}
 
void    game_free(game_t *g)
{
    (void)g;
}
 
void    evaluate_answer(game_t *g)
{
    question_t  *q;
 
    q = g->questions[g->level];
    if (g->pending - 'A' == q->answer)
        g->state = STATE_CORRECT;
    else
        g->state = STATE_WRONG;
}
 
void    handle_lifeline(game_t *g, int lifeline)
{
    question_t  *q;
    int          bit;
    int          removed;
    int          i;
 
    q = g->questions[g->level];
    bit = 1 << (lifeline - 1);
    if (!(g->lifelines & bit))
        return;
    g->lifelines &= ~bit;
    if (lifeline == 1) {
        removed = 0;
        for (i = 0; i < 4 && removed < 2; i++) {
            if (i == q->answer)
                continue;
            g->hidden[i] = 1;
            removed++;
        }
    } else if (lifeline == 2) {
        g->phone_active = 1;
    } else if (lifeline == 3) {
        int correct;
        int spread;
 
        correct = 55 + (rand() % 30);
        g->audience[q->answer] = correct;
        spread = 100 - correct;
        for (i = 0; i < 4; i++) {
            if (i == q->answer)
                continue;
            if (g->hidden[i]) {
                g->audience[i] = 0;
            } else {
                int portion = spread / 3;
                g->audience[i] = portion;
                spread -= portion;
            }
        }
    }
}
 
void    next_question(game_t *g)
{
    g->level++;
    if (g->level >= LEVELS) {
        g->state = STATE_WIN;
        return;
    }
    tci_bzero(g->hidden, sizeof(g->hidden));
    tci_bzero(g->audience, sizeof(g->audience));
    g->phone_active = 0;
    g->pending = 0;
    if (SAFE[g->level])
        g->safe_level = g->level;
    g->state = STATE_QUESTION;
}

lifelines is initialised to 7 — binary 111, one bit per lifeline. handle_lifeline receives a lifeline number (1, 2, or 3), derives the bit with 1 << (lifeline - 1), checks it is still set, clears it, and applies the effect. A second call with the same lifeline number returns immediately because the bit is already clear.

The audience percentages are weighted: the correct answer is assigned a random value between 55 and 84, the remainder is distributed among the wrong answers proportionally. The visible bars are a hint, not a guarantee.


Stubs: font.c and render.c

Create these two files. They will be filled in on the pages that follow. The stubs compile cleanly and produce a working window.

font.c:

#include "game.h"
 
int font_load(game_t *g, const char *path, int ptsize)
{
    (void)g;
    (void)path;
    (void)ptsize;
    return (0);
}
 
void    font_free(game_t *g)
{
    (void)g;
}
 
void    draw_string(game_t *g, int x, int y, const char *s)
{
    (void)g;
    (void)x;
    (void)y;
    (void)s;
}

render.c:

#include "game.h"
 
int     render_init(game_t *g)
{
    (void)g;
    return (0);
}
 
void    render_free(game_t *g)
{
    (void)g;
}
 
void    render_frame(game_t *g)
{
    SDL_RenderClear(g->ren);
    SDL_RenderPresent(g->ren);
}

main.c

main.c owns the SDL2 lifecycle and the event loop. It also defines handle_event — the function that translates SDL key events into game state changes.

#include "game.h"
 
static void shuffle(question_t **questions, int count)
{
    int          i;
    int          j;
    question_t  *tmp;
 
    for (i = count - 1; i > 0; i--) {
        j = rand() % (i + 1);
        tmp = questions[i];
        questions[i] = questions[j];
        questions[j] = tmp;
    }
}
 
void    handle_event(game_t *g, SDL_Event *ev, int *running)
{
    if (ev->type == SDL_QUIT) {
        *running = 0;
        return;
    }
    if (ev->type == SDL_KEYDOWN && ev->key.keysym.sym == SDLK_ESCAPE)
        *running = 0;
    (void)g;
}
 
int main(int argc, char *argv[])
{
    game_t       g;
    question_t **questions;
    int          count;
    SDL_Event    ev;
    int          running;
 
    if (argc < 2) {
        tci_printf("usage: %s questions.txt\n", argv[0]);
        return (1);
    }
    srand((unsigned int)time(NULL));
    questions = load_questions(argv[1], &count);
    if (!questions || count < LEVELS) {
        tci_printf("need at least %d questions, got %d\n", LEVELS, count);
        free_questions(questions, count);
        return (1);
    }
    shuffle(questions, count);
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL_Init: %s", SDL_GetError());
        free_questions(questions, count);
        return (1);
    }
    if (IMG_Init(IMG_INIT_PNG) == 0) {
        SDL_Log("IMG_Init: %s", IMG_GetError());
        SDL_Quit();
        free_questions(questions, count);
        return (1);
    }
    if (TTF_Init() != 0) {
        SDL_Log("TTF_Init: %s", TTF_GetError());
        IMG_Quit();
        SDL_Quit();
        free_questions(questions, count);
        return (1);
    }
    tci_bzero(&g, sizeof(g));
    g.win = SDL_CreateWindow("Who Wants to Be a Game Developer?",
        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WIN_W, WIN_H, 0);
    if (!g.win) {
        SDL_Log("SDL_CreateWindow: %s", SDL_GetError());
        TTF_Quit();
        IMG_Quit();
        SDL_Quit();
        free_questions(questions, count);
        return (1);
    }
    g.ren = SDL_CreateRenderer(g.win, -1, SDL_RENDERER_ACCELERATED);
    if (!g.ren) {
        SDL_Log("SDL_CreateRenderer: %s", SDL_GetError());
        SDL_DestroyWindow(g.win);
        TTF_Quit();
        IMG_Quit();
        SDL_Quit();
        free_questions(questions, count);
        return (1);
    }
    if (font_load(&g, "assets/font.ttf", 16) != 0) {
        SDL_DestroyRenderer(g.ren);
        SDL_DestroyWindow(g.win);
        TTF_Quit();
        IMG_Quit();
        SDL_Quit();
        free_questions(questions, count);
        return (1);
    }
    if (render_init(&g) != 0) {
        font_free(&g);
        SDL_DestroyRenderer(g.ren);
        SDL_DestroyWindow(g.win);
        TTF_Quit();
        IMG_Quit();
        SDL_Quit();
        free_questions(questions, count);
        return (1);
    }
    game_init(&g, questions, count);
    running = 1;
    while (running) {
        while (SDL_PollEvent(&ev))
            handle_event(&g, &ev, &running);
        render_frame(&g);
        SDL_Delay(16);
    }
    render_free(&g);
    game_free(&g);
    font_free(&g);
    SDL_DestroyRenderer(g.ren);
    SDL_DestroyWindow(g.win);
    TTF_Quit();
    IMG_Quit();
    SDL_Quit();
    free_questions(questions, count);
    return (0);
}

The init sequence follows the same pattern as c04: each subsystem is initialised in order, and if any step fails, every subsystem already open is closed before returning. The cleanup at the end mirrors the init in reverse — render_free before font_free before the renderer and window before the SDL subsystems.

SDL_Delay(16) caps the loop at roughly 60 frames per second. The game renders the same frame every 16 ms until a key is pressed — SDL_PollEvent returns 0 when the event queue is empty and control falls through to render_frame. The frame budget is not tight; the dominant cost is TTF_RenderUTF8_Solid per string, which is covered on the next page.

handle_event at this stage only handles quit and escape. The full key dispatch — A through D for answers, 1 through 3 for lifelines, Space and Enter to advance — is built in The Input.

tci_bzero(&g, sizeof(g)) zeroes the entire struct before the first field is written. This ensures every pointer is NULL and every integer is 0 before SDL2 handles are assigned, which makes the cleanup sequence safe even if initialisation fails partway through.


Confirm the window opens

make re
./game questions.txt

A black window should appear. Press Escape to quit. The window is empty because render_frame only clears to black — the background PNG is loaded in The Background, and text appears in The Font. A clean build with an opening window confirms that the Makefile, the SDL2 init chain, and the stub files are all correct.

If make fails with a missing header error, confirm that libsdl2-image-dev and libsdl2-ttf-dev are installed. If the window does not open, the error message from SDL_Log or tci_printf will name the failing step.