AudioSystem and Finishing Touches
Pac-Man Dying
You should now confirm that your game responds correctly when Pac-Man dies. The logic that detects Pac-Man’s death is already implemented for you. It will:
- Pause each ghost
- Call
GhostAI::Start
on each ghost - Unpause each ghost
The expected behavior when this happens is that your ghosts will all reset to their start position and reset to Scatter with a state time of 0 seconds. If this doesn’t work properly, this likely means that your GhostAI::Start
may be missing something, so confirm that you have implemented all the expected logic.
Confirm your ghosts properly reset their position and start out in scatter again when Pac-Man dies:
AudioSystem
As mentioned in the lecture, we’re going to implement a wrapper class for SDL_mixer to both fix some common issues and give us more control over the behavior. You should keep the relevant slides handy for this part of the lab.
The starter code for Lab 5 includes the full declaration of the AudioSystem
class and functions in AudioSystem.h
. You should not need to make any changes to this header file in this lab.
If you look at AudioSystem.cpp
, you’ll see that four functions are already implemented for you:
CacheAllSounds
CacheSound
GetSound
ProcessInput
The rest of the functions in AudioSystem
are not implemented and you will need to implement them. We recommend that you implement the functions in the order described here.
Constructor
- Call
Mix_OpenAudio
using the same parameters as before:Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
-
Call
Mix_AllocateChannels
, passing in the constructor’s parameter into it. This sets the number of channels SDL_mixer is using to the specified value. - Resize the
mChannels
vector so its size corresponds to the number of channels.
Destructor
- Loop over the sounds in the
mSounds
map and callMix_FreeChunk
on each. - Clear the
mSounds
map - Call
Mix_CloseAudio
PlaySound
For now, just assume that there is a channel available. You’ll implement the prioritzation a little bit later once you confirm the basic sounds work properly.
-
Use
GetSound
to get theMix_Chunk*
for the sound.GetSound
will returnnullptr
if the sound cannot be loaded. In this happens, you should output this error message:SDL_Log("[AudioSystem] PlaySound couldn't find sound for %s", soundName.c_str());
And then return
SoundHandle::Invalid
to signify thatPlaySound
failed. -
Remember that the idea behind the
mChannels
vector is that if a particular index has aSoundHandle
which is!IsValid()
, that means the SDL_mixer channel (with the same number as the index) is available. Alternatively, if the index has aSoundHandle
whichIsValid()
, it means that sound handle is currently active on that. So, using a normalfor
loop, find the first index inmChannels
that’s!IsValid()
, meaning that the SDL_mixer channel of that number is available. This is the channel you will want to play this new sound on. -
The
mLastHandle
member variable is used to keep track of the lastSoundHandle
used. Every time you start play a new sound, you need to increment it with++
to make that the new sound’s uniqueSoundHandle
. -
Next, you need to setup a
HandleInfo
for the new sound you’re about to play. For this:- The
mSoundName
andmIsLooping
variables are just the parameters which are passed intoPlaySound
mIsPaused
should always start as falsemChannel
should be set to whichever index you selected from the vector ofmChannels
- The
-
You need to add to
mHandleMap
theSoundHandle
andHandleInfo
pair -
You need to update the index you selected in
mChannels
to have theSoundHandle
for this new sound -
Now play the sound using
Mix_PlayChannel
:- For the first parameter DO NOT pass in
-1
. Instead, you need to pass in index you selected from themChannels
vector (which again, corresponds to the SDL_mixer channel you want). - The second parameter is just the
Mix_Chunk*
you got in step 1. - The third parameter should be
-1
iflooping
istrue
or otherwise0
.
- For the first parameter DO NOT pass in
-
Finally, return the sound handle for the new sound.
Update
Every frame, you need to do a regular for
loop over mChannels
. For any indices which have an IsValid()
SoundHandle
, use Mix_Playing
to see if that sound is still playing on its corresponding SDL channel number.
If the sound is NOT playing anymore, this means you need to remove that SoundHandle
from the mHandleMap
and you should reset that index in mChannels
with .Reset()
as the channel should be flagged as available again.
GetSoundState
- If the
SoundHandle
is not in themHandleMap
, this function returnsSoundState::Stopped
. - Otherwise, it should return either
SoundState::Paused
orSoundState::Playing
, depending on the value of theHandleInfo
’smIsPaused
element for the requestedSoundHandle
.
Keep in mind that using mHandleMap[handle]
is NOT safe if the handle is not in the map. This is because the operator[]
for map will add the element to the map if it does not exist. Instead, you need to use mHandleMap.find(handle)
whenever you aren’t sure if something is in the map or not.
StopAllSounds
- Call
Mix_HaltChannel(-1);
which will stop ALL sounds. - Call
Reset()
on every index inmChannels
and clearmHandleMap
.
PauseSound/ResumeSound
These two functions work pretty similarly.
- If the
SoundHandle
is not inmHandleMap
, you should output a log error message like this:SDL_Log("[AudioSystem] PauseSound couldn't find handle %s", sound.GetDebugStr());
And then the function should return without doing the later steps. (Obviously, saying
ResumeSound
for theResumeSound
function). -
If it’s the handle is in the map, check the value of
mIsPaused
in the handle map to make sure you aren’t trying to pause a sound that is already paused or resume a sound that’s already resumed. Then, call eitherMix_Pause
(forPauseSound
) orMix_Resume
(forResumeSound
). - Update
mIsPaused
in the handle map to the new value.
StopSound
- If the
SoundHandle
is not inmHandleMap
, you should output a log error message like this:SDL_Log("[AudioSystem] StopSound couldn't find handle %s", sound.GetDebugStr());
And then the function should return without doing the later steps.
- If it’s in the map, call
Mix_HaltChannel
on the appropriate channel,Reset()
that index in themChannels
, and remove theSoundHandle
frommHandleMap
.
Keep in mind that if you have an iterator to an element in the map, and then remove that element from the map, the iterator is no longer valid, and derereferencing it after removal may cause a crash.
At this point, if you play the game you should hear sound effects working like in the final video at the bottom of the page. However, you still need to implement the prioritization when we run out of channels.
Prioritization
To test out the prioritization code, we added a special key binding to Pac-Man where if you hold down the N
key, it will spam play the "EatGhost.wav"
sound on every frame.
Currently, the code in PlaySound
assumes that there is a channel available in mChannels
. However, if you loop through mChannels
and none of the channels are available, then before playing a sound, you need to select which sound to overwrite. Use the following prioritization:
- The oldest instance of the same
soundName
and if there is none… - The oldest non-looping sound and if there is none…
- The oldest sound
Remember that because each subsequent sound has its sound handle assigned in ascending order, and mHandleMap
is sorted by SoundHandle
s, it means that if you iterate through mHandleMap
it will iterate in order from oldest to newest.
Once you decide to overwrite a sound, you should:
-
Save that channel as the channel to play the new sound on
-
Output an error message that looks like this:
"[AudioSystem] PlaySound ran out of channels playing %s! Stopping %s"
(Where the first
%s
is the name of new sound that’s about to play and the second%s
is the name of the old sound you’re overwriting). -
Erase the old sound’s
SoundHandle
frommHandleMap
Then you will want to play the new sound on the channel you’ve selected, doing everything you did before when you played the sound. If you structure this well, you can put all the code required for playing the sound in one spot rather than repeating it in multiple places.
Now when you hold down the N
key, you should get a lot of spam about PlaySound
stopping EatGhost.wav
because it ran out of channels. But critically, if you let go of the N
key you should notice that all the game sounds still work as expected as they won’t have been stopped due to the prioritization system.
AudioSystem Unit Tests
We have also provided several unit tests to confirm that your AudioSystem
is implemented as expected. These will automatically run every time you push AudioSystem.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-Lab05 repo and copy your AudioSystem.cpp file into that repo.
Finishing Up
Finally, you need to enable full introduction before the game starts. On Line 25 of PacManMove.h
, change the INTRO_TIME
constant to 4.0f
. (For some reason, on Visual Studio when you change this constant, you may have to force a rebuild for it to recognize it needs to recompile PacManMove.cpp
).
Also, in Ghost.cpp, go back to line 61 to set the starting animation back to “blank” so that the ghosts don’t appear during the intro time.
Now your game should play the intro and sound effects, which will look and sound like this:
Once you’ve pushed your code, you should review the grading specifications to confirm you’ve satisfied them.