SDL School
SDL version Which one?

Manage many objects

So far, each program had one or two moving objects, with one variable for each. A real program can have hundreds. This lesson shows how to track many objects, and how an event gets to the correct one.

Click empty space to add a ball. Click a ball to remove it. Two balls that touch flash white.

The full program

main.c
/* Many balls in one array. Click empty space to add one, or a ball to remove it. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdlib.h>

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

#define MAX_BALLS 64
#define START_BALLS 8
#define NO_BALL -1
#define BALL_SIZE 32.0f
#define MAX_SPEED 160.0f /* pixels per second */
#define FLASH_TIME 0.15f /* seconds */

typedef struct {
    SDL_FRect rect;
    float vx; /* pixels per second */
    float vy;
    SDL_Color color;
    float flash; /* The seconds that are left of a flash. */
} Ball;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static Uint64 last_ticks = 0;

static Ball balls[MAX_BALLS];
static int ball_count = 0;

static float random_speed(void)
{
    return ((float)rand() / (float)RAND_MAX * 2.0f - 1.0f) * MAX_SPEED;
}

static Uint8 random_channel(void)
{
    return (Uint8)(90 + rand() % 166); /* 90 to 255, so that it stays bright. */
}

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;
}

static bool point_in_rect(float x, float y, const SDL_FRect *rect)
{
    return x >= rect->x && x < rect->x + rect->w &&
           y >= rect->y && y < rect->y + rect->h;
}

/* Add a ball with its middle at a point. The array can be full. */
static void add_ball(float x, float y)
{
    Ball *ball;

    if (ball_count == MAX_BALLS) {
        return;
    }

    ball = &balls[ball_count];
    ball->rect = (SDL_FRect){ x - BALL_SIZE / 2.0f, y - BALL_SIZE / 2.0f, BALL_SIZE, BALL_SIZE };
    ball->vx = random_speed();
    ball->vy = random_speed();
    ball->color = (SDL_Color){ random_channel(), random_channel(), random_channel(), SDL_ALPHA_OPAQUE };
    ball->flash = 0.0f;
    ball_count++;
}

/* Move the last ball into the empty slot. The order of balls does not matter. */
static void remove_ball(int index)
{
    ball_count--;
    balls[index] = balls[ball_count];
}

/* Search from the end, because the last ball is drawn on top. */
static int ball_at(float x, float y)
{
    for (int i = ball_count - 1; i >= 0; i--) {
        if (point_in_rect(x, y, &balls[i].rect)) {
            return i;
        }
    }
    return NO_BALL;
}

/* Keep the ball in the window, and reverse its speed at an edge. */
static void bounce_off_walls(Ball *ball)
{
    if (ball->rect.x < 0.0f) {
        ball->rect.x = 0.0f;
        ball->vx = -ball->vx;
    } else if (ball->rect.x + ball->rect.w > WINDOW_WIDTH) {
        ball->rect.x = WINDOW_WIDTH - ball->rect.w;
        ball->vx = -ball->vx;
    }

    if (ball->rect.y < 0.0f) {
        ball->rect.y = 0.0f;
        ball->vy = -ball->vy;
    } else if (ball->rect.y + ball->rect.h > WINDOW_HEIGHT) {
        ball->rect.y = WINDOW_HEIGHT - ball->rect.h;
        ball->vy = -ball->vy;
    }
}

static void update_ball(Ball *ball, float dt)
{
    ball->rect.x += ball->vx * dt;
    ball->rect.y += ball->vy * dt;
    bounce_off_walls(ball);

    if (ball->flash > 0.0f) {
        ball->flash -= dt;
    }
}

/* Test each pair one time. The inner loop starts after the outer index. */
static void find_collisions(void)
{
    for (int i = 0; i < ball_count; i++) {
        for (int j = i + 1; j < ball_count; j++) {
            if (overlap(&balls[i].rect, &balls[j].rect)) {
                balls[i].flash = FLASH_TIME;
                balls[j].flash = FLASH_TIME;
            }
        }
    }
}

static void update_all(float dt)
{
    for (int i = 0; i < ball_count; i++) {
        update_ball(&balls[i], dt);
    }
    find_collisions();
}

static void draw_ball(const Ball *ball)
{
    if (ball->flash > 0.0f) {
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
    } else {
        SDL_SetRenderDrawColor(renderer, ball->color.r, ball->color.g, ball->color.b, SDL_ALPHA_OPAQUE);
    }
    SDL_RenderFillRect(renderer, &ball->rect);
}

/* Send the click to the ball under the mouse, or to the empty space. */
static void handle_click(float x, float y)
{
    int index = ball_at(x, y);

    if (index != NO_BALL) {
        remove_ball(index);
    } else {
        add_ball(x, 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("Manage many objects", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
                                     &window, &renderer)) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);

    for (int i = 0; i < START_BALLS; i++) {
        add_ball((float)(rand() % WINDOW_WIDTH), (float)(rand() % WINDOW_HEIGHT));
    }

    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;
    }

    SDL_ConvertEventToRenderCoordinates(renderer, event);

    if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN && event->button.button == SDL_BUTTON_LEFT) {
        handle_click(event->button.x, event->button.y);
    }
    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;
    }

    update_all(dt);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    for (int i = 0; i < ball_count; i++) {
        draw_ball(&balls[i]);
    }

    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}
main.c
/* Many balls in one array. Click empty space to add one, or a ball to remove it. */
#include <SDL.h>
#include <stdbool.h>
#include <stdlib.h>

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

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

#define MAX_BALLS 64
#define START_BALLS 8
#define NO_BALL -1
#define BALL_SIZE 32.0f
#define MAX_SPEED 160.0f /* pixels per second */
#define FLASH_TIME 0.15f /* seconds */

typedef struct {
    SDL_FRect rect;
    float vx; /* pixels per second */
    float vy;
    SDL_Color color;
    float flash; /* The seconds that are left of a flash. */
} Ball;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static Uint64 last_ticks = 0;

static Ball balls[MAX_BALLS];
static int ball_count = 0;
static bool running = true;

static float random_speed(void)
{
    return ((float)rand() / (float)RAND_MAX * 2.0f - 1.0f) * MAX_SPEED;
}

static Uint8 random_channel(void)
{
    return (Uint8)(90 + rand() % 166); /* 90 to 255, so that it stays bright. */
}

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;
}

static bool point_in_rect(float x, float y, const SDL_FRect *rect)
{
    return x >= rect->x && x < rect->x + rect->w &&
           y >= rect->y && y < rect->y + rect->h;
}

/* Add a ball with its middle at a point. The array can be full. */
static void add_ball(float x, float y)
{
    Ball *ball;

    if (ball_count == MAX_BALLS) {
        return;
    }

    ball = &balls[ball_count];
    ball->rect = (SDL_FRect){ x - BALL_SIZE / 2.0f, y - BALL_SIZE / 2.0f, BALL_SIZE, BALL_SIZE };
    ball->vx = random_speed();
    ball->vy = random_speed();
    ball->color = (SDL_Color){ random_channel(), random_channel(), random_channel(), SDL_ALPHA_OPAQUE };
    ball->flash = 0.0f;
    ball_count++;
}

/* Move the last ball into the empty slot. The order of balls does not matter. */
static void remove_ball(int index)
{
    ball_count--;
    balls[index] = balls[ball_count];
}

/* Search from the end, because the last ball is drawn on top. */
static int ball_at(float x, float y)
{
    for (int i = ball_count - 1; i >= 0; i--) {
        if (point_in_rect(x, y, &balls[i].rect)) {
            return i;
        }
    }
    return NO_BALL;
}

/* Keep the ball in the window, and reverse its speed at an edge. */
static void bounce_off_walls(Ball *ball)
{
    if (ball->rect.x < 0.0f) {
        ball->rect.x = 0.0f;
        ball->vx = -ball->vx;
    } else if (ball->rect.x + ball->rect.w > WINDOW_WIDTH) {
        ball->rect.x = WINDOW_WIDTH - ball->rect.w;
        ball->vx = -ball->vx;
    }

    if (ball->rect.y < 0.0f) {
        ball->rect.y = 0.0f;
        ball->vy = -ball->vy;
    } else if (ball->rect.y + ball->rect.h > WINDOW_HEIGHT) {
        ball->rect.y = WINDOW_HEIGHT - ball->rect.h;
        ball->vy = -ball->vy;
    }
}

static void update_ball(Ball *ball, float dt)
{
    ball->rect.x += ball->vx * dt;
    ball->rect.y += ball->vy * dt;
    bounce_off_walls(ball);

    if (ball->flash > 0.0f) {
        ball->flash -= dt;
    }
}

/* Test each pair one time. The inner loop starts after the outer index. */
static void find_collisions(void)
{
    for (int i = 0; i < ball_count; i++) {
        for (int j = i + 1; j < ball_count; j++) {
            if (overlap(&balls[i].rect, &balls[j].rect)) {
                balls[i].flash = FLASH_TIME;
                balls[j].flash = FLASH_TIME;
            }
        }
    }
}

static void update_all(float dt)
{
    for (int i = 0; i < ball_count; i++) {
        update_ball(&balls[i], dt);
    }
    find_collisions();
}

static void draw_ball(const Ball *ball)
{
    if (ball->flash > 0.0f) {
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
    } else {
        SDL_SetRenderDrawColor(renderer, ball->color.r, ball->color.g, ball->color.b, SDL_ALPHA_OPAQUE);
    }
    SDL_RenderFillRectF(renderer, &ball->rect);
}

/* Send the click to the ball under the mouse, or to the empty space. */
static void handle_click(float x, float y)
{
    int index = ball_at(x, y);

    if (index != NO_BALL) {
        remove_ball(index);
    } else {
        add_ball(x, 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;
        }

        if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) {
            handle_click((float)event.button.x, (float)event.button.y);
        }
    }

    update_all(dt);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    for (int i = 0; i < ball_count; i++) {
        draw_ball(&balls[i]);
    }

    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, "Manage many objects");
    SDL_RenderSetVSync(renderer, 1);

    for (int i = 0; i < START_BALLS; i++) {
        add_ball((float)(rand() % WINDOW_WIDTH), (float)(rand() % WINDOW_HEIGHT));
    }

    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;
}

One struct for each object

A ball needs a position, a speed, a color, and a timer. A struct keeps these values together, so that each ball holds its own state.

typedef struct {
    SDL_FRect rect;
    float vx; /* pixels per second */
    float vy;
    SDL_Color color;
    float flash; /* The seconds that are left of a flash. */
} Ball;

An array to hold them

static Ball balls[MAX_BALLS];
static int ball_count = 0;

This is the same pattern as the stamps in Use the mouse. The first ball_count entries are in use, and the rest are not. Every loop in the program stops at ball_count.

A fixed array is simple and fast, and it cannot fail in the middle of a game. The price is a limit, so add_ball tests for a full array first.

Update each object

    for (int i = 0; i < ball_count; i++) {
        update_ball(&balls[i], dt);
    }

The loop passes the address of each ball. The function then changes the real ball in the array, and not a copy of it. The function does not need to know how many balls exist, or which one it has.

Find the pairs that touch

Each ball must be tested against each other ball, so the program uses a loop inside a loop:

    for (int i = 0; i < ball_count; i++) {
        for (int j = i + 1; j < ball_count; j++) {
            if (overlap(&balls[i].rect, &balls[j].rect)) {
                balls[i].flash = FLASH_TIME;
                balls[j].flash = FLASH_TIME;
            }
        }
    }

The inner loop starts at i + 1. This tests each pair one time, and it stops a ball from testing itself.

The number of tests grows quickly. With 64 balls, this code does 2,016 tests in each frame. Twice as many balls need about four times as many tests. This is no problem for hundreds of objects, but thousands need a smarter method.

How an event finds its object

SDL knows nothing about your objects. It tells you only that a click happened at a position. Your code must find the object, and change its data.

    int index = ball_at(x, y);

    if (index != NO_BALL) {
        remove_ball(index);
    } else {
        add_ball(x, y);
    }

The search runs from the end of the array to the start:

    for (int i = ball_count - 1; i >= 0; i--) {

The program draws the array in order, so the last ball is on top. A click must go to the ball that you can see, so the top ball must come first in the search.

Events between objects

A touch between two balls is also a kind of event, but your own code makes it. The reaction is simple: the code sets a timer in each ball.

    if (ball->flash > 0.0f) {
        ball->flash -= dt;
    }

The collision code does not draw anything. It only changes the data of the ball, and draw_ball reads that data later. The timer lets a reaction last for many frames after a single event. You can use the same idea for a ball that is hurt, or a button that glows.

So every event in this program follows the same path. The code finds the object, changes its data, and lets the update and draw code do the rest.

Remove an object

Removal leaves a gap in the array. To close the gap, you can move each later ball down by one, but that is slow. The order of the balls does not matter here, so the program moves the last ball into the gap.

Remove index 1, with count 5 After, with count 4 ABCDE AECD E moves to the empty slot unused
Ball B goes away, and ball E takes its slot. The count drops by one.
    ball_count--;
    balls[index] = balls[ball_count];

This has two results that you must remember. The order of the array changes, so a ball can jump to the top of the drawing order. Also, an index is valid only until the next removal, so do not keep an index for use in later frames.

To remove balls inside a loop, count down from the end. A loop that counts up misses the ball that moves into the empty slot.

Try this

  1. Change MAX_BALLS to 3, then click many times. Find out what happens to a click on empty space.
  2. Give each ball a lifetime in seconds. Remove each ball at the end of its life, in a loop that counts down.
  3. Make two balls bounce off each other. Swap their speeds, and move them apart.
  4. Drag a ball with the mouse. Remember why you must not keep its index for long.

What you learned