Animations Tutorial
In this tutorial, I will show you how to use animations with the Thor library. The Thor.Animations module provides flexible and generic classes that help you animate sprites, texts, shapes or user-defined objects.
Animations
In Thor, "animation" is a very wide term. It includes any function that modifies an object depending on a progress value in [0, 1]. Here, 0 denotes the start and 1 the end of the animation. In other words, any functions conforming to the following signature can be considered animations, where Animated
is the type of object being animated:
void (Animated& object, float progress);
Typically, animations modify the color, texture rect, scale, or other graphical properties over time, but there is no restriction. This somehow vague approach allows you to handle not only specific classes such as sf::Sprite
, but any types with the correct requirements. It would even be possible to use Thor.Animations for classes that are not related to rendering at all.
To get more concrete, let's look at a specific animation type. thor::FrameAnimation
changes the texture rectangle over time. This class is a functor, that is, it overloads operator()
in order to be called like a function. It is possible to define different texture rects, which are applied one after another, by using the addFrame()
method.
sf::IntRect rect1(...); sf::IntRect rect2(...); thor::FrameAnimation animation; animation.addFrame(1.f, rect1); animation.addFrame(3.f, rect2);
Apart from the texture rect, a relative duration is passed that specifies how long (in relation to others) a specific frame is displayed. The above code shows rect1
for the 1st quarter and rect2
for the 2nd, 3rd and 4th quarter of the whole animation. Note that 1.f
and 3.f
are not absolute durations like seconds; absolute durations are always expressed in terms of sf::Time
.
Now we already have an animation that can be applied to drawable objects, for example sf::Sprite
. The thor::FrameAnimation
in particular requires the existence of a setTextureRect()
member function. Since the provided operator()
is a function template, any class meeting these requirements can be animated. The following code shows how to apply the animation to a sprite for a given progress in [0,1]. The progress can be updated continuously, e.g. once in your game loop.
sf::Sprite sprite;
float progress = 0.f;
animation(sprite, progress);
Animators
Recomputing the progress from a frame time is tedious and doesn't offer a lot of flexibility. What if you have a soldier sprite with several animations such as stand, walk and attack? It would be nice to switch between them at any time and to simply play an animation for a given time, without taking care of the progress.
This is where thor::Animator
comes into play. This class template provides an interface to register multiple animations and to associate them with identifiers such as strings or enums. Assume we have the animationsstand
, walk
and attack
which are correctly initialized; we can then create an animator for sf::Sprite
with std::string
identifiers (these are the two template arguments, respectively). The addAnimation()
method registers an animation for a given identifier and duration. This time, the duration is absolute. In our example, all animations shall last for one second.
thor::FrameAnimation stand, attack, walk; thor::Animator<sf::Sprite, std::string> animator; animator.addAnimation("walk", walk, sf::seconds(1.f)); animator.addAnimation("stand", stand, sf::seconds(1.f)); animator.addAnimation("attack", attack, sf::seconds(1.f));
Now the register step is complete. The thor::Animator
class template can store multiple animations and play one of them.
The playAnimation()
function starts playing an animation, the animator will take care of it for the duration specified in addAnimation()
, which is one second in this case.
animator.playAnimation("walk");
It is also possible to stop a playing animation immediately. When an animation is stopped or has finished, the object remains in the last animated state.
animator.stopAnimation();
In the game loop, we continuously call the update()
method with the elapsed frame time. Internally, the animator takes care of computing the progress for the current animation. The animate()
method then applies the animation to one or more objects, in this example a sprite.
sf::Clock clock;
sf::Sprite sprite;
for (;;)
{
...
animator.update(clock.restart());
animator.animate(sprite);
}
To check whether an animation is playing, you can use the method isPlayingAnimation()
. If an animation is playing — and only in this case — you can get its ID using getPlayingAnimation()
. Be careful, calling this function without a playing animation yields undefined behavior.
if (animator.isPlayingAnimation()) std::cout << "Currently playing: " << animator.getPlayingAnimation() << ".\n"; else std::cout << "Currently stopped.\n";
Customization [advanced]
Thor ships a few common animations, but it is also possible to create your own one. Your animation function simply has to be convertible to std::function<void (Animated&, float)>
, where Animated
is the type of object to animate and the float
denotes a [0,1] progress.
As an example, let's define an animation that rotates a sprite by 360°.
void rotate(sf::Transformable& animated, float progress) {
animated.setRotation(360.f * progress);
} thor::Animator<sf::Sprite, std::string> animator; animator.addAnimation("rotate", &rotate, sf::seconds(1.f));
By using a function template rotate<Animated>()
, it is possible to generalize the animation even further and support any type that provides a setRotation()
method.
That's it! You can of course build arbitrarily complex animations. Where appropriate, you may want to use functors and lambda expressions.