Animations with Sprite Sheets in SDL2

This is an updated version of “SDL2: Animations with Sprite Sheets“, originally posted on 30th March 2014 at Programmer’s Ranch. The source code is available at the Gigi Labs BitBucket repository.

Many of the previous SDL2 tutorials have involved working with images. In this article, we’re going to take this to the next level, using a very simple technique to animate our images and make them feel more alive.

Our project setup for this article is just the same as in “Loading Images in SDL2 with SDL_image“, and in fact our starting code is adapted from that article:

#include <SDL.h>
#include <SDL_image.h>

int main(int argc, char ** argv)
{
    bool quit = false;
    SDL_Event event;

    SDL_Init(SDL_INIT_VIDEO);
    IMG_Init(IMG_INIT_PNG);

    SDL_Window * window = SDL_CreateWindow("SDL2 Sprite Sheets",
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
    SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, 0);
    SDL_Surface * image = IMG_Load("spritesheet.png");
    SDL_Texture * texture = SDL_CreateTextureFromSurface(renderer, image);

    while (!quit)
    {
        SDL_WaitEvent(&event);

        switch (event.type)
        {
            case SDL_QUIT:
                quit = true;
                break;
        }

        SDL_RenderCopy(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);
    }

    SDL_DestroyTexture(texture);
    SDL_FreeSurface(image);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    IMG_Quit();
    SDL_Quit();

    return 0;
}

sdl2-spritesheet-borders

This image is 128 pixels wide and 64 pixels high. It consists of 4 sub-images (called sprites or frames), each 32 pixels wide. If we can rapidly render each image in quick succession, just like a cartoon, then we have an animation! 😀
Now, those ugly borders in the image above are just for demonstration purposes. Here’s the same image, without borders and with transparency:

sdl2-spritesheet-actual

If we now try to draw the above on the default black background, we’re not going to see anything, are we? Fortunately, it’s easy to change the background colour, and we’ve done it before in “Handling Keyboard and Mouse Events in SDL2“. Just add the following two lines before the while loop:

    SDL_SetRenderDrawColor(renderer, 168, 230, 255, 255);
    SDL_RenderClear(renderer);

Now we get an early peek at what the output is going to look like. Press Ctrl+Shift+B to build the project, and then copy SDL2.dll, all the SDL_image DLLs, and the spritesheet into the Debug folder where the executable is generated.

Once that is done, hit F5:

sdl2-spritesheet-early

So at this point, there are two issues we want to address. First, we don’t want our image to take up the whole window, as it’s doing above. Secondly, we only want to draw one sprite at a time. Both of these are pretty easy to solve if you remember SDL_RenderCopy()‘s last two parameters: a source rectangle (to draw only a portion of the image) and a destination rectangle (to draw the image only to a portion of the screen).

So let’s add the following at the beginning of the while loop:

        SDL_Rect srcrect = { 0, 0, 32, 64 };
        SDL_Rect dstrect = { 10, 10, 32, 64 };

…and then update our SDL_RenderCopy() call as follows:

        SDL_RenderCopy(renderer, texture, &srcrect, &dstrect);

Note that the syntax we’re using to initialise our SDL_Rects is just shorthand to set all of the x, y, w (width) and h (height) members all at once.

Let’s run the program again and see what it looks like:

sdl2-spritesheet-clipped

Okay, so like this we are just rendering the first sprite to a part of the window. Now, let’s work on actually animating this. At the beginning of the while loop, add the following:

        Uint32 ticks = SDL_GetTicks();

SDL_GetTicks() gives us the number of milliseconds that passed since the program started. Thanks to this, we can use the current time when calculating which sprite to use. We can then simply divide by 1000 to convert milliseconds to seconds:

        Uint32 seconds = ticks / 1000;

We then divide the seconds by the number of sprites in our spritesheet, in this case 4. Using the modulus operator ensures that the sprite number wraps around, so it is never greater than 3 (remember that counting is always zero-based, so our sprites are numbered 0 to 3).

        Uint32 sprite = seconds % 4;

Finally, we replace our srcrect declaration by the following:

        SDL_Rect srcrect = { sprite * 32, 0, 32, 64 };

Instead of using an x value of zero, as we did before, we’re passing in the sprite value (between 0 and 3, based on the current time) multiplied by 32 (the width of a single sprite). So with each second that passes, the sprite will be extracted from the image at x=0, then x=32, then x=64, then x=96, back to x=0, and so on.

Let’s run this again:

sdl2-spritesheet-irregular

You’ll notice two problems at this stage. First, the animation is very irregular, in fact it doesn’t animate at all unless you move the mouse or something. Second, the sprites seem to be dumped onto one another, as shown by the messy image above.

Fortunately, both of these problems can be solved with code we’ve already used in “Handling Keyboard and Mouse Events in SDL2“. The first issue is because we’re using SDL_WaitEvent(), so the program doesn’t do anything unless some event occurs. Thus, we need to replace our call to SDL_WaitEvent() with a call to SDL_PollEvent():

        while (SDL_PollEvent(&event) != NULL)
        {
            switch (event.type)
            {
                case SDL_QUIT:
                    quit = true;
                    break;
            }
        }

The second problem is because we are drawing sprites without clearing the ones we drew before. All we need to do is add a call to SDL_RenderClear() before we call SDL_RenderCopy():

        SDL_RenderClear(renderer);

Great! You can now admire our little character shuffling at one frame per second:

sdl2-spritesheet-goodslow

It’s good, but it’s a bit slow. We can make it faster by replacing the animation code before the srcrect declaration with the following (10 frames per second):

        Uint32 ticks = SDL_GetTicks();
        Uint32 sprite = (ticks / 100) % 4;

Woohoo! 😀 Look at that little guy dance! (The image below is animated, but this seems only to work in Firefox.)

sdl2-spritesheet-animated

So in this article, we learned how to animate simple characters using sprite sheets, which are really just a digital version of a cartoon. We used SDL_RenderCopy()‘s srcrect parameter to draw just a single sprite from the sheet at a time, and selected that sprite using the current time based on SDL_GetTicks().

11 thoughts on “Animations with Sprite Sheets in SDL2”

  1. Cool. How would I trim the sprite if it’s half outside the window?

    should I draw everything to a larger renderer/texture and then draw only that to the “official” renderer, or would you go through every single sprite and trim the SDL_Rect size?

      1. What I mean is that I cannot plot a sprite with a negative position (x or y, in SDL_Rect), right? (maybe SDL could figure it out, though I doubt it)
        Thus I need a larger canvas to compose the entire scene and only then use that as a source to the final display.
        After some research, this seems the way to go. Gotta set the texture as “stream” though..
        Thanks. 🙂

        1. PS: Maybe what you said would work for the right and lower margins but the up and left would need room as large as the wider sprite. Am I missing something?

        2. Are you sure you can’t just give the rect negative values? Sounds like you might be overcomplicating things, to me. Then again, it’s been a while.

          1. Yep. You can, I was just waiting to see if you’d reply =) Both solutions work. On Lazy Foo’s tutorials, the motivation given for the indirect route is as “a camera” but one could also use this to generate stuff at run-time and have a game/app which doesn’t simply rely on pre-made assets (I wonder if any games have done this at any large extent, can you think of any?)
            As a correction, the texture must be set as “target”. The tutorial didn’t change the function defaults, which tripped me up 🙂
            Thanks!

  2. Hey!
    I just thought that I’d comment since I found this to be an excellent resource. I’m learning C++, and learning this library at the same time seemed to be a bit challenging without knowing where to jump in.
    Thanks so much!

  3. hi, thanks alot! here’s my code. took a while but now it works!

    int main(int argc, char *argv[])
    {
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
    {
    std::cout << "Initialization failed. Error: " << SDL_GetError();
    }

    IMG_Init(IMG_INIT_PNG);

    SDL_Window *GameWindow = SDL_CreateWindow("Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 360, SDL_WINDOW_SHOWN);
    SDL_Renderer *GameRenderer = SDL_CreateRenderer(GameWindow, -1, 0);

    Ibuki IbukiFrames; //from Ibuki class, all individual png's loaded in this class
    SDL_Texture *IbukiTexture0 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_00);
    SDL_Texture *IbukiTexture1 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_01);
    SDL_Texture *IbukiTexture2 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_02);
    SDL_Texture *IbukiTexture3 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_03);
    SDL_Texture *IbukiTexture4 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_04);
    SDL_Texture *IbukiTexture5 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_05);
    SDL_Texture *IbukiTexture6 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_06);
    SDL_Texture *IbukiTexture7 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_07);
    SDL_Texture *IbukiTexture8 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_08);
    SDL_Texture *IbukiTexture9 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_09);
    SDL_Texture *IbukiTexture10 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_10);
    SDL_Texture *IbukiTexture11 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_11);
    SDL_Texture *IbukiTexture12 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_12);
    SDL_Texture *IbukiTexture13 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_13);
    SDL_Texture *IbukiTexture14 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_14);
    SDL_Texture *IbukiTexture15 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_15);
    SDL_Texture *IbukiTexture16 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_16);
    SDL_Texture *IbukiTexture17 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_17);
    SDL_Texture *IbukiTexture18 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_18);
    SDL_Texture *IbukiTexture19 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_19);
    SDL_Texture *IbukiTexture20 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_20);
    SDL_Texture *IbukiTexture21 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_21);
    SDL_Texture *IbukiTexture22 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_22);
    SDL_Texture *IbukiTexture23 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_23);
    SDL_Texture *IbukiTexture24 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_24);
    SDL_Texture *IbukiTexture25 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_25);
    SDL_Texture *IbukiTexture26 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_26);
    SDL_Texture *IbukiTexture27 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_27);
    SDL_Texture *IbukiTexture28 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_28);
    SDL_Texture *IbukiTexture29 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_29);

    SDL_Rect dstrect{0, 0, 95, 98};

    bool gameloop = true;
    while (gameloop)
    {
    SDL_SetRenderDrawColor(GameRenderer, 150, 100, 75, 255);
    IbukiTexture0 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_00);
    SDL_RenderCopy(GameRenderer, IbukiTexture0, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture0);
    IbukiTexture1 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_01);
    SDL_RenderCopy(GameRenderer, IbukiTexture1, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture1);
    IbukiTexture2 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_02);
    SDL_RenderCopy(GameRenderer, IbukiTexture2, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture2);
    IbukiTexture3 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_03);
    SDL_RenderCopy(GameRenderer, IbukiTexture3, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture3);
    IbukiTexture4 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_04);
    SDL_RenderCopy(GameRenderer, IbukiTexture4, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture4);
    IbukiTexture5 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_05);
    SDL_RenderCopy(GameRenderer, IbukiTexture5, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture5);
    IbukiTexture6 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_06);
    SDL_RenderCopy(GameRenderer, IbukiTexture6, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture6);
    IbukiTexture7 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_07);
    SDL_RenderCopy(GameRenderer, IbukiTexture7, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture7);
    IbukiTexture8 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_08);
    SDL_RenderCopy(GameRenderer, IbukiTexture8, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture8);
    IbukiTexture9 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_09);
    SDL_RenderCopy(GameRenderer, IbukiTexture9, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture9);
    IbukiTexture10 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_10);
    SDL_RenderCopy(GameRenderer, IbukiTexture10, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture10);
    IbukiTexture11 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_11);
    SDL_RenderCopy(GameRenderer, IbukiTexture11, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture11);
    IbukiTexture12 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_12);
    SDL_RenderCopy(GameRenderer, IbukiTexture12, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture12);
    IbukiTexture13 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_13);
    SDL_RenderCopy(GameRenderer, IbukiTexture13, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture13);
    IbukiTexture14 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_14);
    SDL_RenderCopy(GameRenderer, IbukiTexture14, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture14);
    IbukiTexture15 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_15);
    SDL_RenderCopy(GameRenderer, IbukiTexture15, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture15);
    IbukiTexture16 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_16);
    SDL_RenderCopy(GameRenderer, IbukiTexture16, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture17);
    IbukiTexture17 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_17);
    SDL_RenderCopy(GameRenderer, IbukiTexture17, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture17);
    IbukiTexture18 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_18);
    SDL_RenderCopy(GameRenderer, IbukiTexture18, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture18);
    IbukiTexture19 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_19);
    SDL_RenderCopy(GameRenderer, IbukiTexture19, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture19);
    IbukiTexture20 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_20);
    SDL_RenderCopy(GameRenderer, IbukiTexture20, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture20);
    IbukiTexture21 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_21);
    SDL_RenderCopy(GameRenderer, IbukiTexture21, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture21);
    IbukiTexture22 = SDL_CreateTextureFromSurface(GameRenderer, IbukiFrames.frame_22);
    SDL_RenderCopy(GameRenderer, IbukiTexture22, NULL, &dstrect);
    SDL_RenderPresent(GameRenderer);
    SDL_Delay(48);
    SDL_RenderClear(GameRenderer);
    SDL_DestroyTexture(IbukiTexture22);
    }

    SDL_DestroyWindow(GameWindow);
    SDL_DestroyRenderer(GameRenderer);
    IMG_Quit();
    SDL_Quit();

    return EXIT_SUCCESS;
    }

    1. Thanks for sharing. I suggest you structure your code better, e.g. use loops and functions to avoid all the repetition. Your code will be a tiny fraction of what you have in there.

  4. Let’s say I have abunch of sprites how would I load them all, an keep them in a count to keep IDs? Would I need to hardcode all the coordinates of each frame an sprite? Is there a way to make it a bit more simple? Sorry if this has been asked a million times.

    1. You don’t hardcode the coordinates of each frame… we’re not even doing that here. All frames in a sprite sheet are usually the same size, so if you know the dimensions of one frame, then you just iterate over to the next one (if your frames are laid out horizontally in the sprite sheet, then you just add the width of the sprite to x each time, and then start from 0 again when you reach the end).

      If you have different sprite sheets with different dimensions, then you can keep this information in a class or struct for each sprite sheet, and apply the same animation logic based on that.

Leave a Reply

Your email address will not be published. Required fields are marked *