Your guide to learning game development
In the last chapter we added turrets that constantly fire bullets towards the player character to make collecting treasure a bit more interesting (and challenging). In this chapter we're going to focus on improving the presentation of the game rather than the gameplay. More specifically, we want to add support for basic animations. In this chapter we're going to animate the bullets fired, make the player character face the direction they're moving, display hearts to keep track of the player character's health points, and make it visually obvious if the treasure is rare. All this will be accomplished using the same technique.
Before we continue, let's take a little time to discuss the concept of animation. In filmography the illusion of smooth motion is achieved by showing the viewer a series of still images in rapid enough succession to fool the human eye. These still images that compose the animation are called "frames". You may remember how in previous chapters we referred to each iteration of a game's main loop as a "frame" as well. The two are not all that dissimilar; much like the frame of a game, each animation frame in filmography is presented to the viewer at a fixed rate which should be maintained so as not to undermine the impression of fluid motion.
In an earlier chapter we discussed how Entities in 2D games are represented by sprites. Many sprites will typically be packed into a single image file. It's normal for every animation frame of a given Entity to be packed into one file, but there are times where it makes sense to pack all the sprites for several different types of Entities together. Packing sprites together this way is especially important when rendering a very large number of Entities and switching textures has too large of a performance penalty. To solve a case like that would require the use of a lower-level rendering API than the one provided by SDL2 (such as OpenGL or Direct3d). Not to worry though; none of the projects in this series will need to render enough Entities to cause a problem of this kind.
The animation frames for each Entity in this game are stored in their own image files. To animate an Entity we begin by choosing the first animation frame. We then sample from the portion of the texture containing this frame and render that to the screen. After a certain amount of time passes, the next frame in the animation is sampled. This process continues until the animation ends or, if the animation is intended to loop from the beginning, the process will continue indefinitely.
Now that we've examined the theory behind animation let's get started with the implementation. To support animated Entities we need to add two new members to the Entity type:
uint animationFrame;
float animationTimer;
The old function we wrote to render Entities wasn't made with animations in mind. To support this change in needs will requires a bit of a re-write. Delete the old renderEntity function and replace it with this new and improved version:
void renderSpriteFrame(SDL_Renderer* renderer, SDL_Texture* texture, Vect2 topLeft, int spriteFrameWidth, int spriteFrameHeight, int animationFrame)
{
SDL_Rect source = void;
source.w = spriteFrameWidth;
source.h = spriteFrameHeight;
source.x = spriteFrameWidth * animationFrame;
source.y = 0;
SDL_Rect dest = void;
dest.x = cast(int)topLeft.x;
dest.y = cast(int)topLeft.y;
dest.w = spriteFrameWidth;
dest.h = spriteFrameHeight;
SDL_RenderCopy(renderer, texture, &source, &dest);
}
There's a few differences between this new code and the old renderEntity function. The most immediately obvious is that we're no longer passing an Entity to the function. We instead pass in the position we want the sprite to be rendered (the topLeft parameter). We're also passing in a new parameter called animationFrame. As the name implies, the value of this parameter represents the index of an animation in a texture. In this project it's quite simple to determine exactly where in a texture an animation frame resides. This is due to how we've packed the sprites inside their .png files; animation frames are evenly spaced from each other. This makes accessing each frame trivial, much like accessing the cells in a grid.
To better visualize how this works, let's consider a concrete example. There are six animation frames in the bullet.png file. Each frame is 24x24 pixels. The left edge of the first frame is 0 pixels in, the second 24 pixels in, the third 48 pixels in, and so on. As you can see, due to our grid-like sprite layout, the left edge of an animation frame can be found by simply multiplying the frame number by the width of the frame. This is exactly what we're doing in the above code when calculating the source rectangle.
Now let's start by setting the animation index for the player character. The player character only has two sprite frames; one facing right and the other facing left. What we want is to face the player character in the direction of its horizontal movement. To do so, add this underneath the code where we apply the acceleration to the player character's position:
if (playerAccel.x > 0.0f)
{
s.player.animationFrame = 0;
}
else if (playerAccel.x < 0.0f)
{
s.player.animationFrame = 1;
}
This isn't really what one would expect to call animation. That's merely due to the fact that I didn't make a walking animation for the player character. The bullet's in the game, however, are meant to animate. In fact, they look a lot more interesting when they are animated. To see what I mean, let's go ahead and set to work. Add the following constant which will determine how long a bullet's animation frame should be displayed before changing to the next:
enum bulletAnimationTime = 0.05f;
Now add this to the fireBullet function right before the break statement to initialize the newly spawned bullet's animation data:
bullet.animationTimer = 0.0f;
bullet.animationFrame = 0;
With that done, we need to update the animationFrame of the bullet as it's being simulated. The following should go just below the code where we integrate the position of the bullet:
bullet.animationTimer += dt;
if(bullet.animationTimer >= bulletAnimationTime)
{
enum bulletMaxAnimationFrames = 6;
bullet.animationFrame += 1;
if(bullet.animationFrame >= bulletMaxAnimationFrames) bullet.animationFrame = 0;
bullet.animationTimer = bullet.animationTimer - bulletAnimationTime;
}
Most of this code should be pretty obvious, but let's break it down anyway. We start by increasing the bullet's animation timer by the delta time until the timer passes the target time (the bulletAnimationTime constant we defined just a moment ago). Once it does, we change to the next animation frame, looping back to the first frame if we've reached the end of the animation. Lastly, we reset the animation timer. You may wonder why we're taking this approach to resetting the timer; why don't we simply set the timer to zero? The reason is because the animation timer has exceeded the target time by some amount and we need to account for that. If we don't, then the timing of the animation would be wrong. So we set the timer to the difference between its current value and the target time which will "shave off" the exess amount from the animation timer.
We're now almost ready to start using our new rendering function. But before we do that, let's make a small helper function to calculate the top left of an Entity:
Vect2 topLeft(Entity* e)
{
Vect2 result = Vect2(
e.pos.x - cast(float)(e.width / 2),
e.pos.y - cast(float)(e.height / 2)
);
return result;
}
Now we're ready to replace those old calls to renderEntity. Modify the renderGame function in the following manner:
if(!s.gameOver)
{
// ... Code that clears the screen and render the background
foreach(ref turret; s.turrets)
{
renderSpriteFrame(renderer, s.texture_turret, topLeft(&turret), turret.width, turret.height, 0);
}
int treasureFrame = s.isTreasureRare ? 1 : 0;
renderSpriteFrame(renderer, s.texture_treasure, topLeft(&s.treasure), s.treasure.width, s.treasure.height, treasureFrame);
renderSpriteFrame(renderer, s.texture_hero, topLeft(&s.player), s.player.width, s.player.height, s.player.animationFrame);
foreach(ref bullet; s.bullets)
{
if (bullet.active)
{
renderSpriteFrame(renderer, s.texture_bullet, topLeft(&bullet), bullet.width, bullet.height, bullet.animationFrame);
}
}
}
Notice how the animation frame of the turret is always zero. This is because the turret doesn't animate since it only has one sprite. The treasure only has two frames, one of which we select depending on if the treasure has been flagged as rare. The sprite frames for the player and the bullets are decided during the update loop and stored in the animationFrame field, so we pass those value to renderSpriteFrame. Honestly, there's not a whole lot to it all. Compile and run the game to see the result. Doesn't that look better than before?
But wait, wasn't there something else we wanted to do this chapter? Yes there was. Isn't it a little bit sad that we're logging the player character's health points to the console rather than showing them on the screen? That's a pretty bad way of communicating vital information to the player. Well, we're ready to address that little issue! So go ahead and remove the call to printf that logs the player's health. We won't be needing that anymore! Also, add a new texture to the GameState for the health display:
SDL_Texture* texture_health;
Let's add some code to render the player character's current health. This should be sandwiched between the code where we render the background and where we render the turrets:
int healthSize = 32;
int healthMargin = 24;
int healthDisplayWidth = (healthSize * maxHealth) + (healthMargin * (maxHealth - 1));
int healthStartX = (windowWidth / 2) - (healthDisplayWidth / 2);
foreach(i; 0 .. maxHealth)
{
Vect2 pos = Vect2(healthStartX + (i * healthSize) + (i * healthMargin), 36);
int healthFrame = i < s.health ? 0 : 1;
renderSpriteFrame(renderer, s.texture_health, pos, healthSize, healthSize, healthFrame);
}
We begin by calculating the total width of the health display by adding together the width of each possible health point and the margins between them. We want the health display to be centered at the top of the screen, so we find the center of the window and subtract half the width of the health display to find where we should start rendering the first health point. We then loop over each possible point of health, rendering hearts at regular intervals with margins between them. We render a full heart for each point of health the player has and an empty heart for each lost point of health.
If you compile and run the game now, you should see hearts displayed at the top of the screen representing the player character's health. As the player character loses health points, the lost hearts turn black. That's a far better means of communicating the player's standing in the game than printing a value to the console!
This chapter covered some interesting ground. Animations are an important part of making a video game feel more natural and can be used to provide much needed feedback to the player. Though this project only has limited animation, what we introduced here can be applied in a variety of ways. Most video games in the days of the SNES or Sega Genesis animated their sprites in a similar way, and to great effect. Though more advanced 2D animation techniques are common these days (such as animation tweening), the basics covered here still have their uses and may be all you would need for simple projects of your own.