SDL School
SDL version Which one?

Read the keyboard

This program moves a square with the arrow keys. The Space key changes its color. You learn the two ways to read the keyboard, and the correct use of each way.

Click the demo, then press the keys. This demo needs a physical keyboard.

The full program

main.c
/* Move a square with the arrow keys. Space changes its color. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SPEED 260.0f /* pixels per second */
#define MAX_DT 0.05f /* seconds */

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FRect square = { 296.0f, 216.0f, 48.0f, 48.0f };
static bool orange = true;
static Uint64 last_ticks = 0;

SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("Read the keyboard", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
                                     &window, &renderer)) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);
    last_ticks = SDL_GetTicksNS();
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }

    if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat &&
        event->key.scancode == SDL_SCANCODE_SPACE) {
        orange = !orange;
    }
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    Uint64 now = SDL_GetTicksNS();
    float dt = (float)(now - last_ticks) / (float)SDL_NS_PER_SECOND;
    const bool *keys = SDL_GetKeyboardState(NULL);

    last_ticks = now;
    if (dt > MAX_DT) {
        dt = MAX_DT;
    }

    if (keys[SDL_SCANCODE_LEFT]) {
        square.x -= SPEED * dt;
    }
    if (keys[SDL_SCANCODE_RIGHT]) {
        square.x += SPEED * dt;
    }
    if (keys[SDL_SCANCODE_UP]) {
        square.y -= SPEED * dt;
    }
    if (keys[SDL_SCANCODE_DOWN]) {
        square.y += SPEED * dt;
    }

    square.x = SDL_clamp(square.x, 0.0f, WINDOW_WIDTH - square.w);
    square.y = SDL_clamp(square.y, 0.0f, WINDOW_HEIGHT - square.h);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    if (orange) {
        SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
    } else {
        SDL_SetRenderDrawColor(renderer, 80, 200, 160, SDL_ALPHA_OPAQUE);
    }
    SDL_RenderFillRect(renderer, &square);
    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}
main.c
/* Move a square with the arrow keys. Space changes its color. */
#include <SDL.h>
#include <stdbool.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SPEED 260.0f /* pixels per second */
#define MAX_DT 0.05f /* seconds */

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FRect square = { 296.0f, 216.0f, 48.0f, 48.0f };
static bool orange = true;
static Uint64 last_ticks = 0;
static bool running = true;

static void frame(void)
{
    SDL_Event event;
    Uint64 now = SDL_GetPerformanceCounter();
    float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();
    const Uint8 *keys = SDL_GetKeyboardState(NULL);

    last_ticks = now;
    if (dt > MAX_DT) {
        dt = MAX_DT;
    }

    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            running = false;
        }

        if (event.type == SDL_KEYDOWN && !event.key.repeat &&
            event.key.keysym.scancode == SDL_SCANCODE_SPACE) {
            orange = !orange;
        }
    }

    if (keys[SDL_SCANCODE_LEFT]) {
        square.x -= SPEED * dt;
    }
    if (keys[SDL_SCANCODE_RIGHT]) {
        square.x += SPEED * dt;
    }
    if (keys[SDL_SCANCODE_UP]) {
        square.y -= SPEED * dt;
    }
    if (keys[SDL_SCANCODE_DOWN]) {
        square.y += SPEED * dt;
    }

    square.x = SDL_clamp(square.x, 0.0f, WINDOW_WIDTH - square.w);
    square.y = SDL_clamp(square.y, 0.0f, WINDOW_HEIGHT - square.h);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    if (orange) {
        SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
    } else {
        SDL_SetRenderDrawColor(renderer, 80, 200, 160, SDL_ALPHA_OPAQUE);
    }
    SDL_RenderFillRectF(renderer, &square);
    SDL_RenderPresent(renderer);
}

int main(int argc, char *argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_SetWindowTitle(window, "Read the keyboard");
    SDL_RenderSetVSync(renderer, 1);
    last_ticks = SDL_GetPerformanceCounter();

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(frame, 0, 1);
#else
    while (running) {
        frame();
    }
#endif

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Two kinds of input

Some actions continue while you hold a key, such as movement. Other actions happen one time for each press, such as a jump. SDL gives you one tool for each kind.

Held keys

    const bool *keys = SDL_GetKeyboardState(NULL);

SDL_GetKeyboardState gives you an array of bool, with one value for each key. The value is true while the key is down.

    const Uint8 *keys = SDL_GetKeyboardState(NULL);

SDL_GetKeyboardState gives you an array of Uint8, with one value for each key. The value is 1 while the key is down.

    if (keys[SDL_SCANCODE_LEFT]) {
        square.x -= SPEED * dt;
    }

The index is a scancode, a number for the physical position of a key. SDL_SCANCODE_W is the same physical key on every keyboard layout, for example, so W, A, S, and D keep their shape.

The program reads the array in each frame, so the square moves while you hold the key. While SDL handles events, it updates the array. SDL owns this memory, so you must not free it.

Single presses

    if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat &&
        event->key.scancode == SDL_SCANCODE_SPACE) {
        orange = !orange;
    }

Each key press makes SDL send SDL_EVENT_KEY_DOWN. The SDL_AppEvent function gets one call for each event, so this code runs one time for each press.

        if (event.type == SDL_KEYDOWN && !event.key.repeat &&
            event.key.keysym.scancode == SDL_SCANCODE_SPACE) {
            orange = !orange;
        }

Each key press makes SDL send SDL_KEYDOWN. The SDL_PollEvent loop gets each event one time, so this code runs one time for each press.

If you hold a key, the operating system sends more key events after a short delay. These events have repeat set. The test !event.key.repeat ignores them.

Stay in the window

    square.x = SDL_clamp(square.x, 0.0f, WINDOW_WIDTH - square.w);
    square.y = SDL_clamp(square.y, 0.0f, WINDOW_HEIGHT - square.h);

SDL_clamp keeps a value between a minimum and a maximum. The largest x is the window width minus the square width, because x is the left edge of the square.

Try this

  1. Add the W, A, S, and D keys, so that both sets of keys move the square.
  2. Make the left Shift key double the speed while you hold it. The scancode is SDL_SCANCODE_LSHIFT.
  3. Hold two arrow keys. The square moves faster on a diagonal, because it gets both steps. Find a way to fix this.

What you learned