Воспроизвести звук в Unity просто. Нужно создать компонент AudioSource прикрепить к нему звуковой файл в виде AudioClip и вызвать audioSource.Play() из скрипта. Или даже поставить автовоспроизведение на при создании объекта(Play on Awake).
Сложности начинаются когда звуков в игре становится много. Их все нужно расставить, прописать приоритеты. Звуки отдельно, музыку отдельно. При регулировке громкости звуков и музыки раздельно тоже сложности. Можно, конечно, регулировать громкость разных каналов в AudioMixer, но он не работает в WebGL. А Webplayer сейчас считается устаревшим.
А если какой то звук повторяется несколько раз подряд(например игрок быстро нажимает на кнопку и играет звук клика), то хорошо бы чтобы тот не обрывался на середине, а начинался новый, не мешая предыдущим. Да еще и при включении паузы звуки игры нужно ставить на паузу, а звуки меню нет. Из коробки такая возможность в Unity есть, но почему то доступна только из скрипта и не все о ней знают.
В общем хочется простой и удобный SoundManager, создание которого я и опишу. Для крупных проектов он не подойдет, а вот для прототипов и небольших игр вполне.
Итак что же должен представлять собой SoundManager? Ну во первых им должно быть удобно пользоваться. То есть никаких «найти объект на сцене», «присоеденить компонент» и прочего для пользователя, все внутри. Так что сразу делаем его синглтоном(Код сокращен, чтобы выделить суть).
private static SoundManager _instance;
public static SoundManager Instance
{
get
{
if (_instance == null)
{
_instance = (SoundManager)FindObjectOfType(typeof(SoundManager));
if (_instance == null)
{
GameObject singleton = (GameObject)Instantiate(Resources.Load<GameObject>(PrefabPath));
_instance = singleton.GetComponent<SoundManager>();
singleton.name = "(singleton) " + typeof(SoundManager).ToString();
DontDestroyOnLoad(singleton);
}
}
return _instance;
}
}
Теперь менеджер сам создаст себя на сцене, так что добавлять его самостоятельно не нужно(и не рекомендуется). Создается он по префабу, путь до которого прописан в коде, так что перемещать префаб не стоит. Можно создавать и с помощью new GameObject() и AddComponent() если хочется. Кроме того объект сразу помечается с помощью DontDestroyOnLoad. Нужно это для того чтобы музыка и звуки продолжали играть без перебоем при перезагрузках сцен.
Теперь к любым методам можно обращаться просто написав SoundManager.Instance.Method(). Чтобы еще немного сократить эту запись для всех методов я дописал статический враппер:
public static void PlayMusic(string name)
{
Instance.PlayMusicInternal(name);
}
Так что писать можно даже еще короче SoundManager.Method().
Объект есть, работать с ним удобно. Дальше добавляем функционал. Самая необходимая функция это 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 (_sounds.Count > 16) {
Debug.Log("Too much duplicates for sound: " + soundName);
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);
}
Для начала несколько проверок звука. Что он не пустой и что таких звуков не стало слишком много(Если где то в цикле по ошибке вызывается). После чего загружаем звук из ресурсов, ждем загрузки, создаем новый объект на сцену, добавляем AudioSource, настраиваем его и запускаем. Функция LoadClipAsync запускает асинхронную загрузку звукового файла из ресурсов по имени. Так что файл надо будет положить в папку «Resources/Sounds/Sounds». Создание объекта происходит по префабу, который загружен из ресурсов. Так что часть параметров(вроде приоритета звука), можно установить префабу из инспектора. Громкость устанавливается так же у каждого объекта отдельно. В отличие от установки громкости AudioListener-у это позволяет регулировать громкость звуков и музыки раздельно. Сохраним объект в списке звуков _sounds, чтобы иметь возможность регулировать его громкость и уничтожать по окончанию.
Параметр pausable нужен чтобы разделить UI звуки и игровые звуки. Первые должны играться всегда и никогда не ставиться на паузу. Вторые приостанавливаются во время паузы и продолжаются при возобновлении игры. Делается это автоматически с помощью флага soundSource.ignoreListenerPause, который почему то недоступен из Inspector-а.
Далее нам нужен метод для добавления музыки в игру. В целом код похож на добавление звука, но используется другой префаб(с дургим приоритетом и настройкой loop).
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;
}
В большинстве неболших проектов достаточно одного трека проигрывающегося в данный момент, так что запуск новой музыки останавливает предыдущие треки автоматически, так что на каждой сцене достаточно вызвать лишь SoundManager.PlayMusic(«MusicForCurrentScene»); Кроме того при создании и остановке музыки добавляется плавное нарастание громкости и плавное угасание. Это позволяет сделать переход плавным и не бьет по слуху. Само плавное изменение громкости можно делать Tween-ом, но можно и ручками, чтобы было меньше зависимостей.
Дальше нам нужна возможность поставить паузу. Так как у всех звуков уже проставлена настройка ставятся ли они на паузу при паузе AudioListener-а, то методы получаются очень простыми.
public static void Pause()
{
AudioListener.pause = true;
}
public static void UnPause()
{
AudioListener.pause = false;
}
Либо можно настроить автоматическое включение паузы.
void Update()
{
if (AutoPause)
{
bool curPause = Time.timeScale < 0.1f;
if (curPause != AudioListener.pause)
{
AudioListener.pause = curPause;
}
}
}
Дальше нам потребуются методы установки и получения громкости.
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;
}
}
Тут все просто. Сохраняем и читаем настройки с помощью PlayerPrefs, при изменении пробегаемся по звукам и применяем новую громкость. Аналогично можно сделать настройку mute и все тоже самое для музыки.
Ну вот и все. SoundManager, которым легко пользоваться готов. Так как мы вынесли шаблоны для звуков и музыки в префабы, то к ним легко можно подключить output из AudioMixer-а. Кроме того есть еще небольшой класс, упрощающий вызовы нужных методов из анимаций, обработчиков кнопок и пр, чтобы не нужно было писать скрипт из-за одной строчки.
Плюсы полученного менеджера:
+ Простота в использовании
+ Чистый код и объекты сцены. Не нужно вешать компоненты звука нигде, искать и вызывать их из кода
+ Музыка, которая не прерывается при загрузке сцены и меняется плавно
+ Геймплейные и UI звуки
+ Поддержка паузы
+ Поддержка AudioMixer
+ Работа на всех платформах, включая не поддерживающие AudioMixer (например WebGL)
+ Поддержка голоса рассказчика(в статье не упомянуто, но в полном коде реализовано)
Ограничения текущей реализации(Пока нету):
— Пока нет позиционного 3d звука
— Изменения pitch-а звука, чтобы много кратное повторение одинаковых звуков не приедалось
— Загрузка звука при использовании может приводить к лагам(Незаметно на мелких проектах и небольших звуках)
— Нет регулировки громкости отдельно взятого звука
— Нет зацикленных звуков, вроде амбиента
Полный код менеджера можно посмотреть на моем GitHub-е:
https://github.com/Gasparfx/SoundManager
Наш проект использующий этот менеджер на GreenLight:
http://steamcommunity.com/sharedfiles/filedetails/?id=577337491
Автор: Gasparfx