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.

At the moment our animation system has one animation for moving while on the ground (if we move the other way we play the same animation but flip each frame). This is adequate for a platforming game (most of the time) but what if we want a game with a top-down perspective where the player can move in any direction? Or what if we wanted to play a different animation for when a character is moving right as opposed to left? I’m sure we could think of a way to hack our current system but that’s not ideal and definitely not something we should do in a tutorial series, so this week I’ll be extending the animation system so that it can maintain and play separate animations for four directions: left, down, up, right.

There’s also another reason why I want to implement animation in four directions sooner rather than later and that is to take advantage of the huge number of freely available top-down sprites created for the Liberated Pixel Cup (LPC). The LPC is:“…a two-part competition: make a bunch of awesome free culture licensed artwork, and then program a bunch of free software games that use it.”

Any of the sprites we use at the moment are for test purposes only and will most likely be replaced in the final game however many of the LPC sprites are released under a Creative Commons License that would allow us to use the sprites (even commercially) as long as we attribute the original authors. When I use a sprite I will provide a link to its original source and also provide a license file with the sprites in the project folder.Having the ability to animate in four directions also provides flexibility as we may in future decide to return to our platforming roots and that will be fine, we’ll just use the existing left and right animations. We may even, as an example of the top of my head, want an enemy that can fly around the level in any direction and has different animations for each direction.

Let’s start by updating the FacingDirection enum in the Animation class to include our new directions.

Animation.hpp
enum class FacingDirection
{
None,
Left,
Right,

// Add the two new facing directions below
Up,
Down
};

Along with the existing None, Left, and Right options we add Up and Down. This adds the possibility of the two directions to our game, although we, of course, need to make further changes before we can start using them properly.

Before we implement the directions in the animation system, I want to change how we store the directions. I no longer want to store them in the animation class. Instead, I want to move the responsibility to the animation component. This way our animation class does not need to know anything about directions so if we make any changes to how we handle animations (even by removing directions altogether) we shouldn’t need to make any drastic changes to the Animation class. In the Animation class, remove the FacingDirection member variable, the getter and setter for the variable, and the reference to it in the constructor. The updated Animation header should then look like this.

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();

std::vector<FrameData> frames;
int currentFrameIndex;
float currentFrameTime;
};

Remember to delete the getter and setter in the implementation file and update the constructor so it no longer sets a default direction.

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

// Remove this function
/*
void Animation::SetDirection(FacingDirection dir)
{
if(direction != dir)
{
direction = dir;
for(auto& f : frames)
{
f.x += f.width;
f.width *= -1;
}
}
}
*/

// Remove this function too
/*
FacingDirection Animation::GetDirection() const
{
return direction;
}
*/

As I mentioned earlier the animation component will now store the direction for that animation. We use a map with the FacingDirection as the key and a shared_ptr to the animation as the value.

Animation.hpp
#include <map>

using AnimationList =
std::map<FacingDirection, std::shared_ptr<Animation>>;

class C_Animation : public Component
{
Public:

void AddAnimation(AnimationState state, AnimationList& animationList);

private:
std::map<AnimationState, AnimationList> animations;
std::pair<AnimationState, std::shared_ptr<Animation>> currentAnimation;
FacingDirection currentDirection;

};

We use the new AnimationList in our animations map and the AddAnimation function has also been changed to accept a reference to an AnimationList. With the header complete we’ll move on to the implementation file. Update the constructor to set a default facing direction.

Animation.cpp
C_Animation::C_Animation(Object* owner) 
: Component(owner),
currentAnimation(AnimationState::None, nullptr),
currentDirection(FacingDirection::Down) { }

We’ll default to a facing direction of down for now. AddAnimations implementation hasn’t changed much but now it accepts an AnimationList instead of just an Animation.

C_Animation.cpp
void C_Animation::AddAnimation(AnimationState state, 
AnimationList& animationList)
{
animations.insert(std::make_pair(state, animationList));

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

In SetAnimationState we need to retrieve the animation for the direction the entity is currently facing.

C_Animation.cpp
void C_Animation::SetAnimationState(AnimationState state)
{
if (currentAnimation.first == state)
{
return;
}

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

if(animation != animationList->second.end())
{
currentAnimation.first = animationList->first;
currentAnimation.second = animation->second;
currentAnimation.second->Reset();
}
}
}

One of the biggest changes is with our SetAnimationDirection function. Previously this function could call the ‘SetDirection’ method on our current animation, however, as we have now removed this function, we must first find the current animation in our animation list and then retrieve the animation for the new direction.

C_Animation.cpp
void C_Animation::SetAnimationDirection(FacingDirection dir)
{
if(dir != currentDirection)
{
currentDirection = dir;

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

if(animation != animationList->second.end())
{
currentAnimation.second = animation->second;
currentAnimation.second->Reset();
}
}
}
}

That’s it for changes to our Animation system. It can now accept a different animation for each direction. We’ll continue to use the keyboard movement component to set the entities direction (although we’ll change this in future).

C_KeyboardMovement.cpp
void C_KeyboardMovement::Update(float deltaTime)
{

if(input->IsKeyPressed(Input::Key::Left))
{
xMove = -moveSpeed;
animation->SetAnimationDirection(FacingDirection::Left);
}
else if(input->IsKeyPressed(Input::Key::Right))
{
xMove = moveSpeed;
animation->SetAnimationDirection(FacingDirection::Right);
}

int yMove = 0;
if(input->IsKeyPressed(Input::Key::Up))
{
yMove = -moveSpeed;
animation->SetAnimationDirection(FacingDirection::Up);
}
else if(input->IsKeyPressed(Input::Key::Down))
{
yMove = moveSpeed;
animation->SetAnimationDirection(FacingDirection::Down);
}

}

As well as the existing left and right we now also set our new up and down directions. We can’t run the game now because we first need to make changes to how we create the animations for our player, but even if we could, we would not be able to test our vertical animations because we do not have any sprites for those directions. This is where the LPC sprites come in. I used an LPC character generator to generate a temporary player character. You can find the sprite sheet (Player.png) in the resources folder for this tutorial on the GitHub page.

If we decide to use LPC sprites or any other modular sprites (where the body and clothes can be mix and matched) we’ll create a system where we can change clothes and weapons in-game but for now, the character generator will help us quickly test our new animation directions.

Now we have a new sprite sheet we need to update how we build the animations in SceneGame. If you remember from a previous tutorial we create an animation using individual frames. A frame is a specific rectangular area of the texture.For each action state (just idle and walking for now) we create four separate animations, one for each facing direction. We then add these animations to our animation component along with their action state.

SceneGame.cpp
void SceneGame::OnCreate()
{


auto animation = player->AddComponent<C_Animation>();

int playerTextureID = textureAllocator.Add(workingDir.Get()
+ "Player.png");

const int frameWidth = 64;
const int frameHeight = 64;
const int upYFramePos = 512;
const int leftYFramePos = 576;
const int downYFramePos = 640;
const int rightYFramePos = 704;

/*******************
* Idle Animations *
*******************/
std::map<FacingDirection, std::shared_ptr<Animation>> idleAnimations;

std::shared_ptr<Animation> idleUpAnimation = std::make_shared<Animation>();
idleUpAnimation->AddFrame(playerTextureID, 0, upYFramePos,
frameWidth, frameHeight, 0.f);
idleAnimations.insert(std::make_pair(FacingDirection::Up,
idleUpAnimation));

std::shared_ptr<Animation> idleLeftAnimation =
std::make_shared<Animation>();
idleLeftAnimation->AddFrame(playerTextureID, 0, leftYFramePos,
frameWidth, frameHeight, 0.f);
idleAnimations.insert(std::make_pair(FacingDirection::Left,
idleLeftAnimation));

std::shared_ptr<Animation> idleDownAnimation =
std::make_shared<Animation>();
idleDownAnimation->AddFrame(playerTextureID, 0, downYFramePos,
frameWidth, frameHeight, 0.f);
idleAnimations.insert(std::make_pair(FacingDirection::Down,
idleDownAnimation));

std::shared_ptr<Animation> idleRightAnimation =
std::make_shared<Animation>();
idleRightAnimation->AddFrame(playerTextureID, 0, rightYFramePos,
frameWidth, frameHeight, 0.f);
idleAnimations.insert(std::make_pair(FacingDirection::Right,
idleRightAnimation));

animation->AddAnimation(AnimationState::Idle, idleAnimations);

/**********************
* Walking Animations *
**********************/
const int walkingFrameCount = 9;
const float delayBetweenWalkingFramesSecs = 0.1f;

std::map<FacingDirection, std::shared_ptr<Animation>> walkingAnimations;

std::shared_ptr<Animation> walkUpAnimation =
std::make_shared<Animation>();
for (int i = 0; i < walkingFrameCount; i++)
{
walkUpAnimation->AddFrame(playerTextureID, i * frameWidth,
upYFramePos,
frameWidth, frameHeight,
delayBetweenWalkingFramesSecs);
}
walkingAnimations.insert(std::make_pair(FacingDirection::Up,
walkUpAnimation));

std::shared_ptr<Animation> walkLeftAnimation =
std::make_shared<Animation>();
for (int i = 0; i < walkingFrameCount; i++)
{
walkLeftAnimation->AddFrame(playerTextureID, i * frameWidth,
leftYFramePos,
frameWidth, frameHeight,
delayBetweenWalkingFramesSecs);
}
walkingAnimations.insert(std::make_pair(FacingDirection::Left,
walkLeftAnimation));

std::shared_ptr<Animation> walkDownAnimation =
std::make_shared<Animation>();
for (int i = 0; i < walkingFrameCount; i++)
{
walkDownAnimation->AddFrame(playerTextureID, i * frameWidth,
downYFramePos,
frameWidth, frameHeight,
delayBetweenWalkingFramesSecs);
}
walkingAnimations.insert(std::make_pair(FacingDirection::Down,
walkDownAnimation));

std::shared_ptr<Animation> walkRightAnimation =
std::make_shared<Animation>();
for (int i = 0; i < walkingFrameCount; i++)
{
walkRightAnimation->AddFrame(playerTextureID, i * frameWidth,
rightYFramePos,
frameWidth, frameHeight,
delayBetweenWalkingFramesSecs);
}
walkingAnimations.insert(std::make_pair(FacingDirection::Right,
walkRightAnimation));


animation->AddAnimation(AnimationState::Walk, walkingAnimations);

}

When you run the game you’ll see our new character. You can move him around the scene to see the different animations for each direction.
Animation in four directions

The player has an animation for up, down, left, and right.

You’ve probably noticed that the LPC sprites are quite a bit smaller than the ones we were using previously so I used our recently written camera zoom to have a closer look at the player by using the left bracket key to zoom in (the right bracket key zooms out).

For the next group of tutorials we will use the top-down perspective to help us develop and test new components in our engine. We may not stick with this perspective and we can easily flip back to our platforming roots but for now the huge library or free sprites will prove very useful.While you were testing the animations you may have noticed that the player does not animate when you are moving in two directions (for example up and right), next week we’ll fix this and also look at optimising how we calculate when a new frame is needed.

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 🙂