Audio Manager
Background
My Ataraxy project recently gained a member, Alex Lobl, who will be working to implement sound effects, music, the works.
In order to make his life easy, I wrote a simple Unity AudioManager which will serve a variety of ways to add sound effects as needed.
I felt it was an easy thing to share with others.
Goal
Simple calls to a singleton manager that will handle asset loading, audio source creation and removal.
Core Features
- Use of a Dictionary that allows access to audio clips simply by providing their name
- Loading clips upon request if not previously loaded to prevent heavy initial load times
- Use of an audio source prefab that can be pinned at specific locations or to specific parents
- Tracking of expired audio sources
- Provide access to the created AudioSource for further customization
- Vertical Remixing
- Crossfade support for switching between single tracks
- Only requires a single call to AudioManager.Instance.Awake() to initialize it.
How to use – Simple Case
The simplest situations of implementing audio involves playing a particular clip once so the desired audience can hear it. In this instance, the player is the desired audience and we want them to hear the entire clip. We can create the audio source as a child of player object, meaning the player will hear the entire sound effect at the same volume.
This is accomplished through the following call:
AudioManager.Instance.MakeSource(“Reflect”).Play();
The first component – AudioManager.Instance – is a static reference to the only existing AudioManager. We don’t need to do any GameObject.Find, or reference passing.
The second component – MakeSource(“Reflect”) – is a call to a particular method with a single given parameter. MakeSource will return a reference to an AudioSource. It will configure that AudioSource at a given location with a given clip. If no location or parent is given, it will default to the player because it is assumed an AudioSource should be heard. Internally, the AudioManager will check it’s dictionary for the name of the clip, if it cannot find that clip, it will load it from the Resources/Audio folder. This prevents front loading all of the audio.
The last component – Play() – This is a call to the returned AudioSource to tell it to begin playing. This is the simplest example where you will make a call when you desire the sound effect to be heard. If further customization is required, you can store the AudioSource reference returned by MakeSource() and change further parameters. AudioSource boasts a number of optional parameters that could be adjusted before calling the Play() function.
HOW TO USE – Positional Audio
There is another main case in which audio is used: when it is not immediately and perfectly audible. Imagine a rocket screaming across the sky, the explosive boom as it connects with a building in the distance, the thunderous footsteps of an approaching dinosaur. In all of these cases, we need the AudioSource to make use of Unity’s 3D volume. I programmed my AudioManager with this in mind.
Case 1 – Distance Source
This is accomplished through the following call (from a rocket near within it’s explosion code):
AudioManager.Instance.MakeSourceAtPos(“NearExplosionB”, transform.position).Play();
The new components here – MakeSourceAtPos(“NearExplosionB”, transform.position) – refers to the position of the rocket when it is exploding.
We want the audio source to be located there. So when the AudioManager creates this source, it does not put it directly in the player’s ear.
The AudioManager places the source at the given position in space. This means that the volume of the audio source will change as the player moves closer to it.
The other components, such as referring to the AudioManager, the returned AudioSource and calling Play() on that AudioSource all remain the same.
CASE 2 – Moving SOURCE
This is for that screaming rocket:
First, we need to hold onto the reference of the AudioSource:
public AudioSource rocketThrust;
Second, query the AudioManager for a source with the rocket’s transform as the parent parameter:
rocketThrust = AudioManager.Instance.MakeSource(“rocketBlaster”, transform.position, transform);
rocketThrust.loop = true;
rocketThrust.Play();
The MakeSource() parameters in order are:
- string clipName – which clip we want to assign to the AudioSource.
- Vector3 sourcePosition – where in world space the AudioSource should be set to.
- Transform parent – who the parent of the AudioSource should be, which it will move with.
By handing the rocket’s transform as the parent, the AudioSource will be assigned as a child of the rocket and will move accordingly with the rocket.
Finally, if we collide or disable our thruster, we want to stop the rocket’s thruster audio.
if (rocketThrust != null)
rocketThrust.Stop();
There are many fancier things you can do with moving sources, such as configuring doppler levels, panning and even rotation, but I will leave those as an exercise for the reader.
How to use – Music
Music is a slightly more complicated but necessary task.
Let’s look at how to do this:
There is one missing piece:
AudioManager.Instance.tracksActive[0] = true;
This allows us to talk to the AudioManager and enable or disable certain music tracks by setting a boolean.
When a track is detected to be disabled or enabled, it will automatically fade in or fade out the volume.
Potential Improvements
Object Pooling
Instead of creating and destroying audio prefabs, it would be more efficient to make use of an object pooling pattern.
- Create 20 Audio Source prefabs. Disable them until they are needed.
- When a new audio source is needed, re-initialize a previously created audio source. Return it customized as requested.
- Finally, when it is complete, return it to the pool of inactive audio sources.
- If we end up needing more than 20 audio source prefabs at once, create more.
Object pooling is a standard pattern to avoid creating and destroying frequently used objects. It isn’t always necessary to pool objects but it can give speed ups if your prefab creation is more expensive. When I upgrade this audio manager later, I will look to improve this.
Mike Geig at Unity gives a great implementation tutorial (and where I borrowed the image from.)
DELAYED PLAYING
Certain components within a game might want to create their AudioSources before they are needed. This could be used to spread out the workload of creating multiple audio sources. Currently, my AudioManager doesn’t support delayed playing. This means that if you call MakeSource() and don’t call that AudioSource’s Play() function, the AudioManager will clean up that AudioSource before it can be used.
This could be addressed by making a custom wrapper script which would hold reference to the AudioSource as well as storing a boolean if it had been played yet. The AudioManager would then only clean up sources that had been played & had finished playing.
Music Track control by Name
So above in my example for toggling music playing or not, there is a design weakness that could be improved.
AudioManager.Instance.tracksActive[0] = true;
The problem with this implementation is that you have to know which track is which. A better solution would be to allow outside scripts to ask for tracks to enable or disable by name. An additional feature would be the idea to ask the AudioManager to PlayExclusiveMusic, which would automatically disable all others.
There is plenty of room for improvement in the music section of my AudioManager and I will improve it based on the demands of the project.
Final words
After a bit more bug fixing I hope to have a UnityPackage up to share!
If you don’t want to wait, feel free to visit Ataraxy’s Github and directly grab my AudioManager. (If you do this, you should also look at my GameManager as it queries for the music to be created and controls which music is playing at a given time)
Thank you for reading!