Sound Effects and Volume
For testing purposes, change the starting level to "Assets/Level03.json"
as that will demonstrate some of the initial sounds we will add.
Player Sound Effects
Because the player’s sound effects play on the player, we don’t need to do any special volume calculations. You can just play the sounds like you did in earlier labs.
You’ll add all the sounds to PlayerMove
.
First, add a looping sound for footsteps:
- Use the
"FootstepLoop.ogg"
and start playing it in the constructor, saving the sound handle - Immediately pause it after starting the sound
- At the end of
PlayerMove::Update
, check if the current state isOnGround
andmVelocity.Length()
is greater than 50, in which case you should resume the sound. If those conditions are not met, pause the sound. - Make sure you stop the sound in the
PlayerMove
destructor, or you’ll end up with multiple instances of this sound when changing/reloading the level
Next, add the following non-looping sounds:
"Jump.ogg"
when you start a jump"Land.ogg"
when you change state fromFalling
toOnGround
"PortalTeleport.ogg"
when you teleport through a portal"PortalShootBlue.ogg"
when you create a blue portal"PortalShootOrange.ogg"
when you create an orange portal"PortalFail.ogg"
when you try to create a portal but you can’t (either because theSegmentCast
doesn’t hit anything, or it hits something that’s not aBlock
)"PortalClose.ogg"
when you pressR
to reset the portals, and there’s at least one portal that will close
So that you don’t hear the landing sound right when the level loads, change it so PlayerMove
begins in the OnGround
state rather than the falling state.
Try the various player sounds. They should sound like this:
Adding Actor Tracking to AudioSystem
To support having sounds get quieter the further they are from the player, we need to add some new functionality to the AudioSystem
.
First, add a class Game*
pointer to the AudioSystem
member data, and change the constructor so it takes in a game pointer as a first parameter (keeping the number of channels as an optional second parameter), and sets the game pointer member variable to what’s passed in.
Then in Game::Initialize
, change the line that dynamically allocates mAudio
to this:
mAudio = new AudioSystem(this, 32);
This will set the game pointer and additionally pass in 32
as the number of channels you want (up from the default of 8).
Member Data Changes
In order to make the sound falloff work, we need to be able to track if a sound is playing for an Actor
and if so, we can update the volume based on the distance of the Actor
to the player.
First, add the following member variables to the declaration of HandleInfo
:
class Actor* mActor = nullptr;
to track the actor, if any (defaults to null)bool mStopOnActorRemove = true;
to note whether the sound should stop if the actor is removed (defaults to true)
Next, in the member data of AudioSystem
we need to add a map to track which sound handles are associated with a particular actor:
// Map for actors to their handles
std::unordered_map<class Actor*, std::set<SoundHandle>> mActorMap;
The key in this map is the pointer to the actor, and the value is an set
that contains all the sound handles associated with that actor. You’ll need to add an include for <set>
for this to work.
PlaySound Changes
Next, change the signature of PlaySound
to the following:
SoundHandle PlaySound(const std::string& soundName, bool looping = false,
class Actor* actor = nullptr, bool stopOnActorRemove = true,
int fadeTimeMS = 0);
This will allow callers to optionally specify an actor when playing the sound.
Then, in the implementation of PlaySound
, set the mActor
and mStopOnActorRemove
variables in the HandleInfo
when you play the sound.
Furthermore, if actor
is not nullptr
, you also want to add the SoundHandle
for the new sound you’re playing to the mActorMap
’s set for that actor.
RemoveActor
Next, add this public member function to AudioSystem
:
void RemoveActor(class Actor* actor);
The purpose of this function is that we’ll call it whenever an actor is destroyed, in which case RemoveActor
will see if there are any sounds associated with the actor which now need to be halted.
The implementation of RemoveActor
should do the following:
- Try to find the actor in
mActorMap
(not all actors will be in the map). If the actor is in the map:- Iterate over each handle in the
set
for that actor- Try to find the handle in the
mHandleMap
(not all handles may be here, if they’ve already stopped). If the handle is in themHandleMap
:- Set its corresponding handle info’s
mActor
tonullptr
- If the handle info has
mStopOnActorRemove
andMix_Playing
says the sound is playing, callMix_HaltChannel
on the sound. (Remember that allMix_*
functions take in themChannel
in the handle info NOT the sound handle)
- Set its corresponding handle info’s
- Try to find the handle in the
- Erase the actor from the map
- Iterate over each handle in the
Call RemoveActor
at the start of the Actor
destructor.
Update Changes
Recall that AudioSystem::Update
checks to see if an existing sound has stopped, in which case it erases the handle info from mHandleMap
.
Before you call mHandleMap.erase
, you should check if the SoundHandle
has an actor associated with it (which you can find out from the HandleInfo
for it). If it does have an actor associated with it, you should erase the SoundHandle
from the actor’s corresponding set
.
Testing Actor Tracking
We’ll now add some additional non-looping sounds, but associated with specific actors. In general, to do this, you’ll call PlaySound
, passing in false
as the second parameter (for non-looping) this
as the third parameter (for the corresponding actor):
"DoorOpen.ogg"
inDoor
when it opens"PelletFire.ogg"
inEnergyLauncher
when it fires a pellet"EnergyCaught.ogg"
inEnergyCatcher
when it catches a pellet"PelletDeath.ogg"
inPellet
when it diesBUT
you need to pass infalse
as the fourth parameter so that the sound doesn’t cut off when the pellet is destroyed (since that happens immediately thereafter)
If you play now, you’ll hear sounds for the above. However, without changing the volume, it just sounds really bad since you’ll hear the energy launchers and pellets from the next room even when you’re far away.
Adding Volume Falloff
Now we’ll add a volume falloff function so that sounds that are further away become quieter.
CalculateVolume
Add the following private
helper function to AudioSystem
:
int CalculateVolume(class Actor* actor, class Actor* listener) const;
This function returns a volume from 0
to 128
based on the distance between the actors and a inner/outer radius function:
- If either
actor
orlistener
isnullptr
, just return 128. - For the position of the actors you MUST use
GetWorldPosition()
since there may be actor parenting involved, and then calculate the distance between these world positions - If the distance is
>= 600.0f
,CalculateVolume
should return 0 - If the distance is
<= 25.0f
,CalculateVolume
should return 128 - For any distance between 25 to 600, you should set the volume as a linear function from volume of 128 to volume of 0. For example:
- A distance of 25 should yield 128
- A distance of 600 should yield 0
- A distance of 312.5, which is half way between 25 and 600, should yield 64
- A distance of 168.75, which is 25% of the way between 25 and 600, should yield 96
- If you’re not sure how to do this, check the slides for further information
Using CalculateVolume
For sounds that have an associated actor, we need to use CalculateVolume
both when we first start playing a sound and then on every frame, so that the volume can update as the player moves from the actor.
First, in PlaySound
immediately after the call to Mix_PlayChannel
, use CalculateVolume
to get the correct volume. You want to pass in the actor
as the first parameter, and the player as the second parameter.
Once you have the correct volume, use Mix_Volume
, passing in the channel as the first parameter and the volume you got back as the second parameter.
Since CalculateVolume
has null checks for actor/player, it’s safe to call it even if the actor pointer is null.
Next, in AudioSystem::Update
, if a particular sound handle is still playing, check to see if it has an actor associated with it. If it does, call CalculateVolume
and Mix_Volume
again so that the volume updates dynamically.
The energy launcher, pellet, energy catcher, and door sounds should now all have their volume dynamically change as you move through the level. It’ll sound like this:
Turret Sounds
For testing purposes, change the starting level to "Assets/Level04.json"
as that has turrets.
State Changing VO Sounds
In TurretHead
, add a map where the key is the TurretState
and the value is the name of the sound file you want to play:
- Idle is
"TurretIdle.ogg"
- Search is
"TurretSearch.ogg"
- Priming is
"TurretPriming.ogg"
- Firing is
"TurretFiring.ogg"
- Falling is
"TurretFalling.ogg"
- Dead is
"TurretDead.ogg"
Add a SoundHandle
to track the current VO sound it’s playing. Then, in ChangeState
:
- Check to make sure the new state is different from the old one
- If another VO sound is still playing, stop it first
- Play the sound from the map corresponding to the state you’re changing to, passing in
this
for the third parameter toPlaySound
, and save the handle in the member variable
This should sound like this:
Friendly Fire Sound
Add a public
TakeDamage
function to TurretHead
that takes in no parameters. The first first time this function gets called for that particular TurretHead
:
- It should stop the current VO sound, if there is one playing
- Play
"TurretFriendlyFire.ogg"
on this actor
For the HealthComponent
in TurretBase
, you will need to set the OnDamage
callback to call the TakeDamage
function on the TurretHead
. The syntax for setting the callback is something like this:
health->SetOnDamage([this](const Vector3& location) {
// Code you want to run
});
Bullet Sound
Every time the turret fires a bullet, you want to play "Bullet.ogg"
. However, instead of playing it on this
actor, you should play it on the target you’re firing at.
You should now have the full set of turret sounds:
Once you’ve pushed this code, you’re ready to move on to part 3.