thecodingidiot.com

Who Wants to Be a Game Developer? GraphicalThe Input

The Input

In g01a, input was blocking. read(0, &c, 1) halted the program until the player pressed a key. The game rendered nothing while waiting.

In g01b, SDL_PollEvent replaces read. It returns immediately with or without an event — the game loop continues running and calls render_frame every iteration, whether or not a key was pressed. Input and rendering are decoupled.

This page implements handle_event and its four static helpers in main.c.


SDL_PollEvent

SDL_PollEvent removes one event from the SDL2 event queue and fills the SDL_Event struct. It returns 1 if an event was present, 0 if the queue was empty. The main loop in main.c drains the queue on each frame:

    while (running) {
        while (SDL_PollEvent(&ev))
            handle_event(&g, &ev, &running);
        render_frame(&g);
        SDL_Delay(16);
    }

The inner while loop processes all queued events before rendering. If two keys were pressed in the same frame, both are handled before the next render_frame call.

SDL_Delay(16) targets approximately 60 frames per second (1000ms / 60 ≈ 16ms). The game is not frame-rate sensitive, but without a delay the loop would spin at full CPU speed for no benefit.


handle_event

handle_event checks the event type first. Only SDL_QUIT and SDL_KEYDOWN are relevant — all other events are ignored. Escape quits from any state, so it is handled before the switch:

void    handle_event(game_t *g, SDL_Event *ev, int *running)
{
    SDL_Keycode sym;
 
    if (ev->type == SDL_QUIT) {
        *running = 0;
        return;
    }
    if (ev->type != SDL_KEYDOWN)
        return;
    sym = ev->key.keysym.sym;
    if (sym == SDLK_ESCAPE) {
        *running = 0;
        return;
    }
    switch (g->state) {
        case STATE_TITLE:    handle_title(g, sym);    break;
        case STATE_QUESTION: handle_question(g, sym); break;
        case STATE_CONFIRM:  handle_confirm(g, sym);  break;
        case STATE_CORRECT:  handle_correct(g, sym);  break;
        case STATE_WRONG:
        case STATE_WIN:
        case STATE_GAMEOVER:
            break;
    }
}

ev->key.keysym.sym is the SDL_Keycode — a named integer constant for each key. Extracting it into a local sym variable avoids repeating the long member access chain inside the switch.

SDLK_ESCAPE is checked once and exits immediately. Without this, every state handler would need its own running parameter and its own Escape check. Moving shared behaviour above the switch keeps each handler focused.

STATE_WRONG, STATE_WIN, and STATE_GAMEOVER fall through to a bare break. Escape is already handled above, and these terminal states accept no other input.


State handlers

handle_title:

static void handle_title(game_t *g, SDL_Keycode sym)
{
    if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER)
        g->state = STATE_QUESTION;
}

Enter or the numpad Enter starts the game. SDLK_KP_ENTER is the separate keycode for the numpad Enter key — accepting both avoids frustrating players who reach for the numpad.

handle_question:

static void handle_question(game_t *g, SDL_Keycode sym)
{
    if (sym == SDLK_a || sym == SDLK_b || sym == SDLK_c || sym == SDLK_d) {
        g->pending = 'A' + (sym - SDLK_a);
        g->state   = STATE_CONFIRM;
    } else if (sym == SDLK_1) {
        handle_lifeline(g, 1);
    } else if (sym == SDLK_2) {
        handle_lifeline(g, 2);
    } else if (sym == SDLK_3) {
        handle_lifeline(g, 3);
    } else if (sym == SDLK_w) {
        g->state = STATE_GAMEOVER;
    }
}

sym - SDLK_a converts SDLK_a through SDLK_d to 0 through 3. 'A' + 0 is 'A', 'A' + 3 is 'D'. The result goes into g->pending and the game enters STATE_CONFIRM.

SDLK_a through SDLK_d have values 97 through 100 — the ASCII codes of the lowercase letters. This is not a coincidence: SDL2 defines SDLK_* constants for printable characters as their ASCII values. The arithmetic sym - SDLK_a therefore equals sym - 97, which is 0 for a, 1 for b, and so on — exactly the offset needed.

handle_confirm:

static void handle_confirm(game_t *g, SDL_Keycode sym)
{
    if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER)
        evaluate_answer(g);
    else if (sym == SDLK_BACKSPACE)
        g->state = STATE_QUESTION;
}

Enter commits the answer and delegates to evaluate_answer, which sets g->state to STATE_CORRECT or STATE_WRONG. Backspace cancels and returns to the question.

handle_correct:

static void handle_correct(game_t *g, SDL_Keycode sym)
{
    if (sym == SDLK_SPACE)
        next_question(g);
}

next_question increments g->level and checks for a win. If the new level is a safe level, it updates g->safe_level — the ladder panel, always visible in the right panel, reflects the updated marker on the next frame. The game then transitions to STATE_QUESTION.


Why this replaces read()

read(0, &c, 1) is a blocking syscall. The kernel suspends the process until the player presses Enter. While suspended, nothing else runs — the screen does not update, no timers fire, nothing.

SDL_PollEvent is non-blocking. The event queue holds keypresses until the game asks for them. The game loop runs at 60 Hz regardless of whether a key was pressed. This is the minimum requirement for a real-time graphical application: the render loop and the input loop must not block each other.

The tradeoff is that there is no longer an implicit "wait for input" in the game code. Every state must be renderable with no input pending — the title screen, the question display, the confirm overlay all draw the same content every frame until input changes the state.


Function ordering in main.c

handle_event calls all four static handlers. They must be defined before it so the compiler sees their definitions before the call sites. The order in main.c is:

shuffle          (static)
handle_title     (static)
handle_question  (static)
handle_confirm   (static)
handle_correct   (static)
handle_event
main

No forward declarations are needed because each function is defined above every call site. main comes last.


Build and play

Build and run:

make re
./game questions.txt

The title screen should appear. Press Enter to start. Navigate through all seven screen states: answer a question, use a lifeline, walk away, or play to the win screen.

If the window is unresponsive, confirm handle_event is being called inside the event loop in main.c and that the state machine transitions are correct. Add a temporary SDL_Log("state: %d", g->state) inside handle_event to trace state changes to the terminal.