The Last Signal. Post #5: full animation system implementation

In this post, I’m going to go into the details of creating a full-blown animation system for our game, something I had promised to do sometime in the future in the previous post.


How animations work in (2D) video games

In video games, the effect of animation is achieved by very quickly switching between pictures which depict the same character/object/effect etc. at slightly different times. If switched sufficiently quickly, the sequence of frames will give the impression of smooth continuous motion.

In our project, the graphics artists provide us with the following deliverables:

    • a sprite sheet
    • a list with frame coordinates (animation data file)

E.g., the sprite sheet for the astronaut used in our game looks like this:

Astronaut

 

The graphics artists create every frame separately and place them in the same file for easier management. Each frame is contained in a rectangle which can be referred to by specifying its coordinates.

When working on (2D) video games, we use a slightly different coordinate system to the one standard in mathematics: while the x-axis still points to the right, the y-axis points downwards and the origin (0;0) lies in the upper left corner of the screen. This convention is carried over from the age of CRT display technology. Back then, an electron beam would trace horizontal lines left to right, top to bottom; this was how every frame in the screen was drawn. While this technology is dated, the tradition persists.

There are numerous ways of how you can refer to a rectangular area in a sprite sheet. You can, e.g. specify the (x;y) coordinates of every corner (vertex). You can, however, do it like this: you specify the coordinates of the upper left vertex of the rectangle and then the width and the height of the rectangle (4 numbers are needed in total). This method is standard and was also used by our graphics artists.

So in the animation data file (which as I mentioned is also provided) they list coordinates for every frame:

Astronaut_Idle = 0 0 64 96
Astronaut_Long_Move_1 = 65 0 64 96
Astronaut_Long_Move_2 = 130 0 64 96
Astronaut_Long_Move_3 = 0 97 64 96
Astronaut_Moving_1 = 65 97 64 96
Astronaut_Moving_2 = 130 97 64 96
Astronaut_Moving_3 = 195 0 64 96
Astronaut_Shoot_1 = 260 0 64 96
Astronaut_Shoot_2 = 195 97 64 96
Astronaut_Shoot_3 = 325 0 64 96
Astronaut_Shoot_4 = 260 97 64 96

There is one line per frame, 11 lines in total, one for each of the 11 frames in the sprite sheet above. To the left of each frame’s coordinate listing is the “name” of the frame; the name tells us which specific animation (“Idle”, “Shoot”, etc.) the frame belongs to and in which order the frame has to be shown. Different animations can have different number of frames, depending on how much variation you want in that animation (“Idle” has only one whereas “Long Move” has 3).

So let’s take “Long Move” animation as an example. All frames have the same size 64 x 96 pixels so the last two numbers in frame definitions are always the same. The first two give the (x;y) coordinates of the upper left corner and they will apparently differ from frame to frame. So to produce the “Long Move” animation we need to clip three 64 x 96 frames with upper left corner coordinates of (65;0), (130;0) and (0;97), respectively.

astronaut3

The three frames, when arranged in the order defined by the graphics artist, make up the “Long Move” animation.

Notice that these frames do not follow one another in the sprite sheet. I don’t know why the graphics artist placed them like that in the sprite sheet. However, it doesn’t matter anyway, as long as the programmer gets the right coordinates in the animation data file and some way of identifying the first frame, the second frame, and so on. In this case, frame name tells what the number of the frame is; if you have an agreement in your team that the line number identifies the order in which the frame has to be shown, that will work as well (you will still need a way to tell where one animation ends and the next one begins).

If you look at the animation data file above, you will see that in addition to the “Long Move” animation, there are 4 more animations. All of them are managed in exactly the same way. These 5 animations are what our player object is using. Every other animated object type, e.g. enemy, bullet, door, power-up etc. will obviously require a new sprite sheet and a new animation data file.

The job of graphics artists ends here when they provide these files. From the programming point of view, part of the job is to create a system that can automatically create and manage frames from such files and be scalable, i.e. you should be able to add new animations easily. The second part is to actually make the animations show in the game. Let’s begin with the first part.


Structure of Animation Data Files

When creating a system to manage animation data files, I looked at example animation code we got in Game Programming I course for guidance. Animation data file used in that implementation looked like this:

../assets/explosion.png
explosion 16
0.1 0 0 64 64
0.1 64 0 64 64
0.1 128 0 64 64
0.1 192 0 64 64
0.1 0 128 64 64
0.1 64 128 64 64
0.1 128 128 64 64
0.1 192 128 64 64
0.1 0 192 64 64
0.1 64 192 64 64
0.1 128 192 64 64
0.1 192 192 64 64
0.1 0 64 64 64
0.1 64 64 64 64
0.1 128 64 64 64
0.1 192 64 64 64

The first line specifies the path to the sprite sheet. The second line specifies animation name and how many frames the animation has. All following lines list the frame coordinates, one frame per line. The very first number in each line specifies for how long that frame has to be shown.

Since I was going to use an external separately downloadable SFML class that manages sprite animations (Animation.h/.cpp and AnimatedSprite.h/.cpp), I knew I would not need the time parameter. So the 0.1 part wouldn’t be needed for our implementation.

This example implementation in Game Programming I was built around the principle of having only one animation in the data file. But in our case, one object was going to use several animations (like our astronaut object – move, shoot, etc.). So for our implementation, I decided to extend the data file structure so that multiple animations, each with all of its frames, could be defined in a single data file. In a similar fashion, just as explosion 16 defines that “explosion” animation will use 16 frames, I added an extra line right after the path to the sprite sheet which defines how many animations that sprite sheet contains (or alternatively, how many animations that object will use):

../Assets/Astronaut.png
5

And then I carefully list all the animations and their frames following the same method:

../Assets/Astronaut.png
5
Idle 1
0 0 64 96
Move 3
65 97 64 96
130 97 64 96
195 0 64 96
Long 3
65 0 64 96
130 0 64 96
0 97 64 96
ShootIdle 2
260 0 64 96
195 97 64 96
ShootMove 2
325 0 64 96
260 97 64 96

Wrapping up Animation.cpp and AnimatedSprite.cpp classes

A concise description of how Animation.cpp and AnimatedSprite.cpp are used to enable animations in your game can be found in my previous blog post (#4). Here I will give a visual explanation of how these classes interact to make animations work and how I wrapped up these classes to suit my animation implementation described above.

Animations

The diagram above shows the most important aspects of how the classes Animation.cpp and AnimatedSprite.cpp work.

The class Animation represents the animation itself (e.g., “Long Move”). It contains two member variables, std::vector m_frames, which stores the coordinates for all the frames, and sf::Texture* m_texture, which is a pointer to the sprite sheet which the animation uses for its frame graphics. The sprite sheet is set by calling Animation object’s method .setSpriteSheet() and frames are added to the animation by calling the .addFrame() method.

The frames are stored locally in the Animation class so they will always be accessible. The texture, on the other hand, is not stored in the class since Animation only stores a pointer to the texture it uses. This means that the textures must not run out of scope for at least as long as you plan to use your animation.

The class AnimatedSprite represents the object (sprite) that uses the animation (e.g., the astronaut). It contains one member variable, Animation* m_animation, which stores a pointer to the animation the sprite uses. (The animation must also not run out of scope for as long as the object exists.) By calling the methods .play() and .update() once every iteration in the game loop you can achieve frame switching in the game.

Animated sprites are updated once every game loop iteration, so this must happen in the GameState::Update() method. Therefore it seems natural that textures and animations be also created and managed in the GameState class, which ensures they will only expire when animated objects expire, i.e. when GameState is over.

The following scheme shows how the above system was extended to manage multiple animations for a single object:

Animations2

  • Textures has been made to an std::vector to store all animation sprite sheets, not just one. It is a member variable of GameState, which ensures that texture sprite sheets will never go out of scope for as long as GameState exists. NB: this vector contains pointers, but since the textures are dynamically allocated, we only need to worry about pointers’ scope
  • All animations that belong to the same object (e.g., Long Move, Shoot , Idle which belong to the Astronaut) have been grouped to an std::map which assigns a name to every animation.
  • Such an animation map is produced from a single animation data file and stored locally in GameState. The animation map is then copied to the object upon its creation. The object has a member variable of the type std::map which receives its value from the original map in the GameState. The original animation map runs out of scope in GameState but the object retains a full copy during its entire lifetime.
  • When creating every animated object in the GameState, animation data file for that object must first be processed and the corresponding animation map created so that it can be passed to the object’s constructor.

So now I have described the idea of my implementation. It wasn’t that I first worked out all the details of this scheme and only then implemented it. I had spent hours and hours just sitting and thinking what would be the best way to make animations work. This system emerged gradually while writing, throwing away, re-thinking and rewriting the code many times.

The next step is then to write this function, a method in the GameState, that will process animation data files and produce an std::map which can be sent to object constructors.


Animation System: loading and creating the different animations

This function is called GameState::CreateAnimations() for which I got inspiration from the Arkanoid code. The original function was written to work with SDL2; I modified it to make it work with SFML and wrapped all there was in an extra for-loop (lines 22-23 and 44-45) to handle multiple animations in one file.

The function takes one string parameter, the path to the animation data file,  and returns an std::map.

std::map GameState::CreateAnimations(const std::string& filename)
{
	std::ifstream res;
	res.open(filename.c_str());
	if (!res.is_open())
		std::cout << "Failed to load " << filename <loadFromFile(object_path.c_str());
	m_animation_textures.push_back(texture);

	std::mapanimations;

	while (!res.eof())
	{
		int animation_count = 0;

		res >> animation_count;
		for (int j = 0; j > animation_name;
			res >> frame_count;

			for (int i = 0; i > rect.left;
				res >> rect.top;
				res >> rect.width;
				res >> rect.height;

				animation.addFrame(rect);
			}
			animations.insert(std::pair(animation_name, animation));
		}
	}
	res.close();

	return animations;
};

Here is a break-down of what the function does:

  • (line 4) opens animation data file
  • (lines 8-9) reads off path to the raster sprite sheet and locally saves it
  • (lines 11-13) dynamically allocates an sf::Texture, loads the raster image into it and adds the texture to the texture vector. So one sprite sheet is added per function call, for every animated sprite/object
  • (line 15) creates a local animations map which maps animation names to animations used by the object. This is what the function will return when it’s done processing the animation data file. This map will contain all animations for the object and it will be possible to refer to each animation by its name. To this end, another function will have to be written, which selects animation by name
  • (lines 19-21) reads off number of animations and runs the following for-loop as many times, once for each animation

This loop is the original for-loop from the Arkanoid code. For every iteration of the loop (= for every object animation), the one animation is created:

  • (lines 24-25) creates a local Animation type object and sets the sprite sheet the path to which was obtained in lines 8-9 and is the first line in every animation data file
  • (lines 27-31) reads off the name for the current animation and how many frames there are in the animation. Stores this information locally
  • (lines 33-42) reads as many lines from the file as there are frames in the animation. Each time creates a local sf::IntRect, assigns the frame coordinates to it, and adds that frame to the currently processed animation via the mentioned .addFrame() method

Lines 44-45 are executed when all the frames for an animation in the animation data file have been processed. That animation is paired with its name (from lines 27 and 30) and inserted into animations map (line 15). The loop is then repeated for the next animation and so on until all animations have been added to the animations map.

At this point all the information to animate the object has been collected and stored in the animations map. In line 49, the method exits and returns the map. This map, which was locally created in line 15, is destroyed when the function exits but at the time of the exit the receiving end is able to make a full copy of the animations map.

Using the GameState::CreateAnimations() method

Now that we have the method that processes animation data files comes the fun part – to use this function. In our game, the function is used as follows:

std::map player_animations = CreateAnimations("../Assets/PlayerAnim.dat");
m_player = new Player(player_animations, m_x, m_y);

This all happens in the GameState::Enter() method. We call the GameState::CreateAnimations() method with the path to the animation data file we want to process. This method returns an std::map map. This value is assigned to a locally created variable of the same type player_animations. This local variable will go out of scope as soon as GameState::Enter() exits, but prior to that the value of player_animations is copied to the obejct which will use these animations, m_player. If the object’s constructor is configured correctly (i.e., it receives not a reference but a value), the player (astronaut) object will contain a copy of player_animations.

Inside animated object classes …

Let me now describe how animated sprites, whose constructors receive copies of std::map, are coded to support animations.

    • the constructor of such classes receives an std::map
Player::Player(std::map animations, float x, float y)
    • the class has a member variable of the same type, m_animations which is set to the value the constructor receives:
m_animations = animations;

The object can at this point manipulate its animations as required.

    • the class has a private method GetAnimationByName() which takes in an std::string and returns an Animation*. This method is required to be able to pick desired animation from the animations map so that we can do something with that animation, e.g. to show it:
//in the *.h file:
private:
    Animation* GetAnimationByName(const std::string& name);

//in the *.cpp file:
Animation* Player::GetAnimationByName(const std::string& name)
{
	auto itr = m_animations.find(name);
	return &itr->second;
}

NB: since the class has its own copy of animations map whose lifetime is guaranteed for the entire lifetime of the object, there is no need to return the value, i.e. copy the animations map again. That is why we only return a reference.

    • the class has an UpdateAnimation() method which performs animation-specific actions on the object once every iteration of the game loop. These animation-specific actions are required when you use SFML animation classes Animation and AnimatedSprite, which as I mentioned we are using for our game:
void Player::UpdateAnimation()
{
	m_sprite->play(*m_current_animation);

	sf::Time frameTime = m_frameClock.restart();
	m_sprite->update(frameTime); 
};

NB: we are calling this method from the class update method which is in turn called every iteration of the game loop by the GameState::Update() method. Calling directly from GameState’s update unnecessarily litters the GameState code plus we enforce decoupling when GameState doesn’t know what’s happening in the object while the object itself takes care of its animations.
NB: as you can see, every class has some animation-specific member variables, namely Animation* m_current_animation and sf::Clock m_frameClock. m_current_animation is set to the right animation depending on what is happening in the game, e.g. Move, Shoot, etc. and then it is played via the .play() method. m_frameClock has to do with determining when it is time to switch to the next frame but since it is part of the ready-made AnimatedSprite class I’m using, I don’t care much about what exactly it does. It simply has to be there.

    • the class has to use AnimatedSprite instead of SFML’s default sf::Sprite. This doesn’t mean you must remove standard sprite from your classes but you have to have methods in the class that return AnimatedSprite because you will need to draw it in the GameState::Draw() method.Also, it is important to set animation speed (line 2) somewhere in your class, probably best in the object’s constructor since it must happen once yet is not likely to happen again:
	m_sprite = new AnimatedSprite;
	m_sprite->setFrameTime(sf::seconds(0.1));
	m_current_animation = GetAnimationByName("Idle");
	m_sprite->play(*m_current_animation);

As can be seen, we also set and start playing default animation, “Idle” for the player in this case.

Selecting specific animation as m_current_animation

This is the final part of this post: How do you select which animation has to be played when?

There are no recipes or answers to this question that can suit every animated sprite in your game. It depends on user input, e.g. if the player is firing, moving, if an object is damaged or under effects of a power-up. In general, it all depends on the game mechanics.

This was the last part I was working on when I was implementing animations. When everything else had been done, I thought this would be the easy part. It turned out to be easy up to a certain point; beyond that point was perfection which I was trying to achieve, and it was really difficult to achieve.

In the beginning of this post, I mentioned five different animations the astronaut has in our game. I will describe when each of these animations has to be played according to our game mechanics:

  • Idle, when the player is doing nothing (not really an animation, just a single frame)
  • Move, when the player is moving but has not yet attained full speed
  • Long Move, when the player is moving and their speed is equal to or above 95% of the maximum speed
  • Shoot Idle, when the player is not moving and is firing
  • Shoot Move, when the player is moving and is firing

Some animations in the list are more difficult to get right than the others. E.g., it is very easy to toggle between “Idle” and “Move”: you just have to call your InputManager one way or another and determine if the player is moving or not. If yes, set the animation to “Move”, otherwise set it to “Idle”. So you could have something like this:

void Player::SelectAnimation()
{
	if (!moving)
		m_current_animation = GetAnimationByName("Idle");
	else
		m_current_animation = GetAnimationByName("Move");
}

We have included 2 out of 5. “Long Move” is also easy to include because you only have to add an extra check that tests the player’s speed:

void Player::SelectAnimation()
{
	if (!moving)
		m_current_animation = GetAnimationByName("Idle");
	else
		if (m_speed < 95.0f)
			m_current_animation = GetAnimationByName("Move");
		else
			m_current_animation = GetAnimationByName("Long Move");
}

3 down, 2 to go. We have two Shoot animations left. Let’s include them both:

void Player::SelectAnimation()
{
	if (!moving)
		m_current_animation = GetAnimationByName("Idle");
	else
		if (m_speed < 95.0f)
			m_current_animation = GetAnimationByName("Move");
		else
			m_current_animation = GetAnimationByName("Long Move");

	if (shooting)
		if (!moving)
			m_current_animation = GetAnimationByName("Shoot Idle");
		else
			m_current_animation = GetAnimationByName("Shoot Move");
}

Notice that additional moving check has to be added after the shooting check to know which of the two shooting animations to play. We cannot check for shooting separately from moving, since their animations partially cover both. This is a redundancy in the code: we have already checked moving once and are now forced to check moving again. However, this redundancy cannot be easily got rid of.

And yet this code doesn’t work as expected … When I ran this code, the shoot animation (be it “Shoot Move” or “Shoot Idle”) never seemed to play, however many times I would shoot.

The reason is this: The animation is set to e.g. “Shoot Move” in this frame and starts playing. The computer begins processing the next frame immediately and determines that the player is no longer shooting in that next frame. Indeed, if you pressed the fire key and released it, the computer will only register you shooting at that particular frame – that is the desired behaviour. The “Shoot Move” animation is still playing but in the SelectAnimation() method, lines 7-9 animation is changed to either “Move” or “Long Move”. And since you’re no longer shooting, the whole if-block in lines 11-15 is skipped, leaving you with a purely move animation. “Shoot Move” was switched before it got to play out completely.

Indeed, upon carefully inspecting the player sprite, I could see the shoot animation flash for a tiny fraction of a second and then disappear all too soon.


The shoot animation, whether “Shoot Idle” or “Shoot Move”, must be allowed to finish playing. This can be achieved by disabling the “move” block (lines 3-9) for as long as the shoot animation is playing. When the shoot animation is over, we should start checking for move again. That’s the simple idea. But how do you know when the shoot animation is over?

AnimatedSprite class is written in such a way that an animation will keep playing over and over again (it will “loop”) until it is manually stopped or another animation is set. AnimatedSprite class has a method .setLooped() which allows to turn this loop behaviour on and off. By default, every AnimatedSprite object is set to loop its animations. If we disable the “move” block, set the animation to “Shoot Move/Idle” and wait for it to be over to enable the “move” block again, we will never see this happening as “Shoot Move/Idle” is looped (just as “Move” animations). However, if we modify the shoot” block like this:

if (shooting)
{
	if (!moving)
		m_current_animation = GetAnimationByName("Shoot Idle");
	else
		m_current_animation = GetAnimationByName("Shoot Move");
	m_sprite->setLooped(false);
}

the “Shoot” animation will stop after it has played out once. We can then use another method, .isPlaying(), to determine if the current animation is stopped or not. Obviously, if animations are looped, this method will always return true, but if animations are not looped, it will only return true when the current animation is playing.

By adding an if-statement before the “move” block to check if a shoot animation is playing, we can skip the “move” block while a “shoot” animation is playing and run the “move” block otherwise. This if-statement must therefore check two things: 1. if the current animation is playing, and 2. if the current animation is a “shoot” animation. This check can be realized with this if-statement:

if (!(m_sprite->isPlaying() && (m_current_animation == GetAnimationByName("Shoot Idle") || m_current_animation == GetAnimationByName("Shoot Move"))))

Notice the ! in front of the whole statement. It is necessary for the code to work as expected, since we want to run the “move” block only if the current animation is not playing (= is over) and the current animation is not “Shoot Idle” or “Shoot Move” (the ! has been factored out and put in front of the brackets since it applies to both terms inside the brackets).

One final addition is required to make the player aniations work correctly. When the “shoot” block has been run the current animation is set not to loop. If we run the “move” block afterwards and set the cureent animation to a “move” animation, it will still be non-looping, so it will play once and stop. So whenever the “move” block is run and sets the current animation to a “move” animation, it also has to reset the looping behaviour to true.

So the final version of the method which selects the right animation for the player looks like this:

void Player::SelectAnimation()
{
	if (!(m_sprite->isPlaying() && (m_current_animation == GetAnimationByName("ShootIdle") || m_current_animation == GetAnimationByName("ShootMove"))))
	{
		if (!moving)
			m_current_animation = GetAnimationByName("Idle");
		else
			if (m_speed setLooped(true);
	}

	if (shooting)
	{
		if (!moving)
			m_current_animation = GetAnimationByName("ShootIdle");
		else
			m_current_animation = GetAnimationByName("ShootMove");
		m_sprite->setLooped(false);
	}

	m_sprite->play(*m_current_animation);

	sf::Time frameTime = m_frameClock.restart();
	m_sprite->update(frameTime);
}

Can this repeating and possibly redudant code in Player::SelectAnimation() be further simplified without losing functionality? One thing which seems questionable is the long if-statement in line 3. Maybe you could throw away the second half of the statement and only check if the current animation is playing?

I thought about it and tested it, and unfortunately it didn’t work without that second half. The thing is that if you don’t additionally check if the current animation is a “shoot” animation, you will only be able to run the “move” block once. Suppose the current animation is a “shoot” animation, it has played out and is now stopped. In the next frame, the “move” block executes, sets the current animation to a “move” animation and sets looping to true. In the frame that follows, looping is set to true and the current animation is a “move” animation. So the “move” block is not run since it only checks against looping behaviour. If you stop moving, the “move” block is still not run and never sets the animation to “Idle” unless you shoot, which sets looping to false again. So the check for animation type in line 3 is necessary.

Final thoughts

From the code examples above and the explanations that follow you probably got the impression of how difficult animations are in video games. The code in Player::SelectAnimation(), however clumsy and repeating, is the minimal code required to make player animations that we wanted to have in our game work. And we didn’t want all that much – just to combine shooting and thruster effects in a single sprite.

Animations tend to get pretty complex pretty fast. The more aspects you add to a sprite, the more quickly your code that manages those animations grows. Suppose we wanted to include a “damage” animation, when player sprite flashes in red when hit by an enemy bullet. The idea is quite simple – check if the player is receiving damage and play the “damage” animation. If not, play the usual animation of whatever the player is doing (moving or shooting). But that actually means that for every of the 5 animations we have right now, there needs to be a separate “damaged” version. If you have only one, “common” damage animation where the player sprite has red tint and is otherwise “idle”, and you get damaged when moving, the thrusters effect will disappear (while you’re still moving) and reappear after “damage” animation is over. Moving, shooting and getting damaged all overlap and must be explicitly taken care of in the code.

This is one of the most difficult tasks I have worked on in the project. But now I understand how 2D animations work and how you can implement them.

About Rokas Paulauskas

2014  Programming