SDL_Init(SDL_INIT_VIDEO);
SDL_Window *window = SDL_CreateWindow("SDL3 Sprite Animation", 200, 200, SDL_WINDOW_OPENGL);
SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
SDL_Surface *spriteSheetSurface = IMG_Load("adventurer-sheet.png");
if (!spriteSheetSurface) {
std::cerr << "Failed to load sprite sheet: " << SDL_GetError() << std::endl;
return 1;
}
Here the sprite spritesheet has transparency but if you use an old spritesheet where there is a key color you can define the key color like this:
SDL_SetSurfaceColorKey( spriteSheetSurface,
true,
SDL_MapRGB(SDL_GetPixelFormatDetails(spriteSheetSurface->format), NULL, 255, 0, 255) // Magenta
);
SDL_Texture *spriteSheet = SDL_CreateTextureFromSurface(renderer, spriteSheetSurface);
SDL_DestroySurface(spriteSheetSurface);
Since the spritesheet is column and rows based, we can define the same area for each sprite:
size_t nbRow = 11;
size_t nbCol = 7;
size_t widthSpr = 50;
size_t heightSpr = 37;
std::vector<SDL_FRect> rects;
for (size_t i = 0; i < nbRow; i++) {
for (size_t j = 0; j < nbCol; j++) {
rects.push_back(SDL_FRect{ static_cast<float>(j * widthSpr),
static_cast<float>(i * heightSpr),
static_cast<float>(widthSpr),
static_cast<float>(heightSpr) });
}
}
We can then define the serie of image for each animations:
std::vector<std::pair<size_t, size_t>> idle1 { {0, 0}, {0, 1}, {0, 2}, {0, 3} };
std::vector<std::pair<size_t, size_t>> crouch { {0, 4}, {0, 5}, {0, 6}, {1, 0} };
std::vector<std::pair<size_t, size_t>> run { {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6} };
To record the time for each frame we use a buffer and a duration, the time elapsed is also stored:
double maxDuration = 150; // in ms for one frame of the aniamtion
double timeBuffer = 0; // in ms
double timeElapsed = 0; // in ms
// before the event loop we use init a counter
double elapsedNano = 0; // time elasped in nano seconds
auto t1 = Clock::now();
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) done = true;
if (event.type == SDL_EVENT_KEY_DOWN) {
SDL_Keycode code = event.key.key;
if (code == SDLK_ESCAPE) done = true;
else if (code == SDLK_Q) current = idle1;
else if (code == SDLK_W) current = crouch;
else if (code == SDLK_E) current = run;
index = 0;
}
}
We can render the current animatioon selected, the mapping to transform the indexed i, j to the sprite sheet is given by:
auto currentPair = current[index];
size_t pos = currentPair.second + currentPair.first * nbCol;
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
auto currentPair = current[index];
size_t pos = currentPair.second + currentPair.first * nbCol;
SDL_FRect src = rects[pos];
SDL_FRect dst = {75, 75, static_cast<float>(widthSpr), static_cast<float>(heightSpr)};
SDL_RenderTexture(renderer, spriteSheet, &src, &dst);
SDL_RenderPresent(renderer);
Then we need to update the animation base on the time elapsed and time buffer.
It's pretty simple if the buffer is larger than the maxDuration we update the index of the animation.
timeBuffer += timeElapsed;
if (timeBuffer > maxDuration) {
timeBuffer = 0;
index = (index + 1) % current.size();
}
We need to limit the framerate for that we use the elapsed nano variable :
auto t2 = Clock::now();
elapsedNano = static_cast<double>(std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count());
if (elapsedNano > 0) {
double delayMs = ((1000000000.0 / 30.0) - elapsedNano) / 1000000.0;
if (delayMs > 0) SDL_Delay(static_cast<Uint32>(delayMs));
}
And we also need too update the time of the animation:
auto t3 = Clock::now();
timeElapsed = static_cast<double>(std::chrono::duration_cast<std::chrono::nanoseconds>(t3 - t1).count()) / 1000000.0;
You can download the full project:
Need another OS ? => Windows, Mac, Linux