Gigi Labs

Please follow Gigi Labs for the latest articles.

Sunday, March 30, 2014

SDL2: Animations with Sprite Sheets

Hi people! :)

[Update 2015-11-14: This article is out of date. Check out the latest version at Gigi Labs.]

We've worked with images in several of the SDL2 tutorials published so far. In today's article, we're going to use a very simple technique to animate our images and make them feel more alive.

Before we start, we first need to set up a new project as follows:

  1. Follow the steps in "SDL2: Setting up SDL2 in Visual Studio (2013 or any other)" to set up an SDL2 project.
  2. Follow the steps at the beginning of "SDL2: Loading Images with SDL_image" to use SDL_image in your project.
  3. Configure your project's working directory as described in "SDL2: Displaying text with SDL_ttf", i.e. in your project's Properties -> Configuration Properties -> Debugging, set Working Directory to $(SolutionDir)$(Configuration)\ .
  4. Start off with the following code, adapted from "SDL2: Loading Images with SDL_image":
#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;
}


Before we do anything, we need a sprite sheet. A sprite sheet is really just a cartoon. Like this:


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! :D

Now, those ugly borders in the image above are just for demonstration purposes. Here's the same image, without borders and with transparency:


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 "SDL2: Keyboard and Mouse Movement (Events)". 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:


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:


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:


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 "SDL2: Keyboard and Mouse Movement (Events)". 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():

        SDL_PollEvent(&event);


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:


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! :D Look at that little guy dance!

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(). The full code is below.

Thanks for reading! Stay tuned for more articles. :)

#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);

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

    while (!quit)
    {
        Uint32 ticks = SDL_GetTicks();
        Uint32 sprite = (ticks / 100) % 4;
        SDL_Rect srcrect = { sprite * 32, 0, 32, 64 };
        SDL_Rect dstrect = { 10, 10, 32, 64 };

        SDL_PollEvent(&event);

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

        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer, texture, &srcrect, &dstrect);
        SDL_RenderPresent(renderer);
    }

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

    return 0;
}

8 comments:

  1. an excellent and concise tutorial - well done - do more!

    ReplyDelete
  2. Great tutorial, once I got SDL up and running, I have progressed much more with these than the others available! :)

    ReplyDelete
  3. I've got a problem: when I execute this code the SDLWindow never forms no matter how much time I wait. Is there some variable that must be changed?

    ReplyDelete
    Replies
    1. There isn't much I can tell you from the little information you provided. I suggest you start with one of the earlier articles (e.g. empty window) to try and isolate the problem.

      Delete
    2. Double check the file path of your image.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Hi, great work, but how do i make it move when i pressed an arrow key?

    ReplyDelete
    Replies
    1. Use keypress events for that... I have an article on how they work.

      Delete

Note: Only a member of this blog may post a comment.