Комментарии

Gridmaniac
Оставлен: Автоматически
Смело оставляйте комментарии, критикуйте, задавайте вопросы

Ритм-игра на Unity

Опубликовано: 13.01.2017

Давно хотел сделать свою ритм-игру, ведь у всех остальных есть фатальный недостаток :D Но если серьезно, то всегда любил ритм-игры. Идей было несколько, но срок поставил себе в 2 дня, а хотелось создать что-то законченное.

Поиск треков

По традиции начал с самого важного этапа - поиска музыки.

Нашел отличный портал музыки по лицензии Creative Commons и, прослушав пару десятков композиций, собрал коллекцию треков от таких авторов, как Aerologic, ROZKOL, EeL, Pocket Boy. Плейлист шикарный, в конце оставлю.

Суть игры

Суть игры Odori Beats

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

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

Если объяснение показалось непонятным, предлагаю сразу посмотреть ролик готовой игры.

Последовательность битов

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

Напишем класс, который будем вешать на каждый трек. Треки будут храниться в виде префабов в качестве подгружаемых runtime ресурсов.

public class TrackIdentity : MonoBehaviour {
   public string Id;
   public string Beats;
   public float Duration;
   public float PreviewTime;
   public float StartTime;
}

Первые 2 параметры очевидны.

Параметр Duration задает пользовательскую длительность трека, что позволяет нам показать результаты игры после логического завершения трека, а не окончания самой песни.

PreviewTime используется в меню для предпрослушивания трека с наиболее драйвового момента.

StartTime запускает трек с заданного времени (для пропуска длительного вступления).

Для создания так называемого набора битов трека создаем отдельную сцену. добавляем префаб-трек на сцену и помещаем на любой объект следующий бихевиор:

void Update () {
    if (Input.GetKeyDown(KeyCode.Keypad0))
    {
        songBeat += "@" + GetComponent().timeSamples;
    }

    s.text = "S: " + starts;
    e.text = "E: " + ends;
    c.text = "C: " + GetComponent().time;
}

Для удобства я также создал 3 текстбокса для вывода времени начала трека, конца и текущего времени.

После запуска сцены начинает играть трек. Нам остается только ритмично нажимать выбранную клавишу, тем самым формируя биты. В результате копируем runtime значение переменной songBeat в буфер обмена, завершаем выполнение сцены и вставляем значение в ту же переменную, сохраняя при этом префаб.

Math

Основной задачей является создание шариков за пределами экрана, их движение к эпицентру и уничтожение по своевременному тапу или по длительному нахождению в эпицентре.

У каждого шарика есть следующие характеристики:

  • Момент создания шарика за пределами экрана;
  • Момент, когда шарик должен долететь к эпицентру;
  • Начальная позиция;
  • Количество здоровья.

Далее создаем шарик в нужный момент:

cSample = track.timeSamples; //Текущий семпл
for (var i = iLimit; i < sps.Length; i++) //Просматриваем все биты трека
{
  s = int.Parse(sps[i]);
  if (s > cSample && s - cSample < bCo) //Если бит готов к вылету
  {
    CreateBeat(int.Parse(sps[i]), cSample); //Создаем шарик
    sps[i] = "0";
    iLimit = i + 1;
  }
  if (s - cSample > bCo) { break; }
}

Полет шарика сделал так:

void Update()
{
    sampleCurrent = audioSource.timeSamples;
    bm = sampleDestination - sampleCreated;
    dm = sampleCurrent - sampleCreated;
    t = dm / bm; //Посчитали момент интерполяции

    rect.anchoredPosition = Vector2.Lerp(initialPos, new Vector2(0, 0), t); //Сам полет
    rect.Rotate(Vector3.forward * -50.0f); //Немного покрутим
    distanceSamples = sampleDestination - sampleCurrent;

    if (Vector2.Distance(rect.anchoredPosition, new Vector2(0, 0)) > 0.1f)
    {
        rect.localScale = Vector3.Lerp(new Vector3(0.4f, 0.4f, 0.4f),
            new Vector3(1.0f, 1.0f, 1.0f), t); //Немного увеличиваем
    }
}

Tap moment

Дрейним здоровье шарика, если игрок не успел тапнуть и объявляем пропуск:

foreach (var beat in beats)
{
    if (Vector2.Distance(beat.rect.anchoredPosition,
        new Vector2(0, 0)) < 0.01f)
    {
        beat.lifeTime--;
        if (beat.lifeTime <= 0)
        {
            DestroyObject(beat.gameObject);
            Miss(); //Тут можно обнулять комбо и т.п.
        }
    }
}

А если нажал, то считаем насколько точно в зависимости от оставшихся семплов:

if (beats.Length > 0 && beats[0].distanceSamples < 6000) {
    tapped = true;
    mus.color = beats[0].color;
    chain++;

    int distance = beats[0].distanceSamples;
    int scorePortion = 0;
    if (distance <= 1000) {
        scorePortion = 100;
        excellent++;
    }
    if (distance > 1000) {
        scorePortion = 50;
        great++;
    }
}

Визуализация

Идея визуализации пришла, когда бродил по Unity Asset Store и наткнулся на Motion Capture Pack. Многие замечали, что если включить более менее драйвовую музыку под любой танец, создается интересный эффект. Кажется, что человек или персонаж танцуют в ритм музыке. Благодаря Mecanim создал различные наборы танцев и добавил аниме персонажей. Для окружения также использовал готовые ассеты. Немного порисовал шарики и UI в PS.

Итог

Игра в Google Play

Если кому-то нужны исходники, пишите.

Треки