Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

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:

  1. Pause each ghost
  2. Call GhostAI::Start on each ghost
  3. 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

  1. Call Mix_OpenAudio using the same parameters as before:
    Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
    
  2. 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.

  3. Resize the mChannels vector so its size corresponds to the number of channels.

Destructor

  1. Loop over the sounds in the mSounds map and call Mix_FreeChunk on each.
  2. Clear the mSounds map
  3. 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.

  1. Use GetSound to get the Mix_Chunk* for the sound.

    GetSound will return nullptr 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 that PlaySound failed.

  2. Remember that the idea behind the mChannels vector is that if a particular index has a SoundHandle which is !IsValid(), that means the SDL_mixer channel (with the same number as the index) is available. Alternatively, if the index has a SoundHandle which IsValid(), it means that sound handle is currently active on that. So, using a normal for loop, find the first index in mChannels 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.

  3. The mLastHandle member variable is used to keep track of the last SoundHandle used. Every time you start play a new sound, you need to increment it with ++ to make that the new sound’s unique SoundHandle.

  4. Next, you need to setup a HandleInfo for the new sound you’re about to play. For this:

    • The mSoundName and mIsLooping variables are just the parameters which are passed into PlaySound
    • mIsPaused should always start as false
    • mChannel should be set to whichever index you selected from the vector of mChannels
  5. You need to add to mHandleMap the SoundHandle and HandleInfo pair

  6. You need to update the index you selected in mChannels to have the SoundHandle for this new sound

  7. 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 the mChannels 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 if looping is true or otherwise 0.
  8. 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

  1. If the SoundHandle is not in the mHandleMap, this function returns SoundState::Stopped.
  2. Otherwise, it should return either SoundState::Paused or SoundState::Playing, depending on the value of the HandleInfo’s mIsPaused element for the requested SoundHandle.

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

  1. Call Mix_HaltChannel(-1); which will stop ALL sounds.
  2. Call Reset() on every index in mChannels and clear mHandleMap.

PauseSound/ResumeSound

These two functions work pretty similarly.

  1. If the SoundHandle is not in mHandleMap, 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 the ResumeSound function).

  2. 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 either Mix_Pause (for PauseSound) or Mix_Resume (for ResumeSound).

  3. Update mIsPaused in the handle map to the new value.

StopSound

  1. If the SoundHandle is not in mHandleMap, 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.

  2. If it’s in the map, call Mix_HaltChannel on the appropriate channel, Reset() that index in the mChannels, and remove the SoundHandle from mHandleMap.

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:

  1. The oldest instance of the same soundName and if there is none…
  2. The oldest non-looping sound and if there is none…
  3. The oldest sound

Remember that because each subsequent sound has its sound handle assigned in ascending order, and mHandleMap is sorted by SoundHandles, 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:

  1. Save that channel as the channel to play the new sound on

  2. 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).

  3. Erase the old sound’s SoundHandle from mHandleMap

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.