SDL School
SDL version Which one?

Sprites and animation

This program shows a small robot that walks across the window and turns around. All of its frames come from one image file. You learn to draw one part of an image, to change the frame with a timer, and to flip an image.

The full program

main.c
/* Animate a walking robot from a sprite sheet. */
#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 */

#define SPRITE_SIZE 16.0f /* pixels in the sheet */
#define FRAME_COUNT 4
#define SECONDS_PER_FRAME 0.12f
#define DRAW_SIZE 112.0f /* pixels on the screen */
#define WALK_SPEED 120.0f /* pixels per second */

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

static float robot_x = 60.0f;
static float direction = 1.0f; /* 1 is right, and -1 is left. */
static int current_frame = 0;
static float frame_timer = 0.0f;

/* Load an image file into a texture. */
static SDL_Texture *load_texture(const char *path)
{
    SDL_Surface *surface = SDL_LoadPNG(path);
    SDL_Texture *texture;

    if (surface == NULL) {
        SDL_Log("SDL_LoadPNG failed: %s", SDL_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);
    return texture;
}

/* Go to the next frame after enough time passes. */
static void update_animation(float dt)
{
    frame_timer += dt;
    if (frame_timer >= SECONDS_PER_FRAME) {
        frame_timer -= SECONDS_PER_FRAME;
        current_frame = (current_frame + 1) % FRAME_COUNT;
    }
}

/* Walk to the edge of the window, then turn around. */
static void update_robot(float dt)
{
    robot_x += direction * WALK_SPEED * dt;

    if (robot_x < 0.0f) {
        robot_x = 0.0f;
        direction = 1.0f;
    } else if (robot_x + DRAW_SIZE > WINDOW_WIDTH) {
        robot_x = WINDOW_WIDTH - DRAW_SIZE;
        direction = -1.0f;
    }
}

static void draw_robot(void)
{
    SDL_FRect source = { (float)current_frame * SPRITE_SIZE, 0.0f, SPRITE_SIZE, SPRITE_SIZE };
    SDL_FRect destination = { robot_x, 240.0f, DRAW_SIZE, DRAW_SIZE };
    SDL_FlipMode flip = (direction < 0.0f) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;

    SDL_RenderTextureRotated(renderer, sheet, &source, &destination, 0.0, NULL, flip);
}

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("Sprites and animation", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
                                     &window, &renderer)) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);

    sheet = load_texture("assets/walker.png");
    if (sheet == NULL) {
        return SDL_APP_FAILURE;
    }
    SDL_SetTextureScaleMode(sheet, SDL_SCALEMODE_NEAREST);

    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;
    SDL_FRect ground = { 0.0f, 352.0f, WINDOW_WIDTH, 128.0f };

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

    update_animation(dt);
    update_robot(dt);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    SDL_SetRenderDrawColor(renderer, 44, 48, 58, SDL_ALPHA_OPAQUE);
    SDL_RenderFillRect(renderer, &ground);
    draw_robot();
    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    SDL_DestroyTexture(sheet);
}
main.c
/* Animate a walking robot from a sprite sheet. */
#include <SDL.h>
#include <SDL_image.h>
#include <stdbool.h>

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

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

#define SPRITE_SIZE 16 /* pixels in the sheet */
#define FRAME_COUNT 4
#define SECONDS_PER_FRAME 0.12f
#define DRAW_SIZE 112.0f /* pixels on the screen */
#define WALK_SPEED 120.0f /* pixels per second */

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_Texture *sheet = NULL;
static Uint64 last_ticks = 0;
static bool running = true;

static float robot_x = 60.0f;
static float direction = 1.0f; /* 1 is right, and -1 is left. */
static int current_frame = 0;
static float frame_timer = 0.0f;

/* Load an image file into a texture. */
static SDL_Texture *load_texture(const char *path)
{
    SDL_Surface *surface = IMG_Load(path);
    SDL_Texture *texture;

    if (surface == NULL) {
        SDL_Log("IMG_Load failed: %s", IMG_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_FreeSurface(surface);
    return texture;
}

/* Go to the next frame after enough time passes. */
static void update_animation(float dt)
{
    frame_timer += dt;
    if (frame_timer >= SECONDS_PER_FRAME) {
        frame_timer -= SECONDS_PER_FRAME;
        current_frame = (current_frame + 1) % FRAME_COUNT;
    }
}

/* Walk to the edge of the window, then turn around. */
static void update_robot(float dt)
{
    robot_x += direction * WALK_SPEED * dt;

    if (robot_x < 0.0f) {
        robot_x = 0.0f;
        direction = 1.0f;
    } else if (robot_x + DRAW_SIZE > WINDOW_WIDTH) {
        robot_x = WINDOW_WIDTH - DRAW_SIZE;
        direction = -1.0f;
    }
}

static void draw_robot(void)
{
    SDL_Rect source = { current_frame * SPRITE_SIZE, 0, SPRITE_SIZE, SPRITE_SIZE };
    SDL_FRect destination = { robot_x, 240.0f, DRAW_SIZE, DRAW_SIZE };
    SDL_RendererFlip flip = (direction < 0.0f) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;

    SDL_RenderCopyExF(renderer, sheet, &source, &destination, 0.0, NULL, flip);
}

static void frame(void)
{
    SDL_Event event;
    Uint64 now = SDL_GetPerformanceCounter();
    float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();
    SDL_FRect ground = { 0.0f, 352.0f, WINDOW_WIDTH, 128.0f };

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

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

    update_animation(dt);
    update_robot(dt);

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    SDL_SetRenderDrawColor(renderer, 44, 48, 58, SDL_ALPHA_OPAQUE);
    SDL_RenderFillRectF(renderer, &ground);
    draw_robot();
    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, "Sprites and animation");
    SDL_RenderSetVSync(renderer, 1);

    sheet = load_texture("assets/walker.png");
    if (sheet == NULL) {
        SDL_Quit();
        return 1;
    }
    SDL_SetTextureScaleMode(sheet, SDL_ScaleModeNearest);

    last_ticks = SDL_GetPerformanceCounter();

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

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

To build this program on your computer, use the setup from Load an image. Save walker.png in your assets folder.

A sprite sheet

A sprite is a small image that moves on the screen. An animation needs many pictures of that sprite. Most programs put these pictures together in one image, a sprite sheet.

frame 0 frame 1 frame 2 frame 3
The sheet is 64 pixels wide and 16 pixels high, with four frames of 16 by 16 pixels.

One sheet means one file to load and one texture to hold. It is also fast, because the graphics card stays on the same texture.

Draw one part of an image

In the last lessons, you passed NULL to draw a full image. To draw a part of it, you pass a source rectangle.

    SDL_FRect source = { (float)current_frame * SPRITE_SIZE, 0.0f, SPRITE_SIZE, SPRITE_SIZE };
    SDL_Rect source = { current_frame * SPRITE_SIZE, 0, SPRITE_SIZE, SPRITE_SIZE };

The source rectangle uses int values, because SDL2 counts whole pixels in a texture.

The frames sit side by side, so each frame starts SPRITE_SIZE pixels after the last one. For frame 2, x is 32.

The source rectangle says which pixels to take from the sheet. The destination rectangle says where to put them on the screen, and how large. The source is 16 pixels wide and the destination is 112 pixels wide, so the robot appears 7 times larger.

Change the frame with a timer

    frame_timer += dt;
    if (frame_timer >= SECONDS_PER_FRAME) {
        frame_timer -= SECONDS_PER_FRAME;
        current_frame = (current_frame + 1) % FRAME_COUNT;
    }

The word frame has two meanings here. The program draws a screen frame about 60 times each second, but the robot needs a new picture only about 8 times each second. So the program adds up dt until enough time passes.

The % operator gives the remainder after a division. The value goes 0, 1, 2, 3, 0, 1, and so on, so the animation repeats.

The code subtracts SECONDS_PER_FRAME from the timer. It does not set the timer to 0, because that loses the small extra time from each step, and the animation drifts slower.

Flip the image

The sheet shows the robot facing right. To face left, you do not need more pictures, because the renderer can flip the image.

    SDL_FlipMode flip = (direction < 0.0f) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;

    SDL_RenderTextureRotated(renderer, sheet, &source, &destination, 0.0, NULL, flip);
    SDL_RendererFlip flip = (direction < 0.0f) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;

    SDL_RenderCopyExF(renderer, sheet, &source, &destination, 0.0, NULL, flip);

This function can also rotate. The 0.0 is the angle in degrees, and NULL means that the image turns around its center.

Draw order

The ground is drawn before the robot, so the robot stands on top of it. This is the same painting order that you learned in Draw a rectangle.

Try this

  1. Change SECONDS_PER_FRAME, and find the speed where the legs look correct for the walk speed.
  2. Use the code from Read the keyboard to walk with the arrow keys. Show frame 2 while the robot stands still.
  3. Change the 0.0 to an angle that grows over time, to make the robot spin.
  4. Draw your own sheet of 16 by 16 frames in a drawing program. Change FRAME_COUNT to match it.

What you learned