C++ Game Engine Development – Part 30 – One Shot Animations

This is part of an ongoing series where we write a complete 2D game engine in C++ and SFML. A new tutorial is released every Monday. You can find the complete list of tutorials here and download the source code from the projects GitHub page.

Last week we ran into an issue with our animation system. Whenever we played the projectile animation we were stuck in that animation as we had no way of transitioning out of it. In this tutorial, we’ll add the ability to set whether an animation is looped or only plays the once. We’ll look at animation transitions next week.

Unsurprisingly we start this week by making changes to the Animation class. We’ll add a bool to define whether an Animation is looped and a getter and setter to provide access to the bool.
Animation.hpp
class Animation
{
public:
…
	void SetLooped(bool looped);
    bool IsLooped();
    
private:
…
    bool isLooped;
};
We set the default looped state to true in the constructor.
Animation.cpp
Animation::Animation() : frames(0), currentFrameIndex(0), 
currentFrameTime(0.f), releaseFirstFrame(true), 
isLooped(true) { }

void Animation::SetLooped(bool looped)
{
    isLooped = looped;
}

bool Animation::IsLooped()
{
    return isLooped;
}
With our flag created the first change we’ll make is to how we increment frames. Currently, the animation will increment the frame when the current one expires regardless of whether it is the last frame or first. Now we have the looped flag we need to add an additional check.
Animation.cpp
bool Animation::UpdateFrame(float deltaTime)
{
    if(releaseFirstFrame)
    {
        RunActionForCurrentFrame();
        releaseFirstFrame = false;
        return true;
    }
    
	// We now also check if animation is looped 
    if(frames.size() > 1 && 
	   (isLooped || 
		currentFrameIndex < frames.size() - 1))
    {
        currentFrameTime += deltaTime;
        
        if(currentFrameTime >= 
		   frames[currentFrameIndex].displayTimeSeconds)
        {
            currentFrameTime = 0.f;
            IncrementFrame();
            RunActionForCurrentFrame();
            return true;
            
        }
    }
    
    return false;
}
We check if the frame is looped or the current frame index is less than the total frames -1 as we are only interested in the looped flag state when it is the last frame in the animation.
Next, we’ll change the AddFrame function so we also pass in whether the animation is looped.
Animation.hpp
class Animation
{
public:
    Animation();
    
    void AddFrame(int textureID, int x, int y, int width, 
				  int height, float frameTime, bool looped);
…

};
Animation.hpp
void Animation::AddFrame(int textureID, int x, int y, int width, 
						 int height, float frameTime, bool looped)
{
    FrameData data;
    data.id = textureID;
    data.x = x;
    data.y = y;
    data.width = width;
    data.height = height;
    data.displayTimeSeconds = frameTime;
    frames.push_back(data);
    
    isLooped = looped;
}
Animation.hpp
void Animation::AddFrame(int textureID, int x, int y, int width, 
			 int height, float frameTime, bool looped)
{
    FrameData data;
    data.id = textureID;
    data.x = x;
    data.y = y;
    data.width = width;
    data.height = height;
    data.displayTimeSeconds = frameTime;
    frames.push_back(data);
    
    isLooped = looped;
}

As well as passing all the usual frame data (textureID, texture position and size etc.) we also pass in a bool indicating whether the animation is looped.

As we’ve changed the frame insertion function we will also need to update any code calling the function.
SceneGame.cpp
void SceneGame::OnCreate()
{
 …
    
    
    /*******************
     * Idle Animations *
     *******************/
    const bool idleAnimationLooped = false;
    unsigned int idleYFramePos = 512;
    
    std::map<FacingDirection, std::shared_ptr<Animation>> idleAnimations;
    
    for (int i = 0; i < 4; i++)
    {
        std::shared_ptr<Animation> idleAnimation = std::make_shared<Animation>();
        
        idleAnimation->AddFrame(playerTextureID, 0, 
								idleYFramePos, frameWidth, frameHeight, 0.f, 
								idleAnimationLooped);
        
        idleAnimations.insert(std::make_pair(directions[i], idleAnimation));
        
        idleYFramePos += frameHeight;
    }
    
    animation->AddAnimation(AnimationState::Idle, idleAnimations);
    
    
    /**********************
     * Walking Animations *
     **********************/
    const bool walkAnimationLooped = true;
    const int walkingFrameCount = 9;
    const float delayBetweenWalkingFramesSecs = 0.1f;
    
    unsigned int walkingYFramePos = 512;
    
    std::map<FacingDirection, std::shared_ptr<Animation>> walkingAnimations;
    
    for (int i = 0; i < 4; i++)
    {
        std::shared_ptr<Animation> walkingAnimation = std::make_shared<Animation>();
        for (int i = 0; i < walkingFrameCount; i++)
        {
            walkingAnimation->AddFrame(playerTextureID, i * frameWidth, walkingYFramePos, 
									   frameWidth, frameHeight, delayBetweenWalkingFramesSecs, 
									   walkAnimationLooped);
        }
        
        walkingAnimations.insert(std::make_pair(directions[i], walkingAnimation));
        
        walkingYFramePos += frameHeight;
    }
    
    animation->AddAnimation(AnimationState::Walk, walkingAnimations);
    
    
    /*************************
     * Projectile Animations *
     *************************/
    const bool projectileAnimationLooped = false;
    const int projectileFrameCount = 10;
    const float delayBetweenProjectileFramesSecs = 0.1f;

    std::map<FacingDirection, std::shared_ptr<Animation>> projectileAnimations;
    
    unsigned int projFrameYPos = 1024;
    
    for (int i = 0; i < 4; i++)
    {
        std::shared_ptr<Animation> projAnimation = std::make_shared<Animation>();
        for (int i = 0; i < projectileFrameCount; i++)
        {
            projAnimation->AddFrame(playerTextureID, i * frameWidth, projFrameYPos, 
									frameWidth, frameHeight, 
									delayBetweenProjectileFramesSecs, 
									projectileAnimationLooped);
        }
        projectileAnimations.insert(std::make_pair(directions[i], projAnimation));
        
        projFrameYPos += frameHeight;
    }
    
    animation->AddAnimation(AnimationState::Projectile, projectileAnimations);

…
}
I’ve added a flag for each animation that indicates whether the animation is looped or not. Idle and projectile animations are set not to loop and the walking animation is set to loop. We would normally expect the idle animation to loop but our idle ‘animation’ consists of only the one frame so no loop is required.

You may be asking why we are setting the animations loop flag every time we add a frame rather than calling the mutator function. And that’s a very good question. I cannot think why I’ve written the code to set the animations looped flag every time we add a frame rather than just calling the mutator the once. This is a behind the scenes look at the process I follow for these tutorials but if you look in the GitHub repo you’ll notice that it contains the code up to tutorial 45 (and possibly even further by the time you read this) even though I have only completed the write-up for the first 30 tutorials. This happens for a number of reasons but it is usually because I want to finish the code for a topic (such as projectile attacks) before I start the write-up. This has the benefit that my programming flow is not constantly interrupted and I can normally write the code for a new feature in one sitting. Also by distancing myself from my code over time, when I come to do the write-up I an essentially performing my own code review and can hopefully spot most of the mistake I make (although if you notice any issues then please let me know in the comments, as with any large project there are bound to be some bugs that slip through). However, it does have one big downside which is when I come to do the write-up and I notice an error or oddity in my programming, which is exactly what has happened here, it is not as easy to fix as it would have been if I didn’t have the code for the next 15+ tutorials already written. Any change I make to the code base here needs to be replicated in each tutorials folder. One small problem with the replication and that tutorials code won’t compile. However, while these issues may happen every now and then don’t worry they will be fixed in time. In fact, tutorial 46 will contain the changes to how we set the looped flag but feel free to make your own changed if you can’t wait until then.

Anyway back to this weeks tutorial. With those changes made we can now run the game and when we press the ‘e’ key (the key we assigned to shoot a projectile in this tutorial) the animation will stop at the last frame.

Player stuck in last animation frame
Player stuck in last animation frame.
We need some way of transitioning to a different animation when we no longer want to shoot projectiles, which is something we will work on next week.
As always, if you have any suggestions for what you would like covered or are having any trouble implementing a feature, then let me know in the comments and I’ll get back to you as soon as I can. Thank you for reading 🙂