Pull to refresh

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

Reading time 6 min
Views 30K
Воспроизвести звук в 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)
            {
                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);
    }


Теперь менеджер сам создаст себя на сцене, так что добавлять его самостоятельно не нужно(и не рекомендуется). Создается он по префабу, путь до которого прописан в коде, так что перемещать префаб не стоит. Можно создавать и с помощью new GameObject() и AddComponent() если хочется. UPD. Теперь создается не по префабу. Кроме того объект сразу помечается с помощью 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 (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);
    }


Для начала несколько проверок звука. Что он не пустой и что таких звуков не стало слишком много(Если где то в цикле по ошибке вызывается). После чего загружаем звук из ресурсов, ждем загрузки, создаем новый объект на сцену, добавляем 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
Tags:
Hubs:
+10
Comments 9
Comments Comments 9

Articles