Project Hikari Development Blog

2D Sprite Animation: Part 1

| Comments

At the heart of any great platformer is good quality animation, and Mega Man games deliver on that front. Due the the technical limitations of the NES the programmers at the time had to get pretty creative with how they represent sprites and animations.

Luckily for us today computer technology has come a long way and we have the luxury of focusing on solving programming tasks without worrying as much about our hardware limitations.

So let’s talk about animations.

Control

Since I wanted fine-grained control over many aspects of the animation I had a few requirements for how I wanted them to be stored and used within the game. Some things I thought about were:

  • Frame dimensions may vary
  • Frame durations may vary
  • Some animations may repeat
  • Some animations may have a “beginning” and a “looping” part

Frames

An animation is really a sequence of images (referred to as “frames” from here on) and some data describing how they are displayed. So let’s define a frame:

  • A frame is simply a rectangle, an origin (or “hotspot”), and a display time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Frame {
private:
    Rectangle2D<int> sourceRect;
    Point2D<int> hotspot;
    float displayTime;

public:
    Frame(
        const Rectangle2D<int> &sourceRect,
        const float &displayTime,
        const Point2D<int> &hotspot = Point2D<int>(0, 0)
    );

    const Rectangle2D<int>& getSourceRectangle() const;
    const Point2D<int>& getHotspot() const;
    const float& getDisplayTime() const;

    void setSourceRectangle(const Rectangle2D<int> &newSourceRect);
    void setHotspot(const Point2D<int> &newHotspot);
    void setDisplayTime(const float &newTime);
};
  • The “source rectangle” is the region of an image or texture to use when displaying this frame.

  • The “hot spot” is an offset from the top-left of the source rectangle that can be used to “move” a frame when displaying it. This is useful if the frames of an animation are not all the same size or the subject is not always in the same relative location within a given frame.

  • The “display time” is how long the frame should be displayed. In Hikari’s case display time is expressed in seconds where 1.0f is one second.

Animations

Now that frames are covered we can focus on the remaining aspects of animations: whether they repeat or not, and if they do, do they start from the beginning or somewhere else?

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
typedef std::vector<Frame> FrameList;

class Animation {
private:
    bool repeat;
    unsigned int keyframe;
    unsigned int syncGroup;
    FrameList frames;

public:
    Animation(
        const FrameList &frames,
        bool doesRepeat = ANIMATION_DEFAULT_REPEAT_SETTING,
        unsigned int keyframe = ANIMATION_BEGINNING_FRAME_INDEX,
        unsigned int syncGroup = ANIMATION_DEFAULT_SYNC_GROUP
    );

    bool doesRepeat() const;
    unsigned int getKeyframeIndex() const;
    unsigned int getNumberOfFrames() const;
    unsigned int getSyncGroup() const;
    const Frame& getFrameAt(unsigned int index) const;

    static const bool ANIMATION_DEFAULT_REPEAT_SETTING;
    static const unsigned int ANIMATION_BEGINNING_FRAME_INDEX;
    static const unsigned int ANIMATION_DEFAULT_SYNC_GROUP;
};

If an animation repeats there is a chance that you may want to play some sort of “introductory” part and then loop over another part indefinitely. This could be handled by using two different animations and playing them in sequence, but then you run into problems having to define the sequences of animations to play and when to play them, etc. It’s a common enough case that I felt baking it into Animation was the right thing to do…

…and that’s what keyframe is for.

An animation’s keyframe is the frame to start playback from when it repeats. If the keyframe is set to 0 then the entire animation will play from the beginning when it repeats. Let’s say, though, that you have a 9-frame animation of Mega Man running. The first 3 frames show him just starting to move, while the remaining 6 frames show him fully running. You can use a single Animation with a keyframe of 2 (since frame indicies are 0-based) and you’re all set. Then animation will play frames 0-8 the first time through, and then 2-8 any subsequent time until the playback is restarted. Cool.

There’s one other thing that’s important to think about: how does one handle transitioning between two animations? Should they always restart from the beginning when you change from one to the other? What about in the case where you have a running and running + shooting animation? The transition between those two should not cause playback to restart — so how do we handle that? That’s where the syncGroup comes in to play.

Sync Groups

The concept of sync groups is simple: animations in the same sync group have identical playback characteristics, animations in different sync groups do not. The sync group is a marker to indicate that there is no need to reset playback when transitioning between animations in the same group. Any time you transition, check if the previous and next animations are in the same sync group. If they are, don’t reset playback. If they aren’t, then start playing from the beginning of the new animation.

Of course, sync groups have their drawbacks. The biggest being that it’s still up to someone to make sure that animations with the same sync group are actually indentical, frame-wise.

Defining animations

Since our data is JSON it’s easy to represent animations in a structured way. Here’s a simple 2-frame, repeating animation:

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
{
    "repeat": true,
    "keyframe": 0,
    "syncGroup": 0,
    "frames": [
        {
            "x" : 0,
            "y" : 0,
            "width" : 16,
            "height" : 16,
            "hotspotX" : 8,
            "hotspotY" : 8,
            "length" : 0.1167
        },
        {
            "x" : 16,
            "y" : 0,
            "width" : 16,
            "height" : 16,
            "hotspotX" : 8,
            "hotspotY" : 8,
            "length" : 0.1167
        }
    ]
}

An animation with many frames written this way can get a little bit lengthy but the definitions are still very readable and I would prefer this over XML.

Next installment we’ll talk more about grouping animations together as well as how we go about actually playing them.

Stay tuned.

Comments