Pull to refresh

Unity3D — кроссфейд, основы работы со звуком (урок)

Reading time 6 min
Views 21K

картинка для привлечения внимания:
«Acorn» — игра, над которой мы работаем,
в ней используется скрипт из этого урока.


В уроке быстро и просто реализуем примерно три задачи:

  • 1а. Плавное затухание громкости эмбиента (или саундтрека) предыдущего уровня при переходе на следующий.
    1б. Далее, этот «звук» удаляется через заданное количество времени.
  • 2. Плавное возникновение (усиление громкости от 0 до 1) эмбиента после загрузки уровня.
  • 3. Кроссфейд эмбиента с самим собой — звуки/саундтрек уровня, за ~10 секунд до своего финала, должны плавно затухать и плавно переходить с усилением в собственное начало.
    Другими словами — «программное» плавное смешивание (луп/микширование/закольцованность, etc.) на лету.

Что из себя представляет кроссфейд...
… как это понимаю «дилетант я»?

Вот, например, скриншот из Adobe Audition.

image

Берём трек (0), делаем возрастание громкости вначале (1) и затухание в конце (2),
клонируем результат в начало (3) и в конец (4), со смещением до перекрестия затуханий (5), обрезаем лишнее (6).
7 — Сохраняем результат.

Мы теряем немного материала в начале и в конце, обрезанные, хм… начало и конец «загрязняются» примесью друг друга.
Как-то так, можно, конечно и по-другому.
Например ,«срезать» полностью вначале или в конце, оставив эм… перекрестие затуханий в противоположном начале или в конце.
Но это «съест» ещё больше материала.

Почему я делаю это программно?

  • 1. Потому что лень. Достаточно было написать скрипт один раз, чтобы забыть об Audacity и Adobe Audition
  • 2. Исходник сохраняется в первозданном виде — без «съедания» начала (переходом в конец),
    без искажений в начале и в конце.
  • 3. Я могу сделать переход на любом отрезке трека, а если допилить скрипт, то и микшировать несколько треков —
    все это без издевательства над аудио- материалом в аудио-редакторах и без лишней траты времени.
  • 4.Традиционный способ ограничивает выбор форматов — луп из *mp3 будет «цокать» в момент перехода.
    Мой способ лишён этого недостатка.
  • 5. Программы вроде Audacity, извините, жутко неудобны. Программы вроде Adobe Audition — стоят денег.

Немного необязательной информации под спойлером.

Микширование в Unity 5.0 через AudioMixer это, наверное, круто.
Но нужно было собственное решение здесь и сейчас (на момент написания урока — версия 4.5.2f1).
Итак, начнём по порядку.


0. Подготовительные работы

  • 1. Создаём проект, сохраняем.
  • 2. В проекте создаём две сцены, сохраняем. Для удобства, можно назвать их просто — 0 и 1
  • 3. Перетаскиваем созданные сцены в окошко Build Settings (Ctrl+Shift+B).
  • 4. Перетаскиваем в окошко Project два музыкальных файла — для первой и второй сцены.
    Желательно *wav — при необходимости его всегда можно сжать в опциях инспектора Unity. Ну, или *ogg.
    В какие папки и как разместить эти файлики — это на ваш вкус.
    Также, рекомендую для урока брать музыку покороче — чтобы не ждать момента закольцованности слишком долго.
    В принципе, это можно обойти (выставить время для перехода раньше) но для точности лучше так.
  • 5. Настраиваем наши музыкальные файлы: снимаем галочку чекбокса 3D Sound — в нашем случае громкость саундтрека/эмбиента не зависит от положения игрока с пространстве относительно источника звука.
    Перетаскиваем один из звуковых файлов в первую сцену и другой во вторую.
    У камеры (или персонажа) должен быть Audio Listener


1. Плавное затухание эмбиента предыдущего уровня при переходе на следующий.
Последующее удаление через заданное количество времени


Создаём новый js скрипт, люблю дурацкие названия — назову его FadeOutAndDestrTimerWhenNextLvlLoad

Первое, что нам нужно — чтобы объект «звук» не уничтожился при переходе на следующий уровень.
Зачем? Затем, чтобы сделать ему плавное затухание и затем удалить.

Лезем читать документацию.

Ага, нам нужна строчка:
DontDestroyOnLoad (transform.gameObject);
Всё, этого достаточно.

Исходя из наших требований, обдумываем три «изменябельных» переменных:

  • продолжительность затухания звука
  • время от момента загрузки уровня до удаления объекта «звук»
  • номер уровня, на котором скрипт запустится —
    нужная штука, ведь скрипт универсальный — мы будем применять его на разных уровнях.

Конвевертируем мысли в строчки:

    var fadeTime = 0;
    var levelToExecute = 0;
    var DestroyTime = 0;

Ага, значит далее скрипт будет состоять из двух функций — «таймер удаления» и «механизм затухания».

Начнем с «таймера удаления».
Тут все элементарно:

    function OnLevelWasLoaded (level : int) {
        if (level == (levelToExecute))
        {
            Destroy (gameObject, DestroyTime);
            //Destroy (gameObject, 20); //- можно и так, конечно.
        }
    }

Если номер загруженного уровня будет соответствовать указанному нами в окошке «levelToExecute», запускается «таймер удаления» — от момента загрузки уровня до времени, указанного нами в окошке DestroyTime.

Хм, а почему бы нам не добавить сразу и запуск (ещё не написанной) функции «механизм затухания» сюда же? Можно.

    function OnLevelWasLoaded (level : int) {
        if (level == (levelToExecute))
        {
            FadeAudio(fadeTime, Fade.Out); 
            //продолжительность затухания, тип фейда - затухание(а не усиление)
            Destroy (gameObject, DestroyTime);
        }
    }

Теперь осталось написать функцию «механизм затухания», допустим такой метод:

    function FadeAudio (timer : float, fadeType : Fade) {
        var start = fadeType == Fade.In? 0.0 : 1.0; //на старте (указанного нового уровня) произойдёт усиление звука из 0 до 1
        var end = fadeType == Fade.In? 1.0 : 0.0;   //в конце произойдёт угасание из 1 в 0
        var i = 0.0;                                //переменное значение (громкости)
        var step = 1.0/timer;                       //шаг ("плавного" изменения громкости) равен единице громкости / таймер
     
        while (i <= 1.0) {                          // до тех пор, ПОКА "0" (громкость) равна или меньше "1" исполнять ↓ , 
                                                    //вплоть до получения значения "0"
            i += step * Time.deltaTime;
            audio.volume = Mathf.Lerp(start, end, i); 
// Mathf.Lerp - находим промежуточные значения громкости соответственно имеющимся start, end, и значение "i"
//растягиваем во времени изменение звука
            yield;
        }
    }

Вот, что у нас получилось:



2,3 «Программный» луп эмбиента с самим собой, плавное усиление эмбиента при загрузке уровня

Мы уже знаем почти все, что нужно для этой части.
Добавим чуть-чуть.

Создаём новый js скрипт, я назову его StartFadeIn_CloneTimer_FadeOut_DestrTimer
Обратите внимание — название не просто дурацкое, в нем указана последовательность исполнения функций, мне так удобно.
Кстати, изначально я написал п.1 и п.2-3 в один скрипт, но оказалось удобнее отключать «кусочек» при потребности.
Можете их объединить, чуть подкорректировав.

Оглянемся назад, в начало статьи — какие требования предъявлены будущему скрипту?
Музыка, за ~N секунд до своего финала, должна начать плавно затухать и плавно переходить с усилением в собственное начало.

Подумаем как это реализовать:

  • 1. Фейд в начале трека, «нерегулируемый», происходит усиление громкости от 0 до 1.
    Он должен срабатывать как на старте уровня, так и у клона (об этом ниже).
  • 2. Фейд в конце трека — происходит затухание громкости от 1 до 0,
    активируется за 10 (например) секунд до финала трека.
    Функция «нерегулируемая», но не совсем — не напрямую. Об этом ниже.
  • 3. Уничтожение объекта «трек» — регулируемая цифра,
    по-умолчанию равна длине трека в секундах, но не обязательно.
    Мы вольны уничтожить трек в любой момент, не дожидаясь конца.
    Скрипт автоматически запустит фейд затухания за 10 секунд до уничтожения,
    соответственно этой цифре.
  • 4. Регулируемая продолжительность фейда.
    Согласно написанному выше это 10с,
    но мы оставим возможность при надобности ее изменять.
  • 5. Создание клона трека — регулируемая цифра,
    она равна длине трека в секундах, минус 10 секунд затухания.
  • 6. Поскольку на разных уровнях разные треки, сделаем скрипт универсальным —
    добавим возможность выбирать нужный нам объект «звук».

Конвертируем мысли в строчки, опять же, перечислим, что у нас здесь есть затухание и усиление, и создадим четыре переменных:

    enum Fade {In, Out}             // для объявления перечисления - наших "усиление" и "затухание"

    var  prefab :     Transform;    //выбор объекта "звук"
    var  DestroyTime: float = 1;    //время (задержка) до удаления
    var  CloneTime:   float = 1;    //время (задержка) до клонирования
    var  fadeTime:    float = 1;    //продолжительность угасания/усиления

Добавим функцию «Старт»:

    function Start(){
    Spawn(); 
    //↑запуск функции "Спавн" (клонирование)
    FadeAudio(fadeTime, Fade.In ); 
    //↑запуск функции "Фейд", включая указанную в переменной продолжительность и тип фейда - усиление
	Destroy ((prefab as Transform).gameObject, DestroyTime); 
    //↑удаление выбранного объекта звук по истечению времени, указанного в DestroyTime
    }

Теперь по порядку, добавим перечисленные выше функции. Клонирование:

    function Spawn(){
    while( true ){
        yield WaitForSeconds(CloneTime); //задержка до исполнения, указана в переменной CloneTime
    	   for (var i : int = 0;i < 1; i++) 
           { Instantiate (prefab, Vector3(i * 2.0, 0, 0), Quaternion.identity); } //клонирование выбранного объекта "звук" 
           FadeAudio(fadeTime, Fade.Out); //продолжительность затухания, срабатывающего в конце
           
    }
    }

Функция «Фейд» у нас уже есть — копипастим её из первой части статьи:

    function FadeAudio (timer : float, fadeType : Fade) {
        var start = fadeType == Fade.In? 0.0 : 1.0;
        var end = fadeType == Fade.In? 1.0 : 0.0;
        var i = 0.0;
        var step = 1.0/timer;
     
        while (i <= 1.0) {
            i += step * Time.deltaTime;
            audio.volume = Mathf.Lerp(start, end, i);
            yield;

        Debug.Log (audio.volume); 
        //проверка срабатывания фейда логируется в консоль - можно удалить/отключить
        }
    }

Вот, что у нас получилось:



Смотрим длину нашего трека, конвертируем её в секунды = DestroyTime
Если лень посчитать — можно использовать онлайн-конвертер минут (гугл в помощь) + добавить остаток секунд.
Отнимаем продолжительность фейда = CloneTime
Вписываем полученные значения в окошки.
В перфаб добавляем нужный трек.
Фейд по вкусу.

Вы должны проследить за тем, чтобы задержка до клонирования/продолжительность фейда/время удаления
не конфликтовали с временнЫми параметрами скрипта из первой части статьи
иначе при переходе на следующий уровень трек может не успеть самоудалиться и начнёт создавать множество своих копий.


И напоследок, если вам интересно, для чего это делается:

steamcommunity.com/sharedfiles/filedetails/?id=252305314

Tags:
Hubs:
+10
Comments 4
Comments Comments 4

Articles