📜 ⬆️ ⬇️

Sound Manager for small games and prototypes on Unity

Playing sound in Unity is easy. You need to create an AudioSource component and attach an audio file to it as an AudioClip and call audioSource.Play () from a script. Or even put autoplay on when creating an object (Play on Awake).

Difficulties begin when the sounds in the game becomes a lot. They all need to arrange, prescribe priorities. Sounds separately, music separately. When adjusting the volume of sounds and music separately is also a difficulty. 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 sound is repeated several times in a row (for example, a player quickly presses a button and plays a click sound), then it would be good if he does not stop in the middle, and a new one starts, without interfering with the previous one. Yes, and when you turn on the pause, the sounds of the game need to be paused, but there are no menu sounds. Out of the box there is such a possibility in Unity, but for some reason it is available only from the 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 completely.




So what should be a SoundManager? Well, firstly it should be convenient to use. That is, no “find an object on the scene”, “attach a component” and other things for the user, everything is inside. So immediately make it a singleton (code 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<SoundManager>(); } } 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 scene, so adding it yourself is not necessary (and not recommended). It is created by prefab, the path to which is registered in the code, so you should not move the prefab. You can create with the help of new GameObject () and AddComponent () if you want . UPD. Now it is not created by prefab. In addition, the object is immediately marked with DontDestroyOnLoad. It is necessary for the music and sounds to continue to play without interruption during scene reloads.
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 even write shorter SoundManager.Method ().

The object is there, it is convenient to work with it. Further we add functionality. The most necessary 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<AudioSource>(); 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 launch it. The LoadClipAsync function starts asynchronous loading of a sound file from resources by name. So the file will need to be put in the “Resources / Sounds / Sounds” folder. The creation of the object occurs on the prefab, which is loaded from resources. So part of the parameters (like sound priority) can be set to the prefab from the inspector. Volume is set the same for each object separately. Unlike the AudioListener volume setting, this allows you to adjust the volume of sounds and music separately. Save the object in the list of _sounds sounds in order to be able to adjust its volume and destroy it at the end.

The pausable parameter is needed to separate UI sounds and game sounds. The first must always be played and never paused. The second ones are suspended during the pause and continue when the game is resumed. This is done automatically using the soundSource.ignoreListenerPause flag, which for some reason is not available from the Inspector.

Next, we need a method for adding music to the game. In general, the code is similar to adding sound, but a different prefab is used (with other 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<AudioSource>(); 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, a single track is playing at the moment, so launching new music stops previous tracks automatically, so on each stage it is enough to call only SoundManager.PlayMusic (“MusicForCurrentScene”); In addition, when creating and stopping music, a smooth increase in volume and smooth extinction is added. This allows you to make the transition smooth and does not hit the ear. The very smooth change in volume can be done by Tween, but it can also be done with handles so that there are fewer dependencies.

Next we need the ability to pause. Since all sounds have already been tuned in, 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 pause.

  void Update() { if (AutoPause) { bool curPause = Time.timeScale < 0.1f; if (curPause != AudioListener.pause) { AudioListener.pause = curPause; } } } 


Next we need the methods of setting and getting the 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; } } 


It's simple. We save and read the settings using PlayerPrefs, when changing, we go over the sounds and use the new volume. Similarly, you can make the mute setting and all the same for music.

Well that's all. SoundManager, which is easy to use ready. Since we brought templates for sounds and music to prefabs, you can easily connect output from AudioMixer to them. In addition, there is a small class that simplifies calling 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:
+ Easy to use
+ Clean code and scene objects. No need to hang sound components anywhere, search and call them from code
+ Music that does not stop when the scene loads 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)
+ Support for the voice of the narrator (not mentioned in the article, but implemented in the full code)

Limitations of the current implementation (not yet available):
- While there is no positional 3d sound
- Changes the pitch of the sound so that multiple repetitions of the same sounds are not boring
- Loading sound when used can lead to lags (discreetly on small projects and small sounds)
- No volume control for a single sound.
- There are 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

Source: https://habr.com/ru/post/274529/


All Articles