Sound Manager for small games and prototypes on Unity
Difficulties begin when there are a lot of sounds in the game. They all need to be set, to set priorities. Sounds separately, music separately. When adjusting the volume of sounds and music separately, there are also difficulties. You can, of course, adjust the volume of different channels in AudioMixer, but it does not work in WebGL. And Webplayer is now considered obsolete.
And if some kind of sound is repeated several times in a row (for example, a player quickly presses a button and plays the sound of a click), it would be nice if it does not break off in the middle, but starts a new one without interfering with the previous one. Yes, and when you turn on the pause, the sounds of the game must be paused, but the sounds of the menu are not. Out of the box, there is such an opportunity in Unity, but for some reason it is accessible only from a script and not everyone knows about it.
In general, I want a simple and convenient SoundManager, the creation of which I will describe. For large projects, it is not suitable, but for prototypes and small games it is.

So what should SoundManager be? Well, firstly, it should be convenient to use. That is, there are no “find an object on the stage”, “connect a component” and other things for the user, everything is inside. So immediately make it a singleton (The code is shortened to highlight the essence).
private static SoundManager _instance;
public static SoundManager Instance
{
get
{
if (_instance != null)
{
return _instance;
}
// Do not modify _instance here. It will be assigned in awake
return new GameObject("(singleton) SoundManager").AddComponent();
}
}
void Awake()
{
// Only one instance of SoundManager at a time!
if (_instance != null)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
Now the manager himself will create himself on the stage, so adding it yourself is not necessary (and not recommended).
Now any methods can be accessed simply by writing SoundManager.Instance.Method (). To further reduce this entry for all methods, I added a static wrapper:
public static void PlayMusic(string name)
{
Instance.PlayMusicInternal(name);
}
So you can write even shorter SoundManager.Method ().
There is an object, it’s convenient to work with it. Next we add functionality. The most essential feature is PlaySound:
void PlaySoundInternal(string soundName, bool pausable)
{
if (string.IsNullOrEmpty(soundName)) {
Debug.Log("Sound null or empty");
return;
}
int sameCountGuard = 0;
foreach (AudioSource audioSource in _sounds)
{
if (audioSource.clip.name == soundName)
sameCountGuard++;
}
if (sameCountGuard > 8)
{
Debug.Log("Too much duplicates for sound: " + soundName);
return;
}
if (_sounds.Count > 16) {
Debug.Log("Too much sounds");
return;
}
StartCoroutine(PlaySoundInternalSoon(soundName, pausable));
}
IEnumerator PlaySoundInternalSoon(string soundName, bool pausable)
{
ResourceRequest request = LoadClipAsync("Sounds/" + soundName);
while (!request.isDone)
{
yield return null;
}
AudioClip soundClip = (AudioClip)request.asset;
if (null == soundClip)
{
Debug.Log("Sound not loaded: " + soundName);
}
GameObject sound = (GameObject)Instantiate(soundPrefab);
sound.transform.parent = transform;
AudioSource soundSource = sound.GetComponent();
soundSource.mute = _mutedSound;
soundSource.volume = _volumeSound * DefaultSoundVolume;
soundSource.clip = soundClip;
soundSource.Play();
soundSource.ignoreListenerPause = !pausable;
_sounds.Add(soundSource);
}
First, a few sound checks. That it is not empty and that there are not too many such sounds (If somewhere in the cycle it is mistakenly called). After that we load the sound from the resources, wait for the download, create a new object on the stage, add the AudioSource, configure it and start it. The LoadClipAsync function starts asynchronously loading a sound file from resources by name. So the file will need to be put in the “Resources / Sounds / Sounds” folder. Creation of an object occurs by prefab, which is loaded from resources. So part of the parameters (like the priority of the sound), you can set the prefab from the inspector. The volume is also set for each object separately. Unlike setting the AudioListener volume, this allows you to adjust the volume of sounds and music separately. Save the object in the list of sounds _sounds,
The pausable parameter is needed to separate UI sounds and game sounds. The first should be played always and never be paused. The second ones are paused and continue when the game resumes. This is done automatically using the flag soundSource.ignoreListenerPause, which for some reason is not available from the Inspector.
Next we need a method to add music to the game. In general, the code is similar to adding sound, but another prefab is used (with another priority and loop setting).
void PlayMusicInternal(string musicName)
{
if (string.IsNullOrEmpty(musicName)) {
Debug.Log("Music empty or null");
return;
}
if (_currentMusicName == musicName) {
Debug.Log("Music already playing: " + musicName);
return;
}
StopMusicInternal();
_currentMusicName = musicName;
AudioClip musicClip = LoadClip("Music/" + musicName);
GameObject music = (GameObject)Instantiate(musicPrefab);
if (null == music) {
Debug.Log("Music not found: " + musicName);
}
music.transform.parent = transform;
AudioSource musicSource = music.GetComponent();
musicSource.mute = _mutedMusic;
musicSource.ignoreListenerPause = true;
musicSource.clip = musicClip;
musicSource.Play();
musicSource.volume = 0;
StartFadeMusic(musicSource, MusicFadeTime, _volumeMusic * DefaultMusicVolume, false);
_currentMusicSource = musicSource;
}
In most small projects, one track that is currently playing is sufficient, so starting new music stops the previous tracks automatically, so that only SoundManager.PlayMusic (“MusicForCurrentScene”) is enough to call on each scene; In addition, when creating and stopping music, a smooth increase in volume and a smooth fade out are added. This allows you to make the transition smooth and does not hit the ear. The smoothest change in volume can be done with Tween, but you can also use the handles so that there are fewer dependencies.
Next we need the opportunity to pause. Since all sounds have already been checked whether they are paused when AudioListener is paused, the methods are very simple.
public static void Pause()
{
AudioListener.pause = true;
}
public static void UnPause()
{
AudioListener.pause = false;
}
Or, you can configure the automatic inclusion of a pause.
void Update()
{
if (AutoPause)
{
bool curPause = Time.timeScale < 0.1f;
if (curPause != AudioListener.pause)
{
AudioListener.pause = curPause;
}
}
}
Next, we need methods for setting and obtaining volume.
void SetSoundVolumeInternal(float volume)
{
_volumeSound = volume;
SaveSettings();
ApplySoundVolume();
}
float GetSoundVolumeInternal()
{
return _volumeSound;
}
void SaveSettings()
{
PlayerPrefs.SetFloat("SM_SoundVolume", _volumeSound);
}
void LoadSettings()
{
_volumeSound = PlayerPrefs.GetFloat("SM_SoundVolume", 1);
ApplySoundVolume();
}
void ApplySoundVolume()
{
foreach (AudioSource sound in _sounds)
{
sound.volume = _volumeSound * DefaultSoundVolume;
}
}
Everything is simple here. We save and read the settings using PlayerPrefs, when changing, go over the sounds and apply a new volume. Similarly, you can make the mute tuning and all the same for music.
That's it. SoundManager is easy to use. Since we took the templates for sounds and music to prefabs, we can easily connect output from AudioMixer to them. In addition, there is still a small class that simplifies the calls of the necessary methods from animations, button handlers, etc., so that you do not need to write a script because of one line.

Pros of the manager received:
+ Ease of use
+ Clean code and scene objects. No need to hang up the sound components anywhere, look for and call them from the code
+ Music, which does not interrupt when loading a scene and changes smoothly
+ Gameplay and UI sounds
+ Pause support
+ AudioMixer support
+ Work on all platforms, including those that do not support AudioMixer (for example WebGL)
+ Narrator's voice support (not mentioned in the article, but implemented in the full code)
Limitations of the current implementation (Not yet):
- While there is no positional 3d sound
- Changes to the pitch of the sound so that a multiple repetition of the same sounds does not bother
- Downloading sound during use can lead to lags (Unnoticed on small projects and small sounds)
- No volume control for a single sound
- No looped sounds, like ambient. The
full code of the manager can be viewed on my GitHub:
https://github.com/Gasparfx/SoundManager
Our project using this manager on GreenLight:
http://steamcommunity.com/sharedfiles/filedetails/?id=577337491