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.

This week we’re going back to our Animation system but not to fix bugs, instead, we’ll be extending it to add frame actions. A frame action is a function that is called during a specific frame in an animation. For example, as part of a death animation, we may want a function to be called on the last frame that removes the dead character and performs any necessary cleanup. If you’ve used the Unity game engine before then you may have created Animation Events, which provide a similar feature to the frame actions that we’ll be writing today.

Unity Animation Events.
Animation Events in Unity.

Hopefully the what and why of action frames will become clearer as we write the code. Talking of code the first thing we need is a method of storing the functions and a way of knowing to which frame these functions belong. To accomplish this we’ll use a map with an integer as its key (representing the frame to run actions on) and a vector of actions as the value. We’ll add this to the Animation class.

Animation.hpp
#include <functional>
#include <map>



using AnimationAction = std::function<void(void)>;

class Animation
{


private:

std::map<int, std::vector<AnimationAction>> actions;
};

I’ve created an alias for an std::function. If you haven’t used them before an std::function is a “general purpose polymorphic function wrapper”. It is a class that can wrap a callable element (in our case a function). We’ll use it to store functions for an animations frame to be invoked during the correct frame. How we invoke these functions will be shown shortly, it is very similar to calling a typical function. How to create and bind the functions is a topic for another week, when we create our first frame action, but it is not difficult once you understand the syntax.

Before deciding on how to store our frame actions I looked into the efficiency of std::functions as I assumed there would be an overhead in calling them, which is the case. However, the overhead is much smaller than I first assumed. In fact, it is the same as a virtual function call. This means something very interesting for how our code can be structured. We could remove the hierarchy prevalent in our Object and Component code and move to something more akin to what is shown here. This is definitely something I will look into for a future tutorial but for now let’s get back to implementing action frames.Now we have the structure to store actions we need a method of adding an action to the collection. We’ll create a new public function to do just that.

Animation.hpp
class Animation
{
public:

void AddFrameAction(unsigned int frame, AnimationAction action);

};
Animation.cpp
void Animation::AddFrameAction(unsigned int frame, AnimationAction action)
{
// If the frame is larger than the number of animation frames
// then this request is ignored.
if (frame < frames.size())
{
auto actionKey = actions.find(frame);

if (actionKey == actions.end())
{
// If there is not an existing entry for this frame
// we create one.
actions.insert(
std::make_pair(frame, 
std::vector<AnimationAction>{action})
);
}
else
{
// An existing entry was found so we
// add the action to the vector
actionKey->second.emplace_back(action);
}
}
}

To add a new action we need pass an unsigned int that represents the frame on which the action will be run and of course the action itself. Using this function we can assign any number of functions to be run each frame.With that done the only thing left to write is the code that will run the actions on the correct frame, again we’ll create a new function to do this.

Animation.hpp
class Animation
{

private:
void RunActionForCurrentFrame();

};
Animation.cpp
void Animation::RunActionForCurrentFrame()
{
if (actions.size() > 0)
{
//TODO: can use bitmask as quick way of checking
//if frame has action.
auto actionKey = actions.find(currentFrameIndex);

if (actionKey != actions.end())
{
for (auto f : actionKey->second)
{
f();
}
}
}
}

This checks if an entry exists for this frame and then loops through the vector invoking the function(s). As this function will be called every animation frame for every animation running on every entity we need it to be quick, that’s where the ‘todo’ comes in. We can increase the speed of these checks by using a bit mask to query if a frame has an action. We’ll implement this shortly but before we do let’s call the new function. We want to check for actions every time we advance a frame. We’ll do this in UpdateFrame. 

Animation.cpp
bool Animation::UpdateFrame(float deltaTime)
{
if(releaseFirstFrame)
{
// To be called when we are releasing the
// first frame of an animation.
RunActionForCurrentFrame();
releaseFirstFrame = false;
return true;
}

if(frames.size() > 1)
{
currentFrameTime += deltaTime;

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

// Also called whenever we increment
// the frame of an animation.
RunActionForCurrentFrame();
return true;
}
}

return false;
}

Because of the way we release frames there are two points where we have to call the new function. Once when we release the first frame and then for every subsequent frame increment.In theory that should be everything we need to add and run frame actions (we can write the code to remove actions when it is required). However, before we finish for the week I would like to go back to that todo we wrote. I know that it won’t take long to implement a bit mask as a method of querying if a frame has an action, which should provide a decent speed increase, so I would like to do that now.

Add a Bitmask to our Animation class. We wrote the Bitmask all the way back in the fourth tutorial.

Animation.hpp
#include “Bitmask.hpp"



class Animation
{


private:

Bitmask framesWithActions;

};

As you may remember the Bitmask class will allow us to set and query a bit at a specified position. We’ll use the frames number as the bit position in AddFrameAction.

Animation.cpp
void Animation::AddFrameAction(unsigned int frame, 
AnimationAction action)
{
if (frame < frames.size())
{
auto actionKey = actions.find(frame);

if (actionKey == actions.end())
{
// We set the bit at the frame position whenever
// we first add an action at that position.
framesWithActions.SetBit(frame);
actions.insert(std::make_pair(frame,
std::vector<AnimationAction>{action})
);
}
else
{
actionKey->second.emplace_back(action);
}
}
}

Whenever we first add an action to a frame we set that frames bit position to 1. This means that we come to check if a frame has an action we can simply query the bit mask if the bit at the position of the frame is set to 1 then we know that it has at least one action (when adding subsequent action we do not adjust the bit mask).

We’ll query the bit mask in the RunActionForCurrentFrame function. 

Animation.cpp
void Animation::RunActionForCurrentFrame()
{
if (actions.size() > 0)
{
if(framesWithActions.GetBit(currentFrameIndex))
{
auto actionsToRun = actions.at(currentFrameIndex);

for (auto f : actionsToRun)
{
f();
}
}
}
}

If the bit is set at that position we assume that there is at least one action to be run so we retrieve the action(s) without any further checks. And that’s it for frame actions, we should have the ability to run an action on specific frames. I say should because we have yet to test it. But don’t worry we will over the next few weeks as we work on the players projectile attack.

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 🙂