For the past few days I've been trying to learn how to animate 2D sprites using SDL2. For that purpose, I've taken a 6 frames animation from a Pokémon game and made a PNG image out of it :
The way I figured out would work fine is is by creating an array leaf_frames
(Leaf being the name of the character I used) containing the position and size of each frame in the image, and have another array leaf_frames_indices
containing the index of the frame in leaf_frames
to be displayed at a certain time into the animation. Each time I want to display a frame, I will simply tell the SDL_RenderCopy
function which portion of the image to copy on the screen using those arrays and the current frame (given by current_frame
). This is how I implemented it.
main.c:
#include <stdio.h>
#include <stdlib.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "config.h"
void keyboard_poll (SDL_Event *, int *);
SDL_Texture *load_image (SDL_Renderer *, const char *);
#define len(x) (sizeof(x) / sizeof(x[0]))
#define eprintf(...) fprintf(stderr, __VA_ARGS__)
#define key_pressed(keys, keysold, key) (keys[key] && !keysold[key])
#define key_released(keys, keysold, key) (!keys[key] && keysold[key])
void
keyboard_poll (SDL_Event *event, int *keys)
{
SDL_Scancode keycode = event->key.keysym.scancode; /* not necessary, but makes the following code easier to read and write */
if (keycode < SDL_NUM_SCANCODES) { /* out of bounds safety check */
if (event->type == SDL_KEYDOWN) keys[keycode] = 1;
if (event->type == SDL_KEYUP) keys[keycode] = 0;
}
}
SDL_Texture *
load_image (SDL_Renderer *renderer, const char *image_path)
{
SDL_Surface *image_surface = IMG_Load(image_path);
if (!image_surface) {
eprintf("Could not load image %s : %s\n", image_path, IMG_GetError());
return NULL;
}
SDL_Texture *image_texture = SDL_CreateTextureFromSurface(renderer, image_surface);
SDL_FreeSurface(image_surface);
return image_texture;
}
int
main (void)
{
int retval = EXIT_SUCCESS;
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
eprintf("Cannot initialize SDL : %s\n", SDL_GetError());
retval = EXIT_FAILURE;
goto cleanup_exit;
}
int img_flags = IMG_INIT_PNG; /* check whether what was asked to be initialized has been initialized or not, ignore the rest */
if ((IMG_Init(img_flags) & img_flags) != img_flags) {
eprintf("Cannot initialize SDL_image : %s\n", IMG_GetError());
retval = EXIT_FAILURE;
goto cleanup_sdl;
}
SDL_Window *window = SDL_CreateWindow(CONF_APP_NAME, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, CONF_WINDOW_WIDTH, CONF_WINDOW_HEIGHT, 0);
if (!window) {
eprintf("Cannot create window : %s\n", SDL_GetError());
retval = EXIT_FAILURE;
goto cleanup_img;
}
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!renderer) {
eprintf("Cannot create renderer : %s\n", SDL_GetError());
retval = EXIT_FAILURE;
goto cleanup_window;
}
SDL_Texture *leaf_sprite = load_image(renderer, "leafchan-animated.png");
if (!leaf_sprite) {
retval = EXIT_FAILURE;
goto cleanup_renderer;
}
/* initialization ends here, declare some "quick and dirty" data to be used with the animation */
SDL_Rect leaf_frames[] = { /* the array contains the coordinates of each frame */
{.h = 19, .w = 16, .x = 0, .y = 0},
{.h = 19, .w = 16, .x = 16, .y = 0},
{.h = 19, .w = 16, .x = 32, .y = 0},
{.h = 19, .w = 16, .x = 48, .y = 0},
{.h = 19, .w = 16, .x = 64, .y = 0},
{.h = 19, .w = 16, .x = 80, .y = 0}
};
SDL_Rect destrect = {
.x = 0,
.y = 0,
.h = CONF_WINDOW_HEIGHT,
.w = CONF_WINDOW_WIDTH
};
int leaf_frames_indices[] = {0, 1, 2, 3, 4, 5, 4, 5, 4, 5, 4, 5, 4, 3, 2, 1, 0};
int current_frame = 0;
int total_frames = len(leaf_frames_indices);
int running = 1;
int should_quit = 0; /* might be better off in a flags[] array ? */
SDL_Event event;
int keys[SDL_NUM_SCANCODES] = {0}; /* group those two in a struct ? */
int keysold[SDL_NUM_SCANCODES] = {0};
/* actual game loop */
while (running) {
/* read all events, update keyboard state */
memcpy(keysold, keys, sizeof(keys));
while (SDL_PollEvent(&event)) {
keyboard_poll(&event, keys);
if (event.type == SDL_QUIT)
should_quit = 1;
}
/* react to events */
if (key_released(keys, keysold, CONF_KEY_EXIT) || should_quit)
running = 0;
/* draw */
SDL_SetRenderDrawColor(renderer, CONF_CLEAR_COLOR, 255);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, leaf_sprite, &leaf_frames[leaf_frames_indices[current_frame]], &destrect);
SDL_RenderPresent(renderer);
/* dirty time management, for demo purposes */
SDL_Delay(100);
/* used to be a 4 lines if else statement, but I just find one-liners to be more elegant, personal taste */
current_frame = (current_frame < (total_frames - 1)) ? current_frame + 1 : 0;
}
/* cleanup */
SDL_DestroyTexture(leaf_sprite);
cleanup_renderer:
SDL_DestroyRenderer(renderer);
cleanup_window:
SDL_DestroyWindow(window);
cleanup_img:
IMG_Quit();
cleanup_sdl:
SDL_Quit();
cleanup_exit:
return retval;
}
config.h:
#define CONF_APP_NAME "Animations SDL2"
#define CONF_WINDOW_WIDTH 128
#define CONF_WINDOW_HEIGHT 152
#define CONF_PIXELFORMAT SDL_PIXELFORMAT_RGBA8888
#define CONF_CLEAR_COLOR 0, 5, 25
#define CONF_KEY_EXIT SDL_SCANCODE_ESCAPE
And sure enough once I start it the animation runs perfectly :
Now that everything works fine, and that I'm pretty satisfied with my error handling, I'm still unsure as for the animation method I used and how I could group all of these things.
My first improvement suggestion would be to create a struct and have all of the animation-related data inside it, but then I feel that's a bit too specific to animations, and would not mix well with functions that use static sprites. What I could do as well, is to have a Sprite
struct which would work for both animated and static sprites via an element .animated
specifying whether or not the sprite is animated, so the drawing function knows how to handle it. Or maybe I could consider every sprite to be an animation, with static sprites only having one frame, which would spare me the need to write multiple implementations of drawing functions.
How should I go about this? I'm unsure how to pack all this together and have it be consistent. Or maybe there's a better way to deal with animations?
Note : I'm using the C99 standard.