SDL School
SDL version Which one?

Detect collisions

This program moves a ball that bounces off the window edges and off a block. You learn to test whether two rectangles overlap, and you write the test yourself.

The full program

main.c
/* A ball bounces off the window edges and off a block. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define MAX_DT 0.05f /* seconds */

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FRect ball = { 80.0f, 60.0f, 24.0f, 24.0f };
static SDL_FRect block = { 240.0f, 180.0f, 160.0f, 120.0f };
static float speed_x = 220.0f; /* pixels per second */
static float speed_y = 170.0f;
static Uint64 last_ticks = 0;

/* Two rectangles overlap if they overlap on both axes. */
static bool overlap(const SDL_FRect *a, const SDL_FRect *b)
{
    return a->x < b->x + b->w && a->x + a->w > b->x &&
           a->y < b->y + b->h && a->y + a->h > b->y;
}

/* The ball is blocked if it left the window or if it is in the block. */
static bool ball_is_blocked(void)
{
    return ball.x < 0.0f || ball.x + ball.w > WINDOW_WIDTH ||
           ball.y < 0.0f || ball.y + ball.h > WINDOW_HEIGHT ||
           overlap(&ball, &block);
}

/* Move on one axis at a time, so we know which speed to reverse. */
static void move_ball(float dt)
{
    ball.x += speed_x * dt;
    if (ball_is_blocked()) {
        ball.x -= speed_x * dt;
        speed_x = -speed_x;
    }

    ball.y += speed_y * dt;
    if (ball_is_blocked()) {
        ball.y -= speed_y * dt;
        speed_y = -speed_y;
    }
}

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("Detect collisions", 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;
    }
    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;

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

    move_ball(dt);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    SDL_SetRenderDrawColor(renderer, 32, 96, 160, SDL_ALPHA_OPAQUE);
    SDL_RenderFillRect(renderer, &block);
    SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
    SDL_RenderFillRect(renderer, &ball);
    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}
main.c
/* A ball bounces off the window edges and off a block. */
#include <SDL.h>
#include <stdbool.h>

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

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define MAX_DT 0.05f /* seconds */

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FRect ball = { 80.0f, 60.0f, 24.0f, 24.0f };
static SDL_FRect block = { 240.0f, 180.0f, 160.0f, 120.0f };
static float speed_x = 220.0f; /* pixels per second */
static float speed_y = 170.0f;
static Uint64 last_ticks = 0;
static bool running = true;

/* Two rectangles overlap if they overlap on both axes. */
static bool overlap(const SDL_FRect *a, const SDL_FRect *b)
{
    return a->x < b->x + b->w && a->x + a->w > b->x &&
           a->y < b->y + b->h && a->y + a->h > b->y;
}

/* The ball is blocked if it left the window or if it is in the block. */
static bool ball_is_blocked(void)
{
    return ball.x < 0.0f || ball.x + ball.w > WINDOW_WIDTH ||
           ball.y < 0.0f || ball.y + ball.h > WINDOW_HEIGHT ||
           overlap(&ball, &block);
}

/* Move on one axis at a time, so we know which speed to reverse. */
static void move_ball(float dt)
{
    ball.x += speed_x * dt;
    if (ball_is_blocked()) {
        ball.x -= speed_x * dt;
        speed_x = -speed_x;
    }

    ball.y += speed_y * dt;
    if (ball_is_blocked()) {
        ball.y -= speed_y * dt;
        speed_y = -speed_y;
    }
}

static void frame(void)
{
    SDL_Event event;
    Uint64 now = SDL_GetPerformanceCounter();
    float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();

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

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

    move_ball(dt);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    SDL_SetRenderDrawColor(renderer, 32, 96, 160, SDL_ALPHA_OPAQUE);
    SDL_RenderFillRectF(renderer, &block);
    SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
    SDL_RenderFillRectF(renderer, &ball);
    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, "Detect collisions");
    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;
}

Test for overlap

Many games use rectangles for collisions, because the test is fast and short. Think about one axis first. If each rectangle starts before the other one ends, they overlap on the x axis.

The same rule applies to the y axis. If the rectangles overlap on both axes, they touch. If they do not, they do not touch.

static bool overlap(const SDL_FRect *a, const SDL_FRect *b)
{
    return a->x < b->x + b->w && a->x + a->w > b->x &&
           a->y < b->y + b->h && a->y + a->h > b->y;
}

The function takes pointers, so C does not copy the two structs. The const tells the reader and the compiler that the function does not change them.

SDL also has this test, as SDL_HasRectIntersectionFloat. You wrote your own version to see how it works.

SDL also has this test, as SDL_HasIntersectionF. You wrote your own version to see how it works.

Bounce

A bounce reverses the speed on one axis. The program must find the correct axis. If the ball hits the side of the block, then speed_x reverses. If it hits the top, then speed_y reverses.

The simple way is to move on one axis at a time:

    ball.x += speed_x * dt;
    if (ball_is_blocked()) {
        ball.x -= speed_x * dt;
        speed_x = -speed_x;
    }

The program moves the ball on the x axis and tests it. If the ball is blocked, the x movement caused the problem. The program then undoes the move and reverses speed_x. It then does the same on the y axis.

The undo is important. Without it, the ball stays inside the block. The next frame finds a collision again and reverses the speed again, so the ball shakes in place.

Fast objects

The test looks at one position in each frame, so a fast object can jump past a thin wall. A step of 30 pixels goes through a wall of 10 pixels. The limit on dt from the last lesson helps, because it keeps each step short.

To be safe, keep this rule: the largest step must be less than the size of the thin objects.

Try this

  1. Add a second block. Add a second overlap call to ball_is_blocked.
  2. Add a second ball. A struct with a rectangle and two speeds makes this easier.
  3. Make the block change color at each hit.

What you learned