[Перевод] Синхронизация ритма в музыкальных играх

image

Недавно я начал работу в Unity над битбоксовой музыкальной игрой Boots-Cut. В процессе прототипирования базовых механик игры я обнаружил, что довольно сложно правильно синхронизировать ноты с музыкой. В Интернете по этой теме нашлось довольно мало статей. Поэтому в своей статье я постараюсь дать наиболее важные подсказки по разработке музыкальной игры (особенно в Unity).

Выяснилось, что самыми важными являются следующие три аспекта:

  • Использование AudioSettings.dspTime вместо Time.timeSinceLevelLoad для отслеживания позиции в песне.
  • Нужно всегда использовать позицию в песне для обновления движений.
  • Не обновляйте ноты в каждом кадре по разнице во времени, интерполируйте их.

Учтём это и приступим к работе!

Основной класс


Необходимо создать класс SongManager для отслеживания позиции в песне, создания нот и других функций управления песней.

Отслеживание позиции


Во всех музыкальных играх нужно отслеживать позицию в песне, чтобы знать, какую ноту следует создать. Ниже представлены поля, необходимые для отслеживания позиции в песне:
//текущая позиция в песне (в секундах)
float songPosition;

//текущая позиция в песне (в ударах)
float songPosInBeats;

//длительность удара
float secPerBeat;

//сколько времени (в секундах) прошло после начала песни
float dsptimesong;

Мы инициализируем эти поля в функции Start():
void Start()
{
    //вычисление количества секунд в одном ударе
    //объявление bpm выполняется ниже
    secPerBeat = 60f / bpm;
    
    //запись времени начала песни
    dsptimesong = (float) AudioSettings.dspTime;

    //начало песни
    GetComponent().Play();
}

Для удобства мы преобразуем bpm в secPerBeat. Позже secPerBeat будет использоваться для вычисления позиции в песне в ударах, что очень важно для создания нот.

Кроме того, мы записываем время начала песни в dsptimesong. Мы используем AudioSettings.dspTime вместо Time.timeSinceLevelLoad, потому что Time.timeSinceLevelLoad обновляется только в каждом кадре, а AudioSettings.dspTime обновляется чаще, так как это таймер аудиосистемы. Чтобы сохранять темп песни, нужно использовать таймер аудиосистемы. Таким образом нам удастся избежать задержки, вызванной разницей во времени между обновлениями кадров и обновлениями аудио.

В функции Update() вычисляется позиция в песне с помощью AudioSettings.dspTime:

void Update()
{
    //вычисление позиции в секундах
    songPosition = (float) (AudioSettings.dspTime - dsptimesong);

    //вычисление позиции в ударах
    songPosInBeats = songPosition / secPerBeat;
}

Мы вычисляем позицию в секундах вычитанием из текущего AudioSettings.dspTime времени начала песни (dsptimesong). Мы получили позицию в секундах, однако в мире музыки ноты записываются в ударах. Поэтому лучше преобразовать позицию в секундах в позицию в ударах. Разделив songPosition на secPerBeat (секунда / (секунда/удар)), мы получим позицию в ударах.

Посмотрите на рисунок:

badb73a8d2dce27857a213f516d3f28e.png

Позиция нот в ударах: 1, 2, 2.5, 3, 3.5, 4.5, а длительность удара — 0,5 с. Поэтому, если после начала песни прошло 1,75 с (songPosition == 1.75), то мы знаем, что находимся в позиции 1.75 (songPosition) / 0.5 (secPerBeat) = 3.5 удара, и необходимо создать ноту удара 3.5.

Информация о песне


Перейдём к полям, в которые мы записали информацию о песне:
//количество ударов в минуту
float bpm;

//сохранение всех позиций нот в ударах
float[] notes;

//индекс ноты, которую нужно создать следующей
int nextIndex = 0;

Для простоты я демонстрирую песню только с одной дорожкой нот (в Guitar Hero Mobile сделано три дорожки, а в Taikono Tatsujin — всего одна).

bpm — это количество ударов в минуту. Как мы видели, для удобства они преобразуются в secPerBeat.

notes — это массив, в котором хранятся все позиции нот в ударах. Например, для представленных на рисунке нот массив notes будет содержать {1f, 2f, 2.5f, 3f, 3.5f, 4.5f}:

badb73a8d2dce27857a213f516d3f28e.png

И, наконец, nextIndex — это целое число, нужное для обхода массива. Оно инициализируется со значением 0, потому что следующая создаваемая нота будет первой нотой песни. При создании ноты счётчик nextIndex увеличивается на единицу.

Создание нот


Мы определяем, должна ли создаваться нота, в функции Update(). Однако сначала нужно определить, сколько ударов будет показываться заранее.

Например, для следующей дорожки:

d255bb692bb4f0dfbef2c9b479d29883.png

текущая позиция в ударах равна 1, но удар 3 уже создан. Это означает, что заранее показываются 3 удара.

Добавим под songPosInBeats = songPosition / secPerBeat;, следующие строки:

if (nextIndex < notes.Length && notes[nextIndex] < songPosInBeats + beatsShownInAdvance)
{
    Instantiate( /* префаб ноты */ );

    //инициализация полей ноты

    nextIndex++;
}

Сначала нужно проверить, не осталось ли нот в песне (nextIndex < notes.Length). Если ноты ещё остались, то мы проверяем, достигла ли песня удара, при котором должна создаваться следующая нота (notes[nextIndex] < songPosInBeats + beatsShownInAdvance). Если достигла, создаём ноту и увеличиваем nextIndex, чтобы отслеживать следующую ноту, которую нужно создать.

Движение нот


Наконец, поговорим о том, как перемещать созданные ноты в соответствии с темпом песни. Это довольно просто, если вспомнить пункт «Не обновляйте ноты в каждом кадре по разнице во времени, интерполируйте их».

Всегда обновляйте движение по позиции в песне, потому что:

  1. Таймер аудиосистемы имеет разницу во времени с таймером кадров
  2. Удары могут находиться ровно посередине двух кадров (что приводит к разнице во времени)

Итак, как же двигать ноты? Интерполяцией!

Для упрощения я вырежу весь код в классе MusicNote и оставлю только функцию Update(), в которой мы двигаем каждую ноту:

//функция обновления нот
void Update()
{
    transform.position = Vector2.Lerp(
        SpawnPos,
        RemovePos,
        (BeatsShownInAdvance - (beatOfThisNote - songPosInBeats)) / BeatsShownInAdvance
    );    
}

На представленной ниже схеме это чётко видно:

dadfdc8a564468007831e6bd98ad4ca2.png

Заключение


Я рассказал об основах программирования музыкальной игры. Следуя этим принципам, можно создавать игры с синхронизацией. В играх с несколькими дорожками можно создавать вложенные массивы notes, удаление нот выполняется проверкой позиции относительно линии удаления, ноты большой длительности реализуются отслеживанием начального и конечного удара, и т.д.

Благодарю за прочтение статьи, надеюсь, она будет полезна. Моя собственная музыкальная игра Boots-Cuts будет готова в следующем году, следите за информацией.

Комментарии (5)

  • 25 марта 2017 в 10:40

    0

    Спасибо за статью, и сразу вопрос) А что если в песне bpm будет меняться?
  • 25 марта 2017 в 14:12

    0

    Еще одно.
    Делайте в настройках регулируемую задержку между звуком и изображением, в идеале небольшой кнопочкой перед запуском уровня. Т.к. человек в разном состоянии имеет разную скорость реакции «глаз/ухо-мозг-обработка-руки», то при полностью синхронном выводе изображения и звука обязательно получится ощущаемый рассинхрон.
  • 26 марта 2017 в 10:22

    0

    Я так и не понял, как из аудиофайла вычислить ноты и bps.

    • 26 марта 2017 в 17:53 (комментарий был изменён)

      0

      Я увы тоже (
      Но это лишь перевод…
      Сам дошёл лишь до «эквалайзер вид сверху»…
    • 26 марта 2017 в 21:01

      0

      Если в кратце, то никак:)

      В ритм играх (допускаю, что конкретно в Guitar Hero это может быть не точно так, как написал), как правило, есть аудиотрек и так называемый simfile — мета-файл в котором и описаны:

      • GAP (OFFSET) — задержка от начала аудиофайла до первой ноты. Авторы часто подгоняют до первой ноты после вступления
      • BPM — темп. Что очень важно, его надо считать очень точно, желательно 5–7 цифр после запятой
      • BPM Changes — обычно время=новый_темп, время=новый_темп и т.д.
      • Delay/Pause — остановки в треке. С нимим есть проблемы точности: паузы вычисляются со своей точностью, которая не совпадает с делением на доли ритма в треке. Их как правило стараются заменить на кратную смену темпа на что-то < 5 и обратно компенсируют. Для таких дел даже скрипт написан
      • Массив данных — собственно, в каждый шаг трека какая нота должна быть сыграна

      Вот пример для кнопочной ритм-игры: DDR (ITG).
      Там 4 кнопки — поэтому 4 символа в слове для состояния. Разумеется есть редактор, вручную набирать не приходиться:

      Cookie Thumper.sm
      #TITLE: Cookie Thumper;
      #SUBTITLE:;
      #ARTIST: Die Antwoord;
      #TITLETRANSLIT:;
      #SUBTITLETRANSLIT:;
      #ARTISTTRANSLIT:;
      #GENRE:;
      #CREDIT:;
      #BANNER:…/banner.png;
      #BACKGROUND:…/bg.png;
      #LYRICSPATH:;
      #CDTITLE:;
      #MUSIC: Cookie Thumper.ogg;
      #OFFSET:0.003;
      #SAMPLESTART:48.350;
      #SAMPLELENGTH:12.000;
      #SELECTABLE: YES;
      #BPMS:0.000=134.000,107.000=67.000,143.000=134.000;
      #STOPS:92.000=0.448
      ;
      #BGCHANGES:;
      #KEYSOUNDS:;

      //---------------dance-single — Nix----------------
      #NOTES:
      dance-single:
      Nix:
      Challenge:
      10:
      0.631,0.711,0.437,0.164,0.665:
      0000
      0000
      0000
      0000
      ,
      0000
      0000
      0000
      0000
      ,
      0000
      0000
      0000
      0000
      ,
      0000
      0000
      0000
      0000
      ,
      0000
      0000
      0000
      0000
      ,
      0000
      0000
      0000
      0000
      ,
      2001
      0000
      3100
      1000
      0010
      0100
      0000
      0110
      ,
      0000
      0100
      0001
      0010
      1000
      0000
      0010
      0000
      ,
      0011
      0000
      0001
      0100
      1000
      0100
      0000
      0110
      ,
      0000
      0010
      1000
      0100
      0010
      0000
      0001
      0000
      ,
      1000
      0000
      1000
      0100
      0000
      0100
      0010
      0000
      0001
      0000
      0100
      0010
      1000
      0000
      0100
      0000
      ,
      0010
      0001
      0010
      0100
      0000
      0100
      1000
      0010
      0100
      0000
      0001
      0000
      1001
      0000
      0000
      0001
      ,
      0100
      0010
      1000
      0010
      0000
      0010
      0100
      0001
      0010
      0000
      0100
      0000
      1100
      0000
      0000
      1000
      ,
      0020
      0000
      0230
      0000
      0301
      0100
      0001
      0000
      0101
      0000
      1001
      0000
      2020
      0000
      3030
      0000
      ,
      0120
      0000
      0130
      0001
      0100
      0010
      1000
      0000
      1100
      0000
      0110
      0000
      0011
      0000
      0000
      0000
      ,
      0001
      0000
      0100
      0010
      1000
      0100
      0010
      0001
      1000
      0000
      0010
      0000
      0100
      0000
      0001
      0000
      ,
      0010
      0100
      1000
      0001
      0000
      0001
      1000
      0010
      0100
      0000
      0001
      0000
      0020
      0000
      0000
      0000
      ,
      0030
      0000
      0000
      0000
      0100
      1000
      0100
      0010
      0001
      0000
      0100
      0000
      0110
      0000
      0000
      0000
      ,
      1000
      0010
      0100
      0001
      1000
      0100
      1000
      0001
      0010
      0000
      0100
      0000
      0020
      0000
      0031
      0000
      ,
      0100
      0010
      0000
      1000
      0000
      0010
      0000
      0010
      0100
      0000
      0001
      0000
      1000
      0000
      0100
      0010
      ,
      0001
      0010
      0100
      1000
      0000
      1000
      0001
      0100
      0010
      1000
      0010
      0000
      0100
      0000
      0000
      0000
      ,
      0001
      0010
      0000
      1000
      0000
      0010
      0000
      0100
      1000
      0010
      0100
      0000
      0001
      0000
      0000
      0000
      ,
      1000
      0100
      0010
      0100
      0001
      0100
      0010
      0000
      0022
      0000
      0000
      0000
      0033
      0000
      1000
      0000
      ,
      1010
      0000
      0010
      0100
      0001
      0000
      1000
      0000
      1100
      0000
      0000
      0000
      0100
      0000
      0100
      0010
      ,
      0001
      0010
      0100
      1000
      0001
      0000
      0100
      0000
      0020
      0000
      0000
      0000
      0030
      0000
      0100
      0000
      ,
      0001
      0000
      0000
      0000
      1000
      0000
      1000
      0010
      0100
      0001
      0100
      0000
      0220
      0000
      0330
      0000
      ,
      2002
      0000
      0000
      0000
      0000
      0000
      3003
      0000
      0001
      0000
      1000
      0001
      1000
      0000
      0000
      1000
      ,
      0001
      0100
      0000
      0110
      0000
      0010
      1000
      0100
      0001
      0000
      0100
      0000
      1100
      0000
      0000
      1000
      ,
      0010
      0100
      0000
      0110
      0000
      0100
      0001
      0010
      1000
      0000
      0010
      0000
      0110
      0000
      0100
      1000
      ,
      0010
      0100
      0000
      0101
      0000
      0001
      0010
      1000
      0001
      0000
      0100
      0000
      1100
      0000
      0100
      0010
      ,
      0001
      0000
      0100
      0000
      0000
      0000
      0101
      0000
      0000
      0000
      0001
      0000
      1000
      0010
      1000
      0010
      1000
      0000
      0001
      0100
      0001
      0000
      0001
      0000
      0101
      0000
      0000
      0000
      0000
      0000
      0100
      0000
      ,
      0010
      1000
      0000
      1100
      0000
      0100
      0001
      1000
      0010
      0000
      0100
      0000
      1100
      0000
      1000
      0010
      ,
      0100
      0001
      0000
      0011
      0000
      0010
      1000
      0010
      0001
      0000
      0100
      0000
      0110
      0000
      0010
      0001
      ,
      0010
      1000
      0000
      1010
      0000
      0010
      0001
      1000
      0100
      0000
      0001
      0000
      1001
      0000
      1000
      0100
      ,
      0001
      0000
      0100
      0000
      0000
      0000
      1100
      0000
      0000
      0000
      1000
      0000
      0001
      0010
      0001
      0010
      0001
      0000
      1000
      0100
      1000
      0000
      1000
      0000
      0200
      0000
      0000
      0000
      0000
      0000
      M0MM
      0000
      ,
      0000
      0000
      0000
      0000
      0000
      0000
      M0MM
      0000
      0000
      0000
      0000
      0000
      0000
      0000
      M0MM
      0000
      0000
      0000
      0000
      0000
      0300
      1000
      0001
      0000
      0101
      0000
      0000
      0000
      0000
      0000
      0000
      0000
      ,
      0000
      0000
      0101
      1001
      1010
      1000
      0100
      0010
      ,
      0001
      0000
      0100
      0001
      1000
      0000
      0010
      0000
      0110
      0000
      0000
      0000
      0100
      0001
      1000
      0000
      ,
      0100
      0000
      0000
      0010
      0000
      0000
      0001
      0000
      0000
      0010
      0000
      0000
      0100
      0000
      0000
      0000
      0000
      0000
      1000
      0000
      0000
      0000
      0000
      0000
      0010
      0000
      0000
      0000
      1000
      0000
      0100
      0000
      0000
      0000
      0000
      0000
      1100
      0000
      0000
      0000
      0000
      0000
      0000
      0000
      0000
      0110
      0000
      0000
      ,
      0000
      0000
      0011
      0000
      0000
      1000
      0001
      0000
      1011
      0000
      0000
      1000
      0100
      0000
      0200
      0000
      ,
      0310
      0000
      0001
      0000
      0100
      0000
      1000
      0000
      0100
      0000
      0000
      0000
      0001
      0010
      0000
      1000
      ,
      0000
      0001
      0000
      0100
      0010
      0000
      0001
      0000
      0100
      0000
      0000
      0000
      0010
      1000
      0000
      0001
      ,
      0000
      0100
      0010
      1000
      0100
      0000
      0010
      0000
      0001
      0000
      0000
      0000
      1000
      0100
      0000
      0001
      ,
      0000
      0100
      1000
      0010
      1000
      0000
      0100
      0000
      0010
      0000
      0000
      0010
      0001
      0000
      0100
      0000
      ,
      0010
      0000
      1000
      0010
      1000
      0000
      0100
      0000
      0010
      0000
      0000
      0000
      0010
      0000
      0100
      1000
      ,
      0001
      0100
      0010
      1000
      0100
      0000
      0010
      0000
      0001
      0000
      0000
      1000
      0100
      1000
      0010
      0000
      ,
      0100
      0001
      1000
      0000
      0100
      0000
      0010
      0000
      0001
      0000
      0000
      0000
      0101
      0000
      0110
      0000
      ,
      0010
      0000
      1000
      0010
      1000
      0010
      0100
      0000
      0001
      0000
      0010
      0000
      1000
      0000
      0010
      0000
      ,
      0100
      0000
      1000
      0000
      0010
      0100
      0010
      0000
      1000
      0000
      0000
      0000
      0101
      0000
      0001
      0000
      ,
      0010
      0000
      0100
      1000
      0001
      1000
      0010
      0000
      0100
      0000
      0110
      0000
      1010
      0000
      0001
      0010
      ,
      0100
      1000
      0100
      0010
      0001
      0000
      1000
      0000
      0010
      0000
      0100
      0000
      0001
      0010
      0001
      0000
      ,
      0010
      0100
      1000
      0010
      0200
      0000
      0000
      0000
      ,
      0300
      0000
      0000
      0000
      ;

© Habrahabr.ru