Animated Sprites
Animation is about creating the illusion of life. And you can’t create it if you don’t have one.
– Brad Bird
Animation in most 2D games is based on the Flip Book animation principle, which creates the illusion of motion by playing a series of static two-dimensional images in rapid succession.
In 1872, the former governor of California, Leland Stanford, a businessman and racehorse owner, hired Edward Muybridge, a renowned photographer and scholar, to settle the question, whether all four feet of a horse were off the ground at the same time while trotting.
Muybridge set up a series of cameras along a racetrack, taking pictures in quick succession, and he was thus able to capture the following images:
As we can see, in at least one picture, the horse had all of its four legs off the ground.
About seven years later, Muybridge repeated the experiment and placed each photo onto a Zoopraxiscope to create an animation of a running horse:
To create a smooth looking animation, at least 24 FPS, the traditional frame rate used in films, is required. Obviously, this means that for every single second of animation, 24 individual images are needed.
In this tutorial, we will discover the basics of Sprite Animation and how to use Sprite Sheets to efficiently store the animation images.
Animating Sprites
The standard approach to animated sprites is to create an array of images that represent the different stages of all the animations of a game character or object. For example, a player character in a small RPG, could have a walking and a running animation, each consisting of ten images, resulting in an array of twenty images in total, the first ten defining the walking cycle and the last ten the running cycle.
The Atlas Effect
As briefly mentioned in an early tutorial, Direct2D offers an effect to do just this. From the MSDN:
You can use the atlas effect to output a portion of an image but retain the region outside of the portion for use in subsequent operations. The CLSID for this effect is CLSID_D2D1Atlas. The atlas effect is useful if you want to load a large image made up of many smaller images, such as various frames of a sprite.
To create the effect, we use the ID2D1DeviceContext::CreateEffect method:
REFCLSID effectId
This specifies the class ID of the desired effect to create. Direct2D offers the following standard effects. The ID for the atlas effect is CLSID_D2D1Atlas.
ID2D1Effect **effect
When the method returns, this parameter contains the address of a pointer to the new effect.
To add the effect to an image, the ID2D1Effect::SetInput method is used:
UINT32 index
This unsigned integer specifies the index of the image to set.
ID2D1Image *input
This is a pointer to the input image.
BOOL invalidate = TRUE
This boolean specifies whether to invalidate the graph at the location of the effect input or not.
Okay, so let us try it out.
So far, so easy.
Now, for Direct2D to know which image to load from the sprite sheet, we have to specify the width of each image, and the padding width between images.
Still easy. Now what is left to do is to tell the atlas about the rectangle; this is done using the ID2D1Effect::SetValue function, for which, strangely enough, no MSDN page exists?
Drawing one of the rectangles (can you guess which one from the definition of the input rectangle?) is now straightforward:
As you can see, the example from the MSDN is a bit messy, the padding isn’t right, and thus the image doesn’t really look the way it should.
While the effects are interesting and useful, I feel like the atlas effect isn’t powerful enough to do what we want to do. We will therefore create our own class to work with sprite sheets.
bell0Atlas
We can emulate the behaviour of the atlas effect with a simple structure:
LPWSTR name
This specifies the name of the animation cycle.
unsigned int startFrame
This unsigned integer defines the first frame of the cycle.
unsigned int numberOfFrames
This unsigned specifies the actual number of frames in this cycle.
float width, height
Those two variables define the width and height of each sprite in the cycle.
float rotationCenterX, rotationCenterY
The rotation centre of each sprite in this animation cycle, given in relative coordinates between 0.0f and 1.0f. This is of the outmost importance when we want to apply affine maps such as scaling or rotations to the sprites.
float paddingWidth, paddingHeight
These two variables define the padding width and height between the sprites.
float borderPaddingWidth, borderPaddingHeight
These two variables define the border padding width and height.
The question now is how to store all those images for each stage of the animation. Clearly, loading hundreds of sprites as individual images would consume far too much memory and processing power. This question also is the reason for the many width and height parameters in the above structure; as the standard solution is to create so-called
Sprite Sheets
A sprite sheet, as seen above, is a single image file containing all the sprites/animations for a game character or object. In such a sprite sheet, it is possible to pack the sprites closely together, this means that when the sprite sheet is opened, a bit of work is necessary to reconstruct the correct images in memory, but the file size can be significantly reduced.
Another advantage of sprite sheets is that most GPUs need to have textures loaded into their own memory before drawing them, which means that switching between different images all the time will result in a huge performance drop.
Basically speaking, sprite sheets are made up of two parts, the frames and the cycles. In the above example, the individual photos of the horse would be the frames, and the frames played in rapid succession, in the correct order, would constitute the running cycle.
To create sprite sheets, you can either draw your own images and nicely stash them into a sprite sheet, or you can try to find some great art on websites like OpenGameArt. A popular tool to create sprite sheets from loose images is the texturepacker software.
Our structure plays well with the following sprite sheet layouts:
The sprite sheet has different animation cycles, with the frames of each cycle being in a single row. The following code assumes each image in the same cycle to have the same size and padding.
To use those sprite sheets together with the frame data structure we defined earlier, we create a new class to store animations:
This class simply holds a pointer to a sprite sheet together with the data of each animation cycle in the sprite sheet. The implementation of the constructor is analogical to the constructor of the sprite class, with the addition of the vector being stored.
Animated Sprites
To use the animation data, we create the AnimatedSprite class, which inherits from Sprite. The AnimatedSprite must keep track of its current animation, the current frame in the animation, and the amount of game time the current frame has been displayed. In addition, the animation’s own FPS is stored in a member variable to allow the animation to slow down or speed up, for instance, as a game character speeds up, the running animation would be played more rapidly.
AnimationData* animationData
This member variable simply holds the data and the spritesheet for this sprite.
unsigned int activeAnimation
This member variable is used to define the currently active animation cycle. It is used in the draw method to find the correct row of images to cycle through.
unsigned int activeAnimationFrame
This member variable defines the currently active frame in the active animation cycle. This is used in the update method to figure out which frame to play next.
float animationFPS = 24.0f
This float sets the speed at which the animation is being played. The standard speed for animations, as in movies, is at 24 frames per second. This is used in the update method to figure out which frame to play next.
double frameTime
This double stores the time the currently active frame has been on display. This is used in the update method to figure out which frame to play next.
The constructor is completely trivial, as it only fills the member variables with the given input. The same goes for the destructor, it simply sets the pointer to the AnimationData class to null.
Now the draw method is already a lot more interesting, but still straightforward. Two problems must be solved: we have to compute a destination and a source rectangle; the destination rectangle, as in previous tutorials, specifies where to draw the sprite, and the source rectangle defines the portion of the sprite sheet to draw.
Let us start with the destination rectangle. To facilitate the use of mathematical operations, such as affine maps, like rotation and scaling, on our sprites, we want the rotation centre of each sprite to be at the desired location, i.e. if the sprite should be at screen coordinates (400,300), we want the actual rotation centre to be at (400, 300).
Thankfully we know the coordinates of the rotation centre in relation to the width and height of the sprite, thus computing the coordinates of the upper-left and lower-right corner of the destination rectangle is still easy. Let
The coordinates of the upper-left corner of the destination rectangle,
The coordinates of the lower-right corner of the destination rectangle,
To compute the source rectangle, we have to get the information about the currently active cycle. Assume that we want to draw the
The coordinates of the upper-left corner of the source rectangle
The coordinates of the lower-right corner of the source rectangle
While this might sound complicated, it is actually straightforward, just try it out for yourself and perhaps sketch the situation for yourself.
Now the real fun begins with the update method. This method is quite complex because it can’t be assumed that the animation speed is slower than the game FPS. For example, a game might run at 60 fps, but we want to play a 24fps animation at triple speed, or 72 fps (just imaging running away from a Säbelzinntager (inside joke!)).
Once the animation fps is higher than the game fps, animation frames have to be skipped. The update method has to figure out how many frames to skip ahead. Once a particular frame has been displayed longer than the animation fps, it is necessary to calculate how many frames to jump ahead. Note that this also means that once a cycle comes to an end, it can’t be restarted with the first frame, as maybe we had to jump ahead for multiple frames and have to restart at frame 1 or 2.
I will just throw the update method at you now, and then try to explain it after you have had a chance to read the code:
Remember that the deltaTime is computed in the game loop. The update method now keeps adding the deltaTime to the time the frame has been displayed, the frameTime. Let
If this first condition is fulfilled, we have to compute how many frames to jump ahead. Let
Finally, to wrap correctly at the end of the cycle, we use a simple modulo calculation. Imagine a cycle with 24 images, with the 23-rd frame currently being active. Now if we have to jump by 3 frames, we compute
This wasn’t so bad, was it? At least we got to do some mathematics. Let us see all of our work in action.
For the next demo, I used two cycles from this fantastic wolf sprite:
The first cycle, or row, represents a running wolf. The second cycle, or row, shows an angry wolf in attack position.
Creating an animated sprite is now surprisingly easy:
Most of this is self-explanatory and works just as in the previous tutorial for normal sprites. We just have to tell Direct2D about our sprite sheet, how large each image is, where to find the rotation centre, and how far apart from each other they are, that is all.
Finally, animating and drawing the sprite is equally easy:
This was almost too easy! Run Wolf, Run!
Okay, so far for drawing one animation, but what if someone evil approaches and our wolf companion wants to warn us by barking? We need a way to change animations. Doing that is actually straightforward, we simply set the desired animation as active, activate the first frame of the new cycle and set the time the frame was displayed to 0. This is the job of the changeAnimation method:
We now have a simple implementation of a Sprite system, and it will work well enough for simple games, but it only works for looping animations. It will glitch once we try to transition between animations. To also support transitions, we will need a finite state machine, but that is a topic for a later, more advanced, tutorial.
It is also possible to not only load a sprite sheet, but also a LUA file with all the information about the sprite sheet, allowing for even more different sheet layouts to be used in our application, but that as well is a topic for a later tutorial.
You can download the source code from here.
References
Literature
(in alphabetic order)
- Game Programming Algorithms, by Sanjay Madhav
- Game Programming Patterns, by Robert Nystrom
- Microsoft Developer Network (MSDN)
- Tricks of the Windows Game Programming Gurus, by André LaMothe
- Wikipedia