Синхронізація музики та ігрових подій на Unity

image
Приклад редактора рівня в грі.

Якщо ви коли-небудь грали в ігри типу Guitar Hero, Osu або Bit Trip Runner ви знаєте, як сильно занурює в «потік» проста залежність геймплея від музики, що грає на тлі. Дивно, що таких ігор, насправді не так вже і багато. Крім того, така синхронізація може бути корисна для створення спецефектів, але тим не менше майже ніде не зустрічається, крім зазначених вище ігор типу rhythm. Ось і я вирішив скористатися таким нехитрим прийомом у власній грі, а також поділитися напрацюваннями.

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

Отже, для початку потрібно визначити клас-подія:

[Serializable]
public class Game_event {
public char key; //В залежності від ключа буде відбуватися те чи інше подія
public float time; //Момент старту події
[NonSerialized]public float finish_time; //Необхідний, щоб подія не було створено повторно після завершення

public bool isFinish(){
//Функція, що перевіряє завершення події
return false;
}

public void Create(){
//Створюємо необхідні для події об'єкти 
//Важливо, що б всі рухи об'єктів залежали від (Main.sound_time - time)
}

public void Destroy(){
//видаляємо їх
}

public Game_event (float time, char key){
this.time = time;
this.key = key;
}
} 


Далі, потрібно клас наследованный від MonoBehaviour, в якому буде код основний і, звичайно, посилання на звуковий об'єкт. У моєму випадку це клас Main.

public static float sound_time=0; //глобальна змінна, в якій буде зберігатися поточний час програється звуку
public static List<Game_event> game_event = new List<Game_event>(); //список подій

void Update () {
sound_time = sound.time;
//sound - об'єкт типу AudioSource, що містить музыку

foreach (Game_event e in game_event) {
if (sound_time>=e._time && sound_time<e.finish_time && !e.active)
{
e.active = true;
e.finish_time = float.MaxValue;
current_event =e;
e.Create();
} 

if (e.active)
if (e.isFinish()) //функція isFinish може бути ресурсномісткою, тому, перш перевіряється активність події
{
e.active=false;
e.finish_time = sound_time;
e.Destroy();
}
}
}


Є кілька варіантів створення різних подій: шляхом перерахування безпосередньо в коді Game_event, створення додаткових класів, або використання скриптової мови начебто Lua, що звичайно зручніше.



Редактор

Найбільш зручний спосіб редагування, на мій погляд, прив'язка певних подій до клавіш, тоді створення рівня перетворюється в «гру на піаніно», де ваша задача лише натискати клавіші в ритм музиці. Саме тому в якості ключа використовуються відповідні символи.

Для реалізації, потрібно визначити доступні для введення клавіші:

public static char[] keys_s = { 'Q','W','E','R','T',
'A','S','D','F','G',
'Z','X','C','V','B'};

//І додати наступний код
void Update () {
...

Event c_e = Event.current;
if (c_e.isKey && c_e.type == EventType.Натискання) {
if (Array.Exists(Main.keys_s, c=>c==c_e.keyCode.ToString()[0])) // Перевіряємо, чи натиснута клавіша в масиві допустимих
{
game_event.Add (new Game_event (sound_time,c_e.keyCode.ToString()[0]));
}
}
}


Тепер в момент натиснення клавіші, в список запишеться відповідна подія, яка буде програватися синхронно звуку.

Може бути дуже зручно, підлаштовувати події під малюнок звукової хвилі. Отримати текстуру з її зображенням можна наступним способом:

float[] samples = new float[sound.clip.samples * sound.clip.channels];
sound.clip.GetData(samples, 0); //Отримуємо масив з даними семпла по якому буде будуватися текстура
int frequency = sound.clip.frequency; //бітрейт семпла
int scale = 10; //пікселів на 1с семпла

SoundTex = new Texture2D ((int)(sound.clip.length*sound.clip.channels*scale), 200);
int height = (int)(SoundTex.height / 2);

for (int i=0; i<SoundTex.width; i++) {
int c_hi = 0;
int c_low = 0;
float s_hi = 0;
float s_low = 0;

//Вираховуємо середнє нижнє і середнє верхнє значення на 1px текстури
for (int k=0; k<(int)(frequency/scale); k++) {
if (samples[k+i*(int)(frequency/scale)]>=0) {
c_hi++; 
s_hi+=samples[k+i*(int)(frequency/scale)];
}
else {
c_low++; 
s_low+=samples[k+i*(int)(frequency/scale)];
}
}

//Малюємо лінію від середнього нижнього до верхнього середнього 
//Поділена вона на більш світлу внутрішню і більш темну верхню частину, виключно для краси
for (int j=0; j<(int)(SoundTex.height); j++) {
if (j<(int)((s_hi/c_hi)*height*0.6 f+height) && 
j>(int)((s_low/c_low)*height*0.6 f+height)) 
SoundTex.SetPixel(i,j,new Color(0.7 f,1,0.7 f));
else 
if (j<=(int)((s_hi/c_hi)*height+height) && 
j>=(int)((s_low/c_low)*height+height)) 
SoundTex.SetPixel(i,j,new Color(0,1,0));
else SoundTex.SetPixel(i,j,new Color(0,0,0,0));
}
}

SoundTex.Apply (); //Застосовуємо зміна до текстурі
//Результат можна подивитися на головній картинці


Подивитися, як все працює у дії можна на цьому відео:


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.