Sound Manager для небольших игр и прототипов на Unity

от автора

Воспроизвести звук в 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

ссылка на оригинал статьи http://habrahabr.ru/post/274529/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *