Ритм-игра на Unity
Давно хотел сделать свою ритм-игру, ведь у всех остальных есть фатальный недостаток :D Но если серьезно, то всегда любил ритм-игры. Идей было несколько, но срок поставил себе в 2 дня, а хотелось создать что-то законченное.
Поиск треков
По традиции начал с самого важного этапа - поиска музыки.
Нашел отличный портал музыки по лицензии Creative Commons и, прослушав пару десятков композиций, собрал коллекцию треков от таких авторов, как Aerologic, ROZKOL, EeL, Pocket Boy. Плейлист шикарный, в конце оставлю.
Суть игры

Суть игры заключается в следующем: есть пульсирующий эпицентр в центральной части экрана, куда летят шарики с разных сторон. Задача игрока - тапать по экрану в тот момент, когда очередной шарик находится ближе всего к эпицентру.
Шарики летят с расчетом на то, что окажутся в эпицентре в подходящий момент, чтобы их уничтожить тапом в ритм музыке, то есть в бит. В зависимости от расстояния от центра в момент тапа, за каждый шарик будет даваться различное кол-во очков. Если тапнуть слишком поздно, то шарик исчезнет, и счетчик комбо обнулится.
Если объяснение показалось непонятным, предлагаю сразу посмотреть ролик готовой игры.
Последовательность битов
Для начала необходимо задать последовательность моментов, в которые игрок должен попадать в определенный ритм (далее буду называть их биты). Самый простой вариант это записать их в строку с разделителем, каждый бит в которой будет представлен в виде порядкового номера семпла аудиофайла.
Напишем класс, который будем вешать на каждый трек. Треки будут храниться в виде префабов в качестве подгружаемых 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.
Итог
Если кому-то нужны исходники, пишите.
Комментарии