Let’s keep working on our sound system for our RTS game – time to really code it up!
This article is also available on Medium.
Last time, we talked about the importance of sounds in video games and how they increase the immersion of the player; we also took a look at Unity’s sound system and even “pre-optimised” our sound file imports to avoid consuming too much memory. In this second part, we’ll focus on how to actually implement the sound system. So we’re going to see how to play some background music, global and local ambient sounds and contextual sounds like unit responses.
Note: I’ve embedded some videos with sound in this article to show our progress as we code the feature, but the quality of the sound recording isn’t very high – sorry in advance!
Preliminary step: moving the listener to a “ground target”
In part one of this duo of episodes, I mentioned that we had to tinker with the default listener setup a bit. Unity assumes that, in most of scenes, the camera is the representation of the player in the virtual world, position-wise. So it makes sense to use this object as the audio listener, especially for all the 3D audio sources.
However, in our RTS game, there is actually an offset between the actual point we are looking at (the 3D point on the ground that is the projection of the 2D point in the middle of the screen) and the position of the camera (up there in the sky, looking down at the ground). Thus we need to account for this difference and rather than keeping the listener on the camera, we need to place it on the ground.
The problem is that, for now, this point is completely abstract – we don’t actually have an object in our scene at that position. So let’s add a new “GroundTarget” empty game object as a child of our camera and add an AudioListener on it:
Remember that we also need to remove the current one from the camera.
Now, we can pass a reference to this “GroundTarget” in the
CameraManager class and simply update its position when we set up the camera, and whenever it later moves:
Alright, we have a “cursor” that tracks where the camera is looking. This point will be used to compute the distance to 3D audio sources, as we’ll see soon with the local ambient sounds.
Adding the background music
But first, let’s start with a simpler type of audio source: a 2D source, independent from distance that you always hear no matter where the camera is on the map.
For now, we’re going to keep it simple for the background music and directly load a clip into the source. (A more advanced system should probably get the clip from a list of possible tracks, either at random or depending on the map that’s currently loaded…) We also need to make sure that the audio loops so that, whenever we reach the end of the clip, we play it back again from the beginning. And finally, it shouldn’t be too loud to remain a background ambiance:
You can use whichever music you’d like and that fits your game – I used a personal composition that is available here for free under the CC0 license (public domain, no attribution required even though it’s always nice ;)), entitled “Awakening”. If you’re looking for some free CC0 music, you can check out freepd.com for example 🙂
If you start the game, you’ll now have some music playing automatically and looping infinitely…
Adding the global ambient sounds
Last time, I mentioned that in an RTS game like Warcraft 3, or like ours, it’s interesting to add global ambient sounds that support and pretty much “repeat” visual cues. For example, our day-and-night cycle could be spiced up by adding some rooster and wolf cries to indicate when the day starts and ends.
For these global ambient sounds, we’ll just add a new AudioSource on the “GAME” object with its default settings, except that we uncheck the “Play On Awake” parameter. But this time, we cant’t setup the audio clip in the source because it will change depending on the current game event. We will need to load the audio clip dynamically, at runtime.
To better reference and find our audio files, we are going to create a new Scriptable Object to hold our game sound parameters – it’s pretty similar to the
GameParameters class and is defined in a new
We can then create our instance of this Scriptable Object and drag our sounds into it (for my tests, I found some free CC0 rooster crow and wolf howl recordings on FreeSound.org):
Also, remember to turn on the day-and-night cycle feature in the game parameters while you’re at it 😉
We can now create our brand new
SoundManager class and pass in the parameters to load when the game starts. We are also going to prepare an event listener and the associated callback function. This function will play a sound “by name” – by directly finding the matching variable on the sound parameters Scriptable Object, we’ll be able to get a more abstract mapping:
SoundManager to our “GAME” object.
This method uses a very powerful of C#: reflection. I talked about it in a recent post about generics in C#; in short, it allows you to dynamically interact with your object types, for example to create an object with a dynamically computed type, or to get the type of variable that is not knowable beforehand by the programmer… or to get an object’s property by name rather than directly calling it, like here. Thanks to this technique, we can simply pass a string (the name of the clip to play) and, as long as we have defined a property with that name, it will automatically get the corresponding value.
All that is left to do is to trigger this event from our
DayAndNightCycler, when we reach the crucial rotations (I added a
rotation variable to help me keep track of the current cycle’s progression):
If I run the game, I now have a wolf howling when night falls and a rooster crowing when the sun rises again – of course, during my tests, I really reduced the length of a day so the entire cycle takes only 20 seconds!
Adding some local ambient sounds
I think that, with local ambient sounds, we should in fact be cautious not to add too many. If you set those on all of your units, it will probably create a cacophony that just exhausts and/or disgusts people from playing your game. So my advice is to keep it subtle and somewhat scarce.
Currently, we have three units: the House, the Tower and the Soldier. We’ve said that, for example, local ambient sounds could be an additional cue that a particular type of building is in view (like a quarry). So let’s ignore the Soldier for now and focus on our buildings. In theory, we could have both the House and the Tower produce some kind of crowd noise, or weapons preparing. But once again – I’d advise against having too many sounds.
Since, at the moment, our House is sort of a headquarters, we’re going to consider it “more important” than the Tower, and thus we’re going to add a local ambient sound only to the House.
The first step is to add the reference to the audio clip in our
BuildingData class so we can easily drag the clips in our Scriptable Object instances (we only fill it for the House):
Then, we’ll add an AudioSource on the building prefab if necessary (i.e. only for the House in our example), and we’ll add a public field in the
BuildingManager script for it:
Most of the settings we already discussed with the previous audio sources – simply make sure that the priority is higher than for the background music, that the volume isn’t too high and the spatial blend is fully enabled. This spatial blend offers some new options:
- the rolloff type: you can choose to have a linear rolloff like me to have the sound decrease steadily as the ground target moves away from the source; or a logarithmic rolloff so it decreases more and more; or even a custom function if you’re feeling fancy (here, the volume keeps on going up and down as the distance increases which makes no sense but gives a pretty curve 😉 ):
- the Min Distance and Max Distance determine the range of distances at which the listener can hear the source – and it is used by the rolloff function to know how much to decrease the volume by
Here is a little video of the scene with the music and global ambient sounds turned off, so we can better hear the local ambiance and how the volume falls off as the distance to the ground target increases – notice how the volume drops whenever I “look away” from the building:
Getting some feedback from our units
Finally, let’s take a look at the contextual sounds.
Adding a type-dependent sound on unit selection
First, we’re going to implement a simple logic to have the units produce a sound (that depends on the unit’s type) when it is selected, to acknowledge the selection. The setup is pretty similar to the one for local ambient sounds; we need to:
- add the sound clip to the data – but this time, since all units can be selected and produce a sound when this happens, we’ll put it in the
- add an AudioSource to the prefab (a second one if there is already one) that does not use spatial blend and has a high priority
- pass this AudioSource to the
UnitManagerand use it when we select the unit to play the matching sound
First, let’s add the audio clip field in the
Then, we can add a few lines in our
UnitManager to use this sound clip:
Finally, let’s take care of our House, Tower and Soldier Scriptable Objects and prefabs:
And tadaa! Now, the units will make a little noise when they are selected 🙂
Adding an “on move” sound for character units
Next, we’ll give the player some feedback when he/she asks a character unit to move to a point on the map. But let’s spice things up a little and distinguish between two cases: either the unit can actually reach the target point, or there are some obstacles or terrain on the way that block it. We will call the first case a “valid” path and the second one an “invalid” path.
We’ll add some additional clip sounds in our
CharacterData class (since movement is specific to character units and impossible for buildings):
Also, remember to drag and drop your sounds into the Scriptable Object asset!
Then, we’ll have to update our
CharacterManager script and modify its
MoveTo() function. Rather than simply assigning the destination, we’ll first compute the path and check it is valid. If it is not, we’ll play the appropriate sound and exit from the function early. Else, we’ll set the destination point for the agent as usual and play the “valid move” sound:
This way, the player will know immediately if the unit can actually perform the action or not! 🙂
Adding a “build ended” sound
We can also pretty easily add a contextual sound for building placement: whenever we finish placing a new building, we should play a little sound along with the current visual cues (the colour change and the fact that the building isn’t “phantom” anymore). We’re actually going to use our
SoundManager once again, so the setup will be closer to the one for global ambient sounds.
First, let’s add a new audio clip in our sound parameters, called
Now, we’ll simply send an event from our
BuildingPlacer script whenever we place a building, reusing the whole “clip by name” logic we implemented before:
And just like that – we now have a new sound whenever we place a building!
Adding a “producing unit” sound
A final example of contextual sound we can implement is to have some feedback whenever we cast a unit skill. For now, the only “skill” that we have is, for the House building, the ability to produce a unit. So let’s setup the logic for our skills to produce a sound when they’re cast, and then take care of this specific example.
Oftentimes, games have multiple sounds for skill casting, especially with magical powers. Think of a mage duel: you’d probably like fireballs and lightning strikes to have a start sound, then play another audio clip all the while they’re travelling around the battlefield, and finally emit a finishing blast sound.
In our case, we don’t really need the mid-part: it would actually be pretty annoying to have a constant noise in the background while producing a unit. However, we can distinguish between the “start” and the “end” of the skill casting.
Note: of course, you can absolutely add a third variable to also implement the “middle sound” if you want – it would be used very similarly to the other two, it’s just about playing it at the right time in our coroutines! 🙂
The “start” is the moment the player presses the button to begin the action. The “end” is the moment the skill is really triggered, after the cast time has elapsed.
So – first of, let’s add these new audio clip fields in the
Now, we can head over to our
SkillManager script. We’ll need to do a bit more during the initialisation phase and more importantly emit the proper sounds when running our
There are three things to comment in this snippet of code:
- the reason we store the
_sourceContextualSourceonce at the beginning is because performing a
GetComponent()call is costly: in terms of game optimisation, it’s better to avoid using too often and better to cache it once 😉
- we also do a basic sanity check on this
sourcevariable to make sure it has a
UnitManagercomponent on it to extract the audio source from. If it does, we’ll play sounds in the future; else, we simply ignore the sound logic. This way, we avoid nasty errors!
- finally, in the
_WrappedTrigger()coroutine, we just place the “start” and “end” sounds in the right place around the waits so that one sound plays before the cast time runs, and the other plays after it has finished
To finish up this article, here’s a video with all of our unit sounds active, plus the background music!
These last two weeks, we’ve discussed and implemented the workings of a basic sound system for our RTS game. Of course, those test sounds are crude and the design of a sound system is a job in itself. Real specialists are good enough to pick distinguishable but harmonious sounds that all come together to form a consistent and engaging world for the player.
But now, here’s a question: if the player wanted to mute the game, how could he/she do it? Right now, there is no way for someone who has the built game to change the options! Only developers and game designers can tune the Scriptable Objects and other parameters and change that… and that’s a shame.
That’s why in the next episodes, we’ll talk about in-game options and updating our game parameters at runtime. But first, we’ll need to properly prepare our game parameters for this new feature…