Project Hikari Development Blog

2D Sprite Animation: Part 3

| Comments

So far we’ve covered what animations are and how to organize them into logical groupings, now we’ll discuss how to go about applying an animation to a sprite to achieve playback.

Reusability

One important thing I want to achieve in building Hikari as a data-driven game is reusability — being able to define an object once and use it many times without the overhead of duplication.

Think about this: you have 10 enemies on the screen all of the same type. All 10 of the enemies will be playing the same animation, the only difference being that each enemy may be playing a different part of the animation at any point in time. It doesn’t make sense to have 10 copies of the animation when really only a single animation is being played.

Solving this problem is easy: separate the animation data from the playback mechanism. Then you can:

  • Share instances of Animation objects
  • Play different parts of the same animation without duplicating it

We’ve already defined our Animation class which solves the animation data part of the problem. So let’s create an Animator:

(Animator.hpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <memory>
#include <functional>

class Animation;

class Animator {
private:
    bool paused;
    float timeElapsed;
    unsigned int currentFrameIndex;
    std::shared_ptr<Animation> animation;

    void play(float delta);
    void handleEndOfAnimation();
    float getCurrentFrameDuration() const;

protected:
    float getTimeElapsed() const;
    unsigned int getCurrentFrameIndex() const;
    std::shared_ptr<Animation> getAnimation() const;

public:
    Animator();
    virtual ~Animator();

    /**
     * Starts playback from the beginning.
     */
    void rewind();

    /**
     * Pauses any currently playing animation. Calls to Animator::update will
     * still work but playback will not advance.
     *
     * @see Animator::isPaused
     * @see Animator::unpause
     */
    void pause();


    const bool isPaused() const;

    /**
     * Resumes animation playback.
     *
     * @see Animator::pause
     * @see Animator::isPaused
     */
    void unpause();

    /**
     * Updates animation playback by a specified amount of time.
     * 
     * @param delta time to advance playback, in seconds
     */
    virtual void update(float delta);

    /**
     * Sets the animation to play.
     */
    void setAnimation(std::shared_ptr<Animation> animation);
};

Notice that Animator has virtual functions; it is a base class. The reason? It’s possible that we could “apply” an animation to something in a different way but the playback mechanism will remain the same. In this way we can subclass Animator and adapt playback for different kinds of things.

Also notice that there is a private “playing” method: void play(float delta). This is where the core playback functionality is at, and that logic should work the same way for all types of Animators. Keep it secret, keep it safe.

Let’s say that we want to be able to reuse Animation objects between game objects and elements of our GUI. It’s easy to do this by subclassing Animator into more specialized classes: SpriteAnimator and GUIIconAnimator.

For sprites all we need to do is change what frame it’s displaying as the animation plays. We do this by changing it’s “source rectangle”, or which area of its source image is displays when rendered.

(SpriteAnimator.cpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
SpriteAnimator::SpriteAnimator(sf::Sprite &sprite)
    : Animator()
    , sprite(sprite)
    , sourceRectangle(sprite.getTextureRect()) {

}

void SpriteAnimator::update(float delta) {
    Animator::update(delta);

    if(const auto& animation = getAnimation()) {
        const auto& currentFrame = animation->getFrameAt(getCurrentFrameIndex());
        const auto& currentFrameRectangle = currentFrame.getSourceRectangle();

        sourceRectangle.top = currentFrameRectangle.getTop();
        sourceRectangle.width = currentFrameRectangle.getWidth();
        sourceRectangle.height = currentFrameRectangle.getHeight();
        sourceRectangle.left = currentFrameRectangle.getLeft();

        sprite.setTextureRect(sourceRectangle);

        // Respect the animation frame's "hot spot"
        sprite.setOrigin(
            static_cast<float>(currentFrame.getHotspot().getX()),
            static_cast<float>(currentFrame.getHotspot().getY())
        );
    }
}

As you can see, this kind of separation makes using Animation objects in different ways easy. You can imagine how GUIIconAnimator might work, assuming a gui::Icon can have it’s “source rectangle” changed. We use this design for animation playback in Hikari – and animations are shared by game objects and GUI elements.

If you’re ever building an animation system, consider decoupling the animation data from the playback mechanism.

Comments