Всем привет. Относительно недавно я написал статью Генерация окружения на основе звука и музыки в Unity3D, в которой привел несколько примеров игр, задействующих механику генерации контента на основе музыки, а так же рассказал про базовые аспекты подобных игр. В статье практически не было кода и я пообещал, что будет продолжение. И вот оно, перед вами. На этот раз мы попытаемся создать трассу для 2D гонки, в стиле Hill Climb, из вашей музыки. Посмотрим, что у нас получится..  Я напомню, что этот цикл статей рассчитан для начинающих разработчиков и для тех, кто только недавно начал работать с звуком. Если вы в уме делаете быстрое преобразование Фурье, то, вероятно, тут вам будет скучно.  Вот наш Road Map на сегодня: Итак, поехали! Как многие знают, чтобы использовать сигнал в цифровых системах, нам нужно его преобразовать. Один из шагов преобразования — это дискретизация сигнала, при котором аналоговый сигнал разбивается на части (временные отчеты), после которого каждому отчету присватывается то значение амплитуды, которое было в выбранный момент.
 Буквой Т обозначен период дискретизации. Чем меньше период, тем точнее будет преобразование сигнала. Но чаще всего говорят об обратной величине: Частотой дискретизации (логично, что это F = 1/T). Для телефонного сингала хватит 8 000 Гц, а, например, один из вариантов формата DVD-Audio требует частоты дискретизации 192 000 Гц. Стандарт в цифровой записи (в игровых редакторах, музыкальных редакторах) равняется 44 100 Гц — это частота CD Audio. Числовые значения амплитуды хранятся в так называемых сэмплах и именно с ними мы будем работать. Значение сэмпла float и оно может быть от -1 до 1. Упрощенно это выглядит вот так.  Волновая форма (или аудио-форма, а в простонародье — "рыба") — визуальное представление звукового сигнала во времени. Волновая форма может показать нам, в каком моменте звука происходит активная фаза, а где наступает затухание. Часто волновая форма представлена для каждого канала отдельно, например, вот так:  Представим что у нас уже есть AudioSource и скрипт, в котором мы работаем. Давайте разберемся, что нам может дать Unity.  Прежде чем мы пойдем дальше, нужно немного поговорить о глубине отрисовки нашего звука. При частоте дискретизации 44100 Гц в каждую секунду нам доступно для обработки 44100 отчетов. Допустим, нам нужно отрисовать трек длиной в 10 секунд. Каждый отчет мы будем рисовать линией в пиксель шириной. Получается, наша осциллограмма будет длинной 441000 пикселей. Получится очень длинная, вытянутая и мало понятная звуковая волна. Но, в ней вы сможете разглядеть каждый конкретный отчет! А еще вы страшно нагрузите систему, независимо от того, как вы это будете рисовать.  Если вы не делаете профессиональный аудио-софт, вам такая точность не нужна. Для общей аудио-картины мы можем разбить все семплы на более крупные периоды и брать, например, среднее по каждым 100 семплам. Тогда наша волна будет иметь вполне внятную форму:  Конечно же, это не совсем точно, так как вы можете пропустить пики громкости, которые вам могут быть нужны, поэтому можно попробовать не среднее значение, а максимальное из данного отрезка. Это даст немного другую картину, но ваши пики не пропадут. Давайте определим точность нашей выборки, как quality, а итоговое количествово отчетов, как sampleCount. Пример расчета всех цифр будет ниже. Далее нам нужно получить сами сэмплы. Это можно сделать из аудиоклипа, с помощью метода GetData. Этот метод принимает в себя массив, куда он записывает семплы. offsetSamples — параметр, который отвечает за начальную точку чтения массива данных. Если вы читаете массив с начала, то там должен быть ноль. Чтобы записать сэмплы, нам нужно подготовить для них массив. Например, вот так: Почему мы умножили длину на количество каналов? Сейчас расскажу... Многие знают, что в звуке мы обычно используем два канала: левый и правый. Кто-то знает, что есть системы 2.1, а так же 5.1, 7.1 в которых источники звука окружают со всех сторон. Тема каналов хорошо описана на вики. Как это устроено в Unity? При загрузке файла, при открытии клипа, можно найти вот такое изображение:  Тут как раз показано, что у нас есть два канала, и можно даже заметить, что они отличаются друг от друга. Unity записывает сэмплы этих каналов друг за другом. Получается вот такая картина:
 Именно поэтому нам нужно в два раза больше места в массиве, чем просто для количества сэмплов. Если вы выберете опцию клипа Force To Mono, то канал будет один и весь звук будет в центре. Превью вашей волны сразу поменяется.   Вот что у нас выходит: Итого, если трек идет 10 секунд и он двухканальный, то мы получаем следующее: В итоге, мы будем работать с 2000 точками, которых нам вполне хватит для отрисовки волны. Теперь нужно включать фантазию и думать, как мы можем использовать эти данные. Как многие знают, в Unity есть удобные средства для вывода разного рода Debug-информации. Толковый разработчик на основе этих средств может сделать, например, весьма мощные расширения для редактора. Наш случай показывает весьма нетипичное использование Debug-методов. Для отрисовки нам нужна линия. Её мы можем сделать с помощью вектора, который будет создан из значений нашего массива. Учтите, чтобы сделать красивую зеркальную аудиоформу, нам нужно "склеить" две половинки нашей визуализации. Далее просто используем Debug.DrawLine для отрисовки наших векторов. Цвет можете выбрать любой. Все эти методы нужно вызывать в Update, так мы будет обновлять информацию каждый кадр. Если хотите, можно добавить "бегунок", который будет показывать текущую позицию играемого трека. Эту информацию можно получить из поля "AudioSource.timeSamples".  Итого, вот наш скрипт: А вот результат:  Прежде чем приступить к данному разделу, хочу отметить следующее: кататься по сгенерированной из музыки трассе, конечно же, весело, но с точки зрения геймплея практически бесполезно. И вот почему: Поэтому, в качестве эксперимента, такой тип генерации довольно забавный, но реальную геймплейную фичу на его основе сделать сложно. В любом случае, продолжим. Итак, нам нужно сделать PolygonCollider2D с помощью наших данных. Это сделать просто. У PolygonCollider2D есть публичное поле points, которое принимает Vector2[]. Нужно для начала перенести наши точки в вектора нужного вида. Сделаем функцию для перевода массива наших сэмплов в векторный массив: После этого, просто передаем наш полученный массив векторов в коллайдер: Смотрим результат. Вот начало нашего трека… хм… выглядит не сильно проходимым (о визуализации пока что не думайте, комментарии будут позже).   У нас слишком резкая аудиоформа, поэтому трасса выходит странной. Нужно её сгладить. Здесь нам пригодится алгоритм скользящего среднего. Более подробно про него можно прочитать на Хабре, в статье Алгоритм скользящего среднего (Simple Moving Average). В Unity алгоритм реализуется следующим образом: Модифицируем наше создание пути: Проверяем...  Теперь наша трасса выглядит вполне нормально. Я использовал ширину окна равную 10. Вы можете модифицировать этот параметр, чтобы подобрать нужное вам сглаживание. Вот полный скрипт данного раздела: Как я уже говорил в начале раздела, при таком сглаживании мы перестаем чувствовать трек, кроме того, скорость машинки не привязана к скорости музыки (BPM). Эту проблему мы разберем в следующей части данного цикла статей. Кроме того, там же мы затронем тему спец. эффектов под бит. Машинку я, кстати, взял из этого бесплатного ассета. Наверное, многие из вас, глянув на скрины, задались вопросом, как я нарисовал саму трассу? Ведь коллайдеров не видно.  Я воспользовался мудростью интернета и нашел способ, с помощью которого вы можете превратить полигон коллайдер в меш, которому вы можете присвоить любой материал, а line renderer сделает стильный контур. Подробно этот способ описан вот тут. Triangulator вы можете взять на Unity Community. То, что мы с вами изучили в этой статье — это базовый набросок для музыкальной игр. Да, в таком виде он, пока что, немного неказистый, но вы уже можете смело сказать "Ребята, я сделал так, чтобы машинка ездила по аудио-дорожке!". Чтобы сделать из этого реальную игру, нужно приложить много сил. Вот список того, что мы можем тут сделать: Все это и многое другое будем изучать в остальных частях данного цикла статей. Всем спасибо за чтение! Head of Infrastructure CDEK