Hi I’m trying to implement Sprite Animation with a SpriteSheet but the animations seem very fast and I cannot slow it down. The normal speed is 0.1f, I want it at 5.0f but it does seem to make a difference. Game Project Download Link
int main(int argc, char** argv) {
Uint32 frameStart;
int frameTime;
GameState gameState = MENU;
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
return 1;
}
if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) {
SDL_Log("Failed to initialize SDL_image: %s", IMG_GetError());
SDL_Quit();
return 1;
}
if (TTF_Init() == -1) {
SDL_Log("Failed to initialize SDL_ttf: %s", TTF_GetError());
IMG_Quit();
SDL_Quit();
return 1;
}
if (!soundManager.init()) {
SDL_Log("Failed to initialize SDL_mixer.");
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 1;
}
std::unique_ptr<Game> game = std::make_unique<Game>();
if (!game->init()) {
SDL_Log("Failed to initialize game.");
soundManager.shutdown();
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 1;
}
renderer = game->getRenderer();
// Load fonts
defaultFont = TTF_OpenFont("assets/Font/arial.ttf", FONT_SIZE);
storylineFont = TTF_OpenFont("assets/Font/lucida_blackletter_regular.ttf", FONT_SIZE);
if (!defaultFont || !storylineFont) {
SDL_Log("Failed to load fonts: %s", TTF_GetError());
soundManager.shutdown();
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 1;
}
// Load textures
menuBackdropTexture = TextureManager::LoadTexture("assets/Backdrops/menu_backdrop.png", renderer);
speechBubbleTexture = TextureManager::LoadTexture("assets/speech_bubble.png", renderer);
// Initialize Knight_idle as a Sprite
Knight_idle = new Sprite(renderer, "assets/Sprites/MC/BlueKnight.png");
if (!Knight_idle->loadTexture()) {
SDL_Log("Failed to load Knight idle sprite.");
return 1;
}
Knight_idle->setAnimationParameters(128, 126, 8, 5.0f); // Set animation parameters with 5.0f frame rate
if (!menuBackdropTexture || !speechBubbleTexture) {
SDL_Log("Failed to load textures.");
TTF_CloseFont(defaultFont);
TTF_CloseFont(storylineFont);
soundManager.shutdown();
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 1;
}
// Load music
if (!soundManager.loadMusic("assets/Music/background_music.wav", "background")) {
SDL_Log("Failed to load background music.");
}
if (!soundManager.loadMusic("assets/Music/story_music.wav", "story")) {
SDL_Log("Failed to load story music.");
}
if (!soundManager.loadMusic("assets/Music/combat_music.wav", "combat")) {
SDL_Log("Failed to load combat music.");
}
if (!soundManager.loadMusic("assets/Music/Lvl1_Story.wav", "lvl1_story")) {
SDL_Log("Failed to load Level 1 Story music.");
}
// Initialize story instances
static Story storyInstance(renderer, storylineFont, storyLines, 50);
static Story tutorialStoryInstance(renderer, storylineFont, tutorialStoryLines, 50);
static Story level1StoryInstance(renderer, storylineFont, level1StoryLines, 50);
static Story level1WinInstance(renderer, storylineFont, level1Win, 50);
static Story level1LoseInstance(renderer, storylineFont, level1Lose, 50);
// Button setup
const int BUTTON_WIDTH = 180;
const int ABOUT_BUTTON_WIDTH = 250;
const int BUTTON_HEIGHT = 70;
const int BUTTON_SPACING = 50;
const int NUM_BUTTONS = 4;
int windowWidth, windowHeight;
SDL_GetRendererOutputSize(renderer, &windowWidth, &windowHeight);
int totalButtonHeight = NUM_BUTTONS * BUTTON_HEIGHT + (NUM_BUTTONS - 1) * BUTTON_SPACING;
int startY = (windowHeight - totalButtonHeight) / 2 + 50;
Button buttons[NUM_BUTTONS] = {
Button((windowWidth - BUTTON_WIDTH) / 2, startY, BUTTON_WIDTH, BUTTON_HEIGHT, "assets/Buttons/start button.png", "assets/Buttons/clicked sta2.png", "assets/Buttons/clicked sta2.png", renderer),
Button((windowWidth - BUTTON_WIDTH) / 2, startY + (BUTTON_HEIGHT + BUTTON_SPACING), BUTTON_WIDTH, BUTTON_HEIGHT, "assets/Buttons/tutorial button.png", "assets/Buttons/clicked tutorial2.png", "assets/Buttons/clicked tutorial2.png", renderer),
Button((windowWidth - BUTTON_WIDTH) / 2, startY + 2 * (BUTTON_HEIGHT + BUTTON_SPACING), BUTTON_WIDTH, BUTTON_HEIGHT, "assets/Buttons/option button.png", "assets/Buttons/option_clicked.png", "assets/Buttons/option_clicked.png", renderer),
Button((windowWidth - ABOUT_BUTTON_WIDTH) / 2, startY + 3 * (BUTTON_HEIGHT + BUTTON_SPACING), ABOUT_BUTTON_WIDTH, BUTTON_HEIGHT, "assets/Buttons/about develper button.png", "assets/Buttons/clicked ab de 2.png", "assets/Buttons/clicked ab de 2.png", renderer)
};
buttons[0].setOnClick([&]() {
gameState = STORY;
storyInstance.reset();
storyBackdropTexture = TextureManager::LoadTexture("assets/Backdrops/castle/castle1.jpg", renderer);
soundManager.stopMusic();
soundManager.playMusic("story", -1);
});
buttons[1].setOnClick([&]() {
gameState = TUTORIAL;
tutorialStoryInstance.reset();
});
buttons[2].setOnClick([&]() {
gameState = OPTIONS;
});
buttons[3].setOnClick([&]() {
gameState = ABOUT;
});
int musicVolume = MIX_MAX_VOLUME;
SDL_Texture* combatBackdropTexture = nullptr;
// Main Game Loop
while (gameState != EXIT) {
float deltaTime = time_(); // Get delta time since the last frame
frameStart = SDL_GetTicks();
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT || (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)) {
gameState = EXIT;
}
// Handle returning to menu
if ((gameState == TUTORIAL || gameState == OPTIONS || gameState == ABOUT ||
gameState == LEVEL1_LOSE) && event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_b) {
gameState = MENU;
for (int i = 0; i < NUM_BUTTONS; ++i) {
buttons[i].resetState();
}
}
}
// Handle story progression
if ((gameState == STORY || gameState == LEVEL1_STORY || gameState == LEVEL1_WIN || gameState == LEVEL1_LOSE) &&
event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_SPACE) {
if (gameState == STORY && storyInstance.isComplete()) {
soundManager.stopMusic();
gameState = LEVEL1_STORY;
level1StoryInstance.reset();
SDL_DestroyTexture(storyBackdropTexture);
storyBackdropTexture = nullptr;
soundManager.playMusic("lvl1_story", -1);// Play Level 1 Story music
}
else if (gameState == LEVEL1_STORY && level1StoryInstance.isComplete()) {
// Load forest backdrop for combat
combatBackdropTexture = TextureManager::LoadTexture("assets/Backdrops/forest/forest2.jpg", renderer);
gameState = PLAYING;
initCombat(1);
soundManager.playMusic("combat", -1);// Play combat music
}
else if (gameState == LEVEL1_WIN && level1WinInstance.isComplete()) {
gameState = MENU;
}
else if (gameState == STORY) {
storyInstance.advanceToNextLine();
}
else if (gameState == LEVEL1_STORY) {
level1StoryInstance.advanceToNextLine();
}
else if (gameState == LEVEL1_WIN) {
level1WinInstance.advanceToNextLine();
}
else if (gameState == LEVEL1_LOSE) {
level1LoseInstance.advanceToNextLine();
}
}
}
// Handle tutorial
if (gameState == TUTORIAL && event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_SPACE) {
if (!tutorialStoryInstance.isComplete()) {
tutorialStoryInstance.advanceToNextLine();
}
}
else if (event.key.keysym.sym == SDLK_b && tutorialStoryInstance.isComplete()) {
gameState = MENU;
}
}
// Handle buttons
if (gameState == MENU) {
for (int i = 0; i < NUM_BUTTONS; ++i) {
buttons[i].handleEvent(&event);
}
}
// Handle combat
if (gameState == PLAYING) {
CombatContext context = {
renderer,
defaultFont,
{255, 255, 255, 255},
{255, 0, 0, 255},
{0, 0, 255, 255},
{0, 0, 0, 255}
};
handleCombatEvent(&event, &context);
if (gameOver) {
SDL_DestroyTexture(combatBackdropTexture);
combatBackdropTexture = nullptr;
gameState = LEVEL1_LOSE;
level1LoseInstance.reset();
soundManager.stopMusic();
soundManager.playMusic("story", -1);
}
else if (victory1) {
SDL_DestroyTexture(combatBackdropTexture);
combatBackdropTexture = nullptr;
gameState = LEVEL1_WIN;
level1WinInstance.reset();
soundManager.stopMusic();
soundManager.playMusic("story", -1);
}
}
}
// Update knight animation
if (Knight_idle) {
Knight_idle->updateAnimation(deltaTime);
}
// Rendering
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
switch (gameState) {
case MENU:
if (!soundManager.isPlayingMusic()) {
soundManager.playMusic("background", -1);
}
SDL_RenderCopy(renderer, menuBackdropTexture, nullptr, nullptr);
for (int i = 0; i < NUM_BUTTONS; ++i) {
buttons[i].render(renderer);
}
break;
case STORY: {
if (storyBackdropTexture) {
SDL_RenderCopy(renderer, storyBackdropTexture, nullptr, nullptr);
}
SDL_Rect speechBubbleRect = {
(windowWidth - 800) / 2,
windowHeight - 400,
800,
300
};
SDL_RenderCopy(renderer, speechBubbleTexture, nullptr, &speechBubbleRect);
storyInstance.update(SDL_GetTicks());
storyInstance.render(renderer,
speechBubbleRect.x + 50,
speechBubbleRect.y + 90,
TEXT_WRAP_WIDTH);
if (storyInstance.isComplete()) {
renderText(renderer, defaultFont, "Press SPACE to continue", { 255, 255, 255, 255 },
(windowWidth - 300) / 2, windowHeight - 50);
}
break;
}
case LEVEL1_STORY: {
// Use castle ruins backdrop for story
static SDL_Texture* level1StoryBackdrop = nullptr;
if (!level1StoryBackdrop) {
level1StoryBackdrop = TextureManager::LoadTexture("assets/Backdrops/castle/castle1.jpg", renderer);
}
if (level1StoryBackdrop) {
SDL_RenderCopy(renderer, level1StoryBackdrop, nullptr, nullptr);
}
// Render the knight character
if (Knight_idle) {
SDL_Rect knightRect = {
((windowWidth - 200) / 2) - 500, // X Position
windowHeight - 700, // Y Position
200, // Width
300 // Height
};
Knight_idle->render(knightRect.x, knightRect.y, knightRect.w, knightRect.h);
}
SDL_Rect speechBubbleRect = {
(windowWidth - 800) / 2,
windowHeight - 400,
800,
300
};
SDL_RenderCopy(renderer, speechBubbleTexture, nullptr, &speechBubbleRect);
level1StoryInstance.update(SDL_GetTicks());
level1StoryInstance.render(renderer,
speechBubbleRect.x + 50,
speechBubbleRect.y + 90,
TEXT_WRAP_WIDTH);
if (level1StoryInstance.isComplete()) {
renderText(renderer, defaultFont, "Press SPACE to continue", { 255, 255, 255, 255 },
(windowWidth - 300) / 2, windowHeight - 50);
}
break;
}
case PLAYING: {
// Only use forest backdrop during combat
if (combatBackdropTexture) {
SDL_RenderCopy(renderer, combatBackdropTexture, nullptr, nullptr);
}
CombatContext context = {
renderer,
defaultFont,
{255, 255, 255, 255},
{255, 0, 0, 255},
{0, 0, 255, 255},
{0, 0, 0, 255}
};
renderCombat(&context);
break;
}
case LEVEL1_WIN: {
// Use same backdrop as level 1 story for consistency
static SDL_Texture* level1StoryBackdrop = nullptr;
if (!level1StoryBackdrop) {
level1StoryBackdrop = TextureManager::LoadTexture("assets/Backdrops/castle/castle1.jpg", renderer);
}
if (level1StoryBackdrop) {
SDL_RenderCopy(renderer, level1StoryBackdrop, nullptr, nullptr);
}
SDL_Rect speechBubbleRect = {
(windowWidth - 800) / 2,
windowHeight - 400,
800,
300
};
SDL_RenderCopy(renderer, speechBubbleTexture, nullptr, &speechBubbleRect);
level1WinInstance.update(SDL_GetTicks());
level1WinInstance.render(renderer,
speechBubbleRect.x + 50,
speechBubbleRect.y + 90,
TEXT_WRAP_WIDTH);
if (level1WinInstance.isComplete()) {
renderText(renderer, defaultFont, "Press SPACE to continue", { 255, 255, 255, 255 },
(windowWidth - 300) / 2, windowHeight - 50);
}
break;
}
case LEVEL1_LOSE: {
// Use same backdrop as level 1 story for consistency
static SDL_Texture* level1StoryBackdrop = nullptr;
if (!level1StoryBackdrop) {
level1StoryBackdrop = TextureManager::LoadTexture("assets/Backdrops/castle/castle1.jpg", renderer);
}
if (level1StoryBackdrop) {
SDL_RenderCopy(renderer, level1StoryBackdrop, nullptr, nullptr);
}
SDL_Rect speechBubbleRect = {
(windowWidth - 800) / 2,
windowHeight - 400,
800,
300
};
SDL_RenderCopy(renderer, speechBubbleTexture, nullptr, &speechBubbleRect);
level1LoseInstance.update(SDL_GetTicks());
level1LoseInstance.render(renderer,
speechBubbleRect.x + 50,
speechBubbleRect.y + 90,
TEXT_WRAP_WIDTH);
if (level1LoseInstance.isComplete()) {
renderText(renderer, defaultFont, "Press SPACE to return to menu", { 255, 255, 255, 255 },
(windowWidth - 300) / 2, windowHeight - 50);
}
break;
}
//other cases
SDL_RenderPresent(renderer);
frameTime = SDL_GetTicks() - frameStart;
if (FRAME_DELAY > frameTime) {
SDL_Delay(FRAME_DELAY - frameTime);
}
}
// Cleanup
SDL_DestroyTexture(menuBackdropTexture);
SDL_DestroyTexture(speechBubbleTexture);
SDL_DestroyTexture(storyBackdropTexture);
delete Knight_idle; // Clean up the Sprite object
soundManager.shutdown();
TTF_CloseFont(defaultFont);
TTF_CloseFont(storylineFont);
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 0;
}
#ifndef SPRITE_H
#define SPRITE_H
#include <SDL.h>
#include <string>
// Animation frame definitions
#define TZ_H (512 / 4) // Width of a single frame (128px)
#define TZ_V (252 / 2) // Height of a single frame (126px)
class Sprite {
public:
Sprite(SDL_Renderer* renderer, const std::string& filePath);
~Sprite();
bool loadTexture();
void render(int x, int y);
void updateAnimation(float deltaTime);
void setSize(int width, int height);
int getWidth() const;
int getHeight() const;
SDL_Texture* getTexture() const;
void destroyTexture();
void render(int x, int y, int renderWidth = -1, int renderHeight = -1);
// Method to set the frame rate dynamically
void setFrameRate(float rate);
// Method to set animation parameters
void setAnimationParameters(int frameW, int frameH, int frames, float rate);
// Quick setup for standard animations
void setupDefaultAnimation();
private:
SDL_Renderer* renderer;
SDL_Texture* texture;
std::string filePath;
int width;
int height;
int frameWidth;
int frameHeight;
int numFrames;
float frameTimer;
float frameRate;
int currentFrame;
};
#endif // SPRITE_H
#include "Sprite.h"
#include <SDL_image.h>
#include <stdio.h>
Sprite::Sprite(SDL_Renderer* renderer, const std::string& filePath)
: renderer(renderer), texture(nullptr), filePath(filePath),
width(0), height(0), frameWidth(0), frameHeight(0),
numFrames(0), frameTimer(0.0f), frameRate(0.1f), currentFrame(0) {
}
Sprite::~Sprite() {
if (texture) {
SDL_DestroyTexture(texture);
}
}
bool Sprite::loadTexture() {
SDL_Surface* surface = IMG_Load(filePath.c_str());
if (!surface) {
fprintf(stderr, "Failed to load image %s: %s\n", filePath.c_str(), IMG_GetError());
return false;
}
texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface);
if (!texture) {
fprintf(stderr, "Failed to create texture: %s\n", SDL_GetError());
return false;
}
SDL_QueryTexture(texture, nullptr, nullptr, &width, &height);
return true;
}
void Sprite::setAnimationParameters(int frameW, int frameH, int frames, float rate) {
frameWidth = frameW;
frameHeight = frameH;
numFrames = frames;
frameRate = rate;
}
void Sprite::render(int x, int y, int renderWidth, int renderHeight) {
if (!texture) {
fprintf(stderr, "Texture not loaded for sprite: %s\n", filePath.c_str());
return;
}
// Calculate source rectangle based on current frame
SDL_Rect srcRect = {
(currentFrame % (width / frameWidth)) * frameWidth,
(currentFrame / (width / frameWidth)) * frameHeight,
frameWidth,
frameHeight
};
// Determine output size (use frame dimensions if not specified)
int outputWidth = (renderWidth == -1) ? frameWidth : renderWidth;
int outputHeight = (renderHeight == -1) ? frameHeight : renderHeight;
SDL_Rect dstRect = { x, y, outputWidth, outputHeight };
SDL_RenderCopy(renderer, texture, &srcRect, &dstRect);
}
// Overloaded render function with default parameters
void Sprite::render(int x, int y) {
render(x, y, -1, -1); // Call the main render function with default values
}
void Sprite::updateAnimation(float deltaTime) {
frameTimer += deltaTime;
while (frameTimer >= frameRate) {
currentFrame = (currentFrame + 1) % numFrames;
frameTimer -= frameRate;
}
}
void Sprite::setSize(int width, int height) {
this->width = width;
this->height = height;
}
int Sprite::getWidth() const { return width; }
int Sprite::getHeight() const { return height; }
SDL_Texture* Sprite::getTexture() const {
return texture;
}
void Sprite::destroyTexture() {
if (texture) {
SDL_DestroyTexture(texture);
texture = nullptr;
}
}