Animations, Sounds, and Finishing Touches
The game is now playable, but it could use some polish. Specifically, you’re going to add sprite animations, sound effects/music, and a win condition.
Animations
The Lab04
directory includes a declaration of AnimatedSprite
in AnimatedSprite.h/cpp. The AnimatedSprite
class inherits from SpriteComponent
. Much of the code is already implemented, but you’ll have to implement the Update
function.
First, take a moment to understand the member data in AnimatedSprite.h:
mAnims
map associates an animation name like “run” with a vector of corresponding textures. This way, you can support switching to different animations.mAnimName
is the name of the current animation, or empty if the animation hasn’t been set yetmIsPaused
isfalse
if the animation is running, ortrue
if it should be pausedmAnimTimer
is used to track the amount of time elapsed in the current animationmAnimFPS
is the animation “frames per second”. It defaults to 10, which is what we want for this lab
Now you must implement AnimatedSprite::Update
. The basic idea is:
- If
mAnimName.empty()
, don’t do anything (since we haven’t set an animation)- If the animation is not paused (i.e.
!mIsPaused
), updatemAnimTimer
based on the animation FPS and delta time - Do the wrapping of
mAnimTimer
to make sure it isn’t>=
the number of animation frames (consult the slides if you don’t know what this means). - Call
SetTexture
(which is inherited fromSpriteComponent
), passing in the correct texture based on themAnimName
andmAnimTimer
- If the animation is not paused (i.e.
Steps (b) and (c) should ALWAYS happen, even if the animation is paused. This is because we want the animation texture to still get set properly even when the animation is paused.
AnimatedSprite Unit Tests
As with Lab 3, we have provided a handful of unit tests that validate your AnimatedSprite functionality. These will automatically run every time you push AnimatedSprite.cpp. Keep in mind you need to pass the unit tests to satisfy the spec for animation.
You can also run the tests locally if you clone the https://github.com/itp380-20243/tests-Lab04 repo and copy your AnimatedSprite.cpp file into that repo.
Goomba Animations
You’ll first test AnimatedSprite
with Goomba
. In Goomba.cpp, instead of creating a SpriteComponent
, create an AnimatedSprite
.
Then, add code like this to create an animation called "walk"
:
std::vector<SDL_Texture*> walkAnim{
GetGame()->GetTexture("Assets/Goomba/Walk0.png"),
GetGame()->GetTexture("Assets/Goomba/Walk1.png")
};
// This assumes "asc" is the animated sprite component
asc->AddAnimation("walk", walkAnim);
Generally, you want to create all the animations you need in the constructor of the actor, so they are ready to be used when it’s time to play the animation.
Do not add the animation every time you want to play it. This is super inefficient. You only need to add the animation once for each AnimatedSprite
. Once it’s added, you can change to that animation whenever you want to.
Add an additional animation to Goomba
called "dead"
that only contains the one "Assets/Goomba/Dead.png"
texture from before.
To play an animation, you need to call the SetAnimation
function passing in the name of the animation to play. This assumes that you previously added an animation with that name.
For example, this would set the current animation to "walk"
:
// This assumes "asc" is the animated sprite component
asc->SetAnimation("walk");
Now, set it up so that by default the Goomba uses the "walk"
animation and when it gets stomped, change the animation to "dead"
.
When you run your game, you should notice that the goombas now play a walking animation that alternates between two frames. When a goomba gets stomped, it should still correctly show the dead texture.
Mario Animations
Now in the Player
class, switch out the SpriteComponent
for an AnimatedSprite
.
Mario has several different animations. All of Mario’s textures are in "Assets/Mario/"
. For brevity, I will omit the directory from the list of animations and textures, but you still need to specify that when loading them. Here are the animations:
"idle"
–Idle.png
"dead"
–Dead.png
"jumpLeft"
–JumpLeft.png
"jumpRight"
–JumpRight.png
"runLeft"
–RunLeft0.png
,RunLeft1.png
,RunLeft2.png
"runRight"
–RunRight0.png
,RunRight1.png
,RunRight2.png
Set the player’s initial animation to "idle"
.
Now in PlayerMove
, you’ll need to add code that switches between the various animations as needed. We recommend putting this in new function, and calling the function at the end of PlayerMove::Update
.
Most of the animation decisions are straightforward but jumping is a little weird. This is because if you start jumping to the left but then let go of the left key, you still want Mario to face to the left and not be idle.
Here is the logic:
- If Mario is on the ground and…
- Moving to the right –
"runRight"
- Moving to the left –
"runLeft"
- Not moving left or right –
"idle"
- Moving to the right –
- If Mario is in the air and…
- Moving to the right –
"jumpRight"
- Moving to the left –
"jumpLeft"
- If Mario’s not moving right or left, then you decide what to play based on the current animation:
- If the current anim is
"runRight"
or"jumpRight"
or"idle"
–"jumpRight"
- Otherwise –
"jumpLeft"
- If the current anim is
- Moving to the right –
● If Mario is dead – "dead"
Keep in mind you can get the name of the current animation from an AnimatedSprite
via GetAnimName
.
Verify that all the Mario animations play when expected, especially the running/jumping ones.
Sound Effects and Music
For sounds, we’ll just use the basic SDL mixer functionality covered in lecture.
First, add an #include "SDL2/SDL_mixer.h"
to Game.h.
Next, in Game::Initialize
before the call to LoadData
, add:
Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
Likewise, in Game::Shutdown
call Mix_CloseAudio
(it takes no parameters).
Loading Sounds
In Game.h/cpp add the following function:
Mix_Chunk* GetSound(const std::string& filename);
This will use the same map technique we used for textures. GetSound
either finds the sound in the map, or if not in the map it’ll load the sound file and add it to the map. To load a sound file into a Mix_Chunk*
, use Mix_LoadWAV
: https://wiki.libsdl.org/SDL_mixer/Mix_LoadWAV.
Unloading Sounds
In Game::UnloadData
delete all the cached sounds with Mix_FreeChunk
: https://wiki.libsdl.org/SDL_mixer/Mix_FreeChunk.
Sound Effects
You can play one-off sound effects using Mix_PlayChannel
: https://wiki.libsdl.org/SDL_mixer/Mix_PlayChannel.
- Pass
-1
as the first argument to tell SDL to choose the next available channel. - The second argument is the
Mix_Chunk*
to play. - Set the third argument to
0
to tell it not to loop the sound.
Mix_PlayChannel
returns an integer of the channel on which the sound’s being played, which lets you control the sound after it has started. But for one-off sound effects, you typically won’t want to save the return value.
Now hook up the following sound effects:
- When the player jumps (a full jump, not half jumps) –
"Assets/Sounds/Jump.wav"
- When the player stomps an enemy – “Assets/Sounds/Stomp.wav”
- When the player collides with the bottom side of the block –
"Assets/Sounds/Bump.wav"
Verify the different sound effects you’ve added work as expected.
Music
You can also play music with Mix_PlayChannel
. However, because you want the music to loop, you should pass in -1
for the third parameter, which says to infinitely loop the sound.
Now in Game::LoadData
, play the "Assets/Sounds/Music.ogg"
music file, and set it to looping. Make sure you save the return value of Mix_PlayChannel
in a member variable, because we will want to stop the music when the player wins or loses.
Verify the music plays and keeps looping.
Winning/Losing
Remove the code that caps the player’s position at 448.0f
. Instead, just let the player fall off screen, and once off-screen, make the player "die"
(which does the same thing as when a goomba kills him).
When the player dies, stop playing the music with Mix_HaltChannel
, passing in the channel number of the music you saved in your member variable. Then play the "Assets/Sounds/Dead.wav"
as a one-off sound effect (don’t loop the death sound).
The player “wins” the game once their x-position is greater than 6368
, which is where the flagpole is at. When the player wins, stop playing the music and play the "Assets/Sounds/StageClear.wav"
sound (don’t loop it). Also, set the player to ActorState::Paused
so he can no longer move.
Make sure the death sound and win sounds work. Your game should now play like in the video!
Once you’ve pushed your code, you should review the grading specifications to confirm you’ve satisfied them.