Разработка ритм-игры TERRORHYTHM. "Под капотом", или заметки программиста

TERRORHYTHM 'Main menu
В закладки

Первый вопрос, который задает себе разработчик, планирующий сделать ритм игру - “А будет ли реально ритм влиять на геймплей?”. Если ответ утвердительный, и разработчику мало простого визуального отклика пары эффектов на сцене, то в самое ближайшее время возникает второй вопрос - “Хочу ли я давать игроку загружать свою собственную музыку?”. Конечно, этот вопрос может возникать, даже если вы делаете просто интерактивную музыкальную сцену, но по-настоящему критичным он становится только для тех, кто привязывает геймплей к музыке. Ведь дать пользователю возможность загружать желаемые трэки - значит вложиться в разработку анализатора и генератора игрового уровня именно по этому трэку, а также проводить объемное тестирование на большом количестве различной музыки, сопровождаемое всесторонней отладкой. А переложить создание уровней на самих игроков и пользователей через Steam Workshop - идея сомнительная, если не имеешь устоявшегося сообщества.

Работая над игрой TERRORHYTHM, мы понимали, что программный анализ звука - задача хоть и популярная, но все же нетривиальная, и взяться за нее небольшой студией - дело ответственное и в чем-то даже рискованное. И все же мы не решились отказаться от реализации это killer-фичи, дать пользователю кромсать мобов в ритм его любимой композиции.

Ритм - вот центр нашего геймплея. Тот самый пресловутый BPM (beats per minute), с частотой которого вы качаете головой, или барабаните пальцами по столу под драйвовый трэк. Но чтобы строить игру вокруг ритма, его сначала нужно добыть. Музыка для игрового движка - это по сути всего лишь пачка сэмплов, которые этот движок умеет воспроизводить своими силами, плюс немного информации о них.

Задача извлечения из музыки BPM не новая, даже если игнорировать сегмент ритм-игр, производители DJ-оборудования и программных секвенсеров музыки проделали огромную работу в этом направлении. И даже после всех этих исследований производители часто доверяют определение темпа человеку.

стучим по кнопочке CUE в ритм песни, чтобы “вертушка” поймала его

Мы изучили множество публикаций и материалов, прежде чем пришли к какому-то промежуточному решению, которое сработало для нас.

Музыка - сложная звуковая волна, колебания воздуха в вашей комнате, в клубе, или же между мембраной ваших наушников и барабанной перепонкой. Преобразование этих колебаний в нечто мелодичное и ритмичное (то, что наш мозг делает автоматически), требует определенных вычислений. В большинстве своем эти вычисления - быстрое преобразование Фурье, которое представляет комплексный звуковой сигнал в виде суммы синусоид определеных частот и громкостей. Для тех, кому сложно с ходу понять, как кривая связана со звуком, поясним: звуковая волна синусоидальной формы - это простейший музыкальный тембр, знакомый каждому, например, по телефонным гудкам. Частота этой синусоиды - это высота звука, а амплитуда - его громкость. Для наглядности приведем разложение на синусоиды простейших форм звуковой волны:

Конечно, для музыки спектр получаемых частот выглядит куда сложнее, практически по всему спектру есть частоты разной громкости. Хотя, если вы вспомните любой музыкальный спектроанализатор (люди часто ошибочно называют это “эквалайзер”), объяснения не потребуются - это наглядная картина того, во что превращается звук после быстрого преобразования Фурье :)

громкости звучания частот от низких к высоким слева направо

Так вот благодаря игровому движку, который сделал это преобразование за нас, мы можем работать с музыкой, как с массивом сэмплов (количество которых на секунду зависит от качества и сжатия записи), из которых в каждый момент времени мы можем понять - какие частоты сейчас звучат и с какой амплитудой.

Используя эти данные нам нужно решить несколько задач. Задача первая и главная - определение BPM песни. К ритму композиции привязаны почти все внутриигровые параметры - от скорости движения и спавна монстров до скорости проигрывания отдельных анимаций и роста комбо-счетчика.

Чтобы определить BPM, нужно сначала понять, что такое бит. Обычно под битом мы понимаем начало короткого повторяющегося музыкального отрезка, зацикливание которого (с небольшими изменениями) задает основной темп песни. Классической основой ритм-секции является сочетание ударных и баса (будь то рок, металл, джаз или электронная музыка). Знание об этом дает нам примерное представление, пики громкости каких частоты мы будем искать в нашей музыке. Наша цель основная цель - диапазон до 120Гц, однако помимо него мы проанализируем еще небольшую полосу в районе 1кГц, где часто “сидят”, например, электро-гитары, поддерживающие ритм секцию.

Во-первых нам нужно понять, в каком диапазоне громкости звучат данные чистоты на протяжении песни, чтобы делать какие-то относительные выводы. Получив значения минимальной и максимальной громкостей, можно приступать к анализу. Если говорить коротко, анализ представляет собой последовательный проход по сэмплам от начала песни к ее концу, сравнивая значения частот на выбранном диапазоне с минимальными\максимальными значениями и между собой. Составив своеобразную “карту перепадов громкости низких частот”, мы стараемся выявить периодические последовательности - пики, повторяющиеся через равные промежутки времени (к счастью для меломанов и к сожалению для анализирующих звук, басовые и ударные партии могут быть сложными и разнообразными). Наиболее часто встречающееся значение такого промежутка мы берем в качестве длительности бита, от которого уже вычисляем BPM.

Анализатор реагирует по-разному на разные песни - разумеется, хаус-музыка, где бас-бочка отбивает четкий постоянный ритм, проще всего поддается анализу. С агрессивными жанрами металла, где барабанщики вытворяют невиданной быстроты и сложности финты, ситуация обратная. Тут как раз нам помогает дополнительная полоса частот, в которой сидят гитары и “рабочий барабан” (самый пронзительный и звонкий из барабанов установки). К тому же не стоит забывать про музыкальные жанры с плавающей размерностью, где музыкальные фразы могут быть разного размера и длины - джаз, матрок и другие.

Еще одним препятствием анализу является компрессия звука. Этот прием пост-обработки музыки сейчас применяется многими продюсерами в различных жанрах. Суть его такова, что разницу между самыми тихими и самыми громкими звуками (т.н. динамический диапазон) в песне намеренно сокращают с целью без потери качества и “перегрузки” оборудования на пиках увеличить конечную громкость записи. Очевидно, что экстремальная компрессия (которой иногда грешат поп и электро продюсеры) может значительно усложнить составление “карты перепадов громкости” и сделать конечный результат анализа слишком неточным.

Для того, чтобы создать еще одну опору алгоритму определения BPM, нами была добавлена синхронизация в процессе проигрывания музыки. Предварительный анализ трэка давал нам усредненные значения, которые мы брали за основу. В соответствии с этими значениями мы устанавливали время бита и запускали главный таймер игры. В процессе воспроизведения музыки мы периодически синхронизируем этот таймер с составленной ранее “карты перепадов”. Если отклонение большое - следующая синхронизация назначается через короткое время и в геймплей вводятся некоторые послабления, чтобы игрок не получал штрафы из-за технической ошибки анализа, если отклонение небольшое - значит все идет хорошо и следующую синхронизацию можно провести позже.

Добавив к этим основам еще несколько тонкостей анализа и синхронизации, мы получили относительно рабочую версию механизма, улавливающего бит песни. Теперь можно и пошаманить над интерактивностью уровня - спавн более сложных цепочек мобов в наиболее напряженные моменты песни, красивый отклик сцены и т.п.

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

Страница игры в Steam: http://store.steampowered.com/app/752380

С уважением,

EvilCoGames Team

evilcogames@gmail.com

{ "author_name": "EvilCorporationGames", "author_type": "self", "tags": [], "comments": 4, "likes": 16, "favorites": 1, "is_advertisement": false, "subsite_label": "flood", "id": 13866, "is_wide": false }
{ "id": 13866, "author_id": 34888, "diff_limit": 1000, "urls": {"diff":"\/comments\/13866\/get","add":"\/comments\/13866\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/13866"}, "attach_limit": 2, "max_comment_text_length": 5000 }

4 комментария 4 комм.

Популярные

По порядку

Ann Che

1

Интересно... Не очень поняла, как работает синхронизация таймера игры при отклонениях в самой композиции. Ну понятно, если брать незамысловатые танцевальные треки - тут легко. А как быть, если я загрузила мелодию со множеством ударных рисунков, или основной ритм которой сам меняется, ну, к примеру Bohemian Rhapsody?

Ответить

EvilCorporationGames

Ann
0

Процент распознанного постоянного ритма в песне должен быть не менее 70%, чтобы загрузка прошла успешно. Т.е. поиграть под джазовые импровизации с меняющимся темпом или Bohemian Rhapsody не получится (если, конечно, вы не решите отрезать от последней собственно ритмичную часть) В процессе воспроизведения мы следим за отрисованной заранее картой пиков. Это дает возможность перезапускать наш главный таймер бита (настроенный на основной bpm) в тех случаях, когда в композиции встретилась короткая пауза\проигрыш, не соответствующая основному размеру песни, а потом ритм снова возвращается. Так же это помогает правильно запустить таймер в самом начале, ведь далеко не во всех композициях основной ритм начинается с первой секунды аудиофайла.

Ответить

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

Комментарий удален

0

Прямой эфир

Подписаться на push-уведомления
[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } } ]