Unity Audio Management: Struggles, Solutions, and Systems

Posted on

Sound is essential, so building a great audio manager was a key focus when working on my latest demo game The Adventures of Lazo.

Since my game does not have a lot of concurrent sounds, I thought it would be a good idea to have a central audio manager that every other component in the game could easily access and use. I wanted my manager to have the following qualities:

  1. Easily accessible from anywhere in the game
  2. Ability to dynamically add/assign audio clips
  3. Methods for easily changing music or playing sound effects
  4. Capability to fade music in or out

Meeting all four of these requirements was trickier than I anticipated.

Handling the audio data

The first task was to attach audio files to the audio manager. To make it straightforward, I created two data types: an enum called SoundClip for categorizing sounds and a struct AudioFile to pair a SoundClip with an AudioClip.

public enum SoundClip {
    MenuSelect
    World,
    // Etc.
}

[System.Serializable]
struct AudioFile {
    public SoundClip Key;
    public AudioClip Clip;
}

I then added a List<AudioFile> field to the audio manager, enabling me to change game sounds directly from the Unity editor. However, retrieving files was not efficient. To address this, I converted the list into a Dictionary<SoundClip, AudioClip> during the OnEnable phase.

private Dictionary<SoundClip, AudioClip> soundClips = new();

private void OnEnable() {
    foreach (var audioFile in soundClipsList) {
        soundClips[audioFile.Key] = audioFile.Clip;
    }
}

Now, accessing an audio file is as simple as soundClips[SoundClip.Battle];. The downside is that adding a new sound involves manual updates to the SoundClip enum, which I plan to improve in the future.

Playing the audio

With the data now in place, I needed to handle playing and fading music and sound effects. I went through several iterations before landing on a solution I felt good about.

My initial attempt

Failed audio manager architecture.
Failed audio manager architecture.

My first attempt is depicted above. This approach checked off requirements 1-3 but failed when it came to fading music in and out. The fading functionality required the use of IEnumerators, which are not available outside of MonoBehaviour classes.

When I googled this problem, I started reading about MonoBehaviour proxies. The idea was to attach a MonoBehaviour script to a component that’s solely responsible for running coroutines. This was interesting, but it seemed messy to me; I wanted to find a better way.

Singletons to the rescue?

Audio manager using a singleton.
Audio manager using a singleton.

My next attempt was to convert the ScriptableObject to a singleton. It worked fine, but I’m not a big fan of singletons. They feel like I’m bending the class architecture the wrong way, as if I’m breaking some unwritten rule. I also don’t like having this random global class that can be called upon from anywhere. I prefer being explicit and injecting my dependencies into my classes using SerializedFields.

Nevertheless, this was an option I had available if I couldn’t find a better way.

The final product

Architecture of my finished audio manager.
Architecture of my finished audio manager.

My final product mixed some of the concepts from the above attempts. I started off by creating a GameObject with an AudioSource attached. I then created and attached an AudioPlayer class, which had methods to play sound effects, fade music, and change music. Next, I created a prefab from this GameObject and removed it from my scene.

Back in my AudioManager class, I converted it back to a ScriptableObject and added a SerializeField to accept my AudioPlayerPrefab. I also added two private properties of type AudioPlayer: musicPlayer and sfxPlayer. I added getter methods for each of these properties that would instantiate an AudioPlayerPrefab the first time and add DontDestroyOnLoad to it.

Finally, I added a couple of methods that forward to the attached AudioPlayer classes, like this:

public void ChangeMusic(SoundClip key) {
    MusicPlayer.ChangeMusic(GetSoundClip(key));
}

public void PlaySFX(SoundClip key) {
    SFXPlayer.PlaySFX(GetSoundClip(key));
}

All in all, I’m very pleased with how the manager turned out. It works great and allows me to play sounds anywhere in my game. Music changes fade in and out smoothly, and the audio doesn’t cut off during a scene change (it carries through since the AudioManager persists for the duration of the game).

If you want to check out the finished code, you can find it here.