In this tutorial we will start to bring our little Viking to life by working on animation. We want to be able to, at the very least, have a walking and idle animation (although we will soon add attack, jumping, falling etc.). We also want to be able to easily and efficiently switch between the animations based on the players current state.

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.

In this tutorial we will start to bring our little Viking to life by working on animation. We want to be able to, at the very least, have a walking and idle animation (although we will soon add attack, jumping, falling etc.). We also want to be able to easily and efficiently switch between the animations based on the players current state.

Before we can animate our Viking we will need the vikings animation sprites. You can download the individual images from here. Individual images are not ideal (more on this shortly) so we will want to combine our sprites into a single sprite sheet. I accomplished this using TexturePacker.

A sprite sheet in TexturePacker.

A sprite sheet is simply a collection of smaller images. The images are placed within the sprite sheet so that we can easily retrieve a single image (more on this when we build our animation class). The main benefit of having one texture for multiple images is that it helps to increase rendering performance. It allows our renderer to combine what would have been multiple draw calls into one. They can also help us with our animations, rather than each animation maintaining lists to many different textures on disk, we only need to store the position of the sprite within the sprite sheet for that frame. We’ll see an example of this shortly, but for now, let’s get started by creating an Animation class.

Animation.hpp
#ifndef Animation_hpp
#define Animation_hpp

#include <vector>

struct FrameData
{
int id; // Texture id (retrieved from our texture allocator).
int x; // x position of sprite in the texture.
int y; // y position of sprite in the texture.
int width; // Width of sprite.
int height; // Height of sprite.
float displayTimeSeconds; // How long to display the frame.
};

#endif /* Animation_hpp */

We start with an enum to store the data for each frame within an animation. This store’s everything we currently need to know about an animation frame and allows us to quickly iterate over different sprites within a sprite sheet to create our desired animations.

Most of the information we need for a frame of animation.

We need an animation class to act as a collection of frames. We’ll start with the minimum functionality that we require and build on it in future tutorials.

Animation.hpp
class Animation
{
public:
Animation();

void AddFrame(int textureID, int x, int y,
int width, int height, float frameTime);

const FrameData* GetCurrentFrame() const;

bool UpdateFrame(float deltaTime);

void Reset();

private:
void IncrementFrame();

// Stores all frames for our animation.
std::vector<FrameData> frames;

// Current frame.
int currentFrameIndex;

// We use this to decide when to transition to the next frame.
float currentFrameTime;
};

AddFrame allows us to add a frame to the animation. Its important to note that animation frames are played in the order they are added.

Animation.cpp
#include "Animation.hpp"

Animation::Animation() : frames(0), currentFrameIndex(0),
currentFrameTime(0.f)
{

}

void Animation::AddFrame(int textureID, int x, int y,
int width, int height, float frameTime)
{
FrameData data;
data.id = textureID;
data.x = x;
data.y = y;
data.width = width;
data.height = height;
data.displayTimeSeconds = frameTime;

frames.push_back(data);
}

GetCurrentFrame simply returns the data for the current frame. We will use this soon to draw our animated sprites.

Animation.cpp
const FrameData* Animation::GetCurrentFrame() const
{
if(frames.size() > 0)
{
return &frames[currentFrameIndex];
}

return nullptr;
}

UpdateFrame checks if it is time to transition to the next frame. It returns true if the frame has changed. When a frames time is up it calls IncrementFrame, which increments the frame index by 1. If we’ve reached the end of the animation the frame index is looped to the beginning. In future, we will want to be able to define if an animation should play in a loop or play once and then stop or revert back to a previous animation.

Animation.cpp
bool Animation::UpdateFrame(float deltaTime)
{
if(frames.size() > 0)
{
currentFrameTime += deltaTime;

if(currentFrameTime >=
frames[currentFrameIndex].displayTimeSeconds)
{
currentFrameTime = 0.f;
IncrementFrame();
return true;
}
}

return false;
}

void Animation::IncrementFrame()
{
// For more information on how this works see here:
//https://www.cprogramming.com/tips/tip/increment-and-decrement-counters-with-rollover
currentFrameIndex = (currentFrameIndex + 1) % frames.size();
}

Reset, as you have probably guessed, resets the animation to its initial state. This will be used to start playing from the first frame whenever we transition to an animation. For example, if we move from a walking to an idle state, we want to reset the idle state so it plays from the beginning frame rather than the frame it was on when we transitioned out of the animation.

Animation.cpp
void Animation::Reset()
{
currentFrameIndex = 0;
currentFrameTime = 0.f;
}

Now we have the basic structure for an animation we need a way of adding an animation to an object. We’ll accomplish this by creating an animation component, called C_Animation.

C_Animation.hpp
#ifndef C_Animation_hpp
#define C_Animation_hpp

#include "Component.hpp"
#include "Animation.hpp"
#include "C_Sprite.hpp"

enum class AnimationState
{
None,
Idle,
Walk
};
#endif /* C_Animation_hpp */

We create an enum to store our objects Animation States. Currently we only want our player to walk and stand idle. We’ll add attack, jump, and a few other animations in a future tutorial and we may also want a way to load in states procedurally rather than hardcoded but for now this provides a simple method of switching between states.

C_Animation.hpp
class C_Animation : public Component
{
public:
C_Animation(Object* owner);

void Awake() override;

void Update(float deltaTime) override;

// Add animation to object. We need its state as well
// so that we can switch to it.
void AddAnimation(AnimationState state,
std::shared_ptr<Animation> animation);

// Set current animation states.
void SetAnimationState(AnimationState state);

// Returns current animation state.
const AnimationState& GetAnimationState() const;

private:
std::shared_ptr<C_Sprite> sprite; // 1
std::map<AnimationState, std::shared_ptr<Animation>> animations;

// We store a reference to the current animation so we
// can quickly update and draw it.
std::pair<AnimationState,
std::shared_ptr<Animation>> currentAnimation;
};

1. Our animation component requires that an object has a sprite component. We’ll draw the animation frame using this component. We’ll need to add a few helper functions to the sprite component to enable us to do this, which we will do shortly.

We want to override the Awake method to retrieve a pointer to the objects sprite component. We also need to override the Update method to update our current animation. If the current animation is incremented we need to retrieve the new frame data and update our sprite with the new texture rect. The Update method will be responsible for setting our sprite based on our current animation frame.

C_Animation.cpp
#include "C_Animation.hpp"
#include "Object.hpp"

C_Animation::C_Animation(Object* owner) : Component(owner),
currentAnimation(AnimationState::None, nullptr)
{

}

void C_Animation::Awake()
{
sprite = owner->GetComponent<C_Sprite>();
}

void C_Animation::Update(float deltaTime)
{
if(currentAnimation.first != AnimationState::None)
{
bool newFrame = currentAnimation.second->UpdateFrame(deltaTime);

if(newFrame)
{
FrameData& data = currentAnimation.second->GetCurrentFrame();

sprite->Load(data.id); // 1

// We haven’t created this method yet but we’ll do that shortly.
sprite->SetTextureRect(data.x, data.y, data.width, data.height);
}
}
}

1. We need to ensure that we have the right texture loaded. Ideally however each sprite in an animation will be part of the same sprite sheet.

Adding an animation is a straightforward process. We insert the pair (state and animation) into our collection. If we do not have a current animation set we will also set this.

C_Animation.cpp
void C_Animation::AddAnimation(
AnimationState state, std::shared_ptr<Animation> animation)
{
auto inserted = animations.insert(std::make_pair(state, animation));

if (currentAnimation.first == AnimationState::None)
{
SetAnimationState(state);
}
}

There are three main steps to setting or changing an animation state:

  1. Check the state we are trying to change to is not already the current state. If it is then we return and do nothing more.
  2. Attempt to find the animation state in our collection. This makes sure we actually have the animation we want to transition to.
  3. If we do find the animation, we then set it as our current state and reset the animation (so it plays from the beginning). This animation will then be updated and displayed in the next call to Update.

C_Animation.cpp
void C_Animation::SetAnimationState(AnimationState state)
{
// We only set an animation of it is not already
// our current animation.
if (currentAnimation.first == state)
{
return;
}

auto animation = animations.find(state);
if (animation != animations.end())
{
currentAnimation.first = animation->first;
currentAnimation.second = animation->second;

currentAnimation.second->Reset();
}
}

const AnimationState& C_Animation::GetAnimationState() const
{
// Returns out current animation state. We can use this
// to compare the objects current state to a desired state.
return currentAnimation.first;
}

You may have noticed that our animation component calls a SetTextureRect method in our sprite component that we have not yet created so let’s do that now. We’ll also make a small change to how we load our sprites. We want to limit the number of sprite texture changes by ensuring that do we do not set a texture unless it is different from our current texture.

C_Sprite.hpp
class C_Sprite : public Component
{
public:

void SetTextureRect(int x, int y, int width, int height);
void SetTextureRect(const sf::IntRect& rect);

private:

int currentTextureID; // We’ll now keep track of our currently set texture.

};

C_Sprite.cpp
C_Sprite::C_Sprite(Object* owner) : 
Component(owner),
currentTextureID(-1) // Set current texture id to -1
{}

void C_Sprite::Load(int id)
{
// Check its not already our current texture.
if(id >= 0 && id != currentTextureID)
{
currentTextureID = id;
std::shared_ptr<sf::Texture> texture = allocator->Get(id);
sprite.setTexture(*texture);
}
}

void C_Sprite::Load(const std::string& filePath)
{
if(allocator)
{
int textureID = allocator->Add(filePath);

// Check its not already our current texture.
if(textureID >= 0 && textureID != currentTextureID)
{
currentTextureID = textureID;
std::shared_ptr<sf::Texture> texture = allocator->Get(textureID);
sprite.setTexture(*texture);
}
}
}


void C_Sprite::SetTextureRect(int x, int y, int width, int height)
{
sprite.setTextureRect(sf::IntRect(x, y, width, height));
}

void C_Sprite::SetTextureRect(const sf::IntRect& rect)
{
sprite.setTextureRect(rect);
}

Now we have our animation collection and component we can finally add it to our Viking object. We’ll do this in the OnCreate method in SceneGame.

SceneGame.hpp
#include “C_Animation.hpp” // Make sure you include our new animation component.

SceneGame.cpp
void SceneGame::OnCreate()
{
std::shared_ptr<Object> player = std::make_shared<Object>();

auto sprite = player->AddComponent<C_Sprite>();
sprite->SetTextureAllocator(&textureAllocator);
// We’ve removed the line to set the viking sprite here.
// The animation component will now be responsible for
// updating the sprite.

auto movement = player->AddComponent<C_KeyboardMovement>();
movement->SetInput(&input);

// Add our new animation component:
auto animation = player->AddComponent<C_Animation>();

int vikingTextureID = textureAllocator.Add(workingDir.Get() + "Viking.png");

const int frameWidth = 165; //1
const int frameHeight = 145;

std::shared_ptr<Animation> idleAnimation = std::make_shared<Animation>();//2

// How long we want to show each frame.
const float idleAnimFrameSeconds = 0.2f;

idleAnimation->AddFrame(vikingTextureID, 600, 0,
frameWidth, frameHeight, idleAnimFrameSeconds);//3
idleAnimation->AddFrame(vikingTextureID, 800, 0,
frameWidth, frameHeight, idleAnimFrameSeconds);
idleAnimation->AddFrame(vikingTextureID, 0, 145,
frameWidth, frameHeight, idleAnimFrameSeconds);
idleAnimation->AddFrame(vikingTextureID, 200, 145,
frameWidth, frameHeight, idleAnimFrameSeconds);

// This adds the idle animation that we’ve just built.
// It will also set it as our active animation.
animation->AddAnimation(AnimationState::Idle, idleAnimation);

objects.Add(player);
}

1. We manually set our sprites width and height for now. In future, we will generate some form of data file (XML, JSON etc.) to do this for us.

2. We’ll start by creating an idle animation (we’ll create the walking animation next week).

3. Our idle animation has four frames so we need to call AddFrame four times. Each frame has the same width and height so I only need to work out there x and y position, which is the top left point for each sprite in the sprite sheet. Luckily for me, TexturePacker can generate a list of the sprites x and y positions. If you look in the resources folder for this tutorial you’ll find Viking.json. We don’t currently parse this file in our game (I have manually copied and pasted the sprite positions) but that’s definitely something we can add in future.

If you run the game our viking will now be animated. Goodbye static sprites!

You’ll have to trust me that the Viking is doing its idle animation.

In next weeks tutorial, we’ll add a walking animation and I’ll show you how we can switch between them depending on the characters current state. This has been quite a long tutorial so if you’ve stuck with it, well done! I hope it proves useful.

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 🙂

Have 60+ UFOs onscreen that have taught themselves to avoid each other and the sides of their environment.