Gamasutra: Blog Graham Tattersall – Coding to the Beat

Posting blog berikut, kecuali disebutkan, ditulis oleh anggota komunitas Gamasutra.
Pikiran dan pendapat Gamasutra atau perusahaan induknya.

Jadi, Anda ingin membuat ritme permainan atau mencoba membuatnya, tetapi permainan dan musik musik dengan cepat menjadi tidak sinkron, dan sekarang Anda tidak yakin apa yang harus dilakukan. Anda datang ke tempat yang tepat. Saya sudah bermain sejak permainan ritme sekolah menengah, ketika saya sering mengunjungi mesin DDR di arcade lokal saya. Hari ini saya selalu mencari genre baru, dan dengan entri seperti Crypt of the Necrodancer atau Bit.Trip.Runner, masih banyak yang bisa dilakukan dalam permainan ritme. Saya menaruh beberapa pekerjaan ke dalam beberapa prototipe berbasis ritme di Unity, yang akhirnya mencurahkan waktu satu bulan untuk menciptakan Atomic Beats, permainan ritme / puzzle pendek. Saya akan membahas beberapa teknik pengkodean yang paling berguna yang saya pelajari dalam permainan ini yang tidak tercakup di tempat lain, atau saya mungkin akan dibahas secara lebih rinci.

Pertama, saya sangat berterima kasih kepada posting blog Yu Chao yang Menyinkronkan Musik di Rhythm Games & # 39 ;. Yu membahas inti sinkronisasi waktu audio untuk mesin game di Unity, dan membuat kode sumber untuk Boots-Cut tersedia, yang membantu membuat proyek ini tidak berjalan. Anda dapat melihat panduan cepat untuk sinkronisasi musik di Unity, tetapi saya akan membahas inti dari apa yang ia uraikan dan banyak lagi. Artikel saya dan Boots-Cut.

Inti dari setiap permainan ritme adalah pemilihan waktu. Orang-orang sangat sensitif terhadap penyimpangan dalam waktu ritmik, sehingga sangat penting untuk memastikan bahwa setiap tindakan, gerakan, atau input dalam permainan ritme secara langsung disinkronkan dengan musik. Sayangnya metode tradisional untuk melacak waktu di Unity, seperti Time.time, LevelLoad, Time dan Time. Waktu akan dengan cepat kehilangan sinkronisasi dengan pemutaran audio apa pun. Sebaliknya kami akan mengakses waktu sesuai dengan sistem audio menggunakan AudioSettings.dspTime, yang bergantung pada sistem audio yang sebenarnya telah diproses, dan akan selalu tetap sinkron dengan audio saat diputar (Ini mungkin bukan kasus dengan audio yang sangat panjang) file karena beberapa efek mungkin ikut bermain, tetapi untuk aplikasi panjang normal, itu harus bekerja dengan baik). Fungsi ini akan menjadi inti dari bagaimana kami melacak waktu lagu, dan kami akan membangun kelas utama kami di sekitarnya.

Kelas Konduktor adalah lagu utama yang mengelola sisa permainan ritme kami yang akan dibangun. Dengan itu, kami akan melacak posisi lagu, dan mengontrol semua tindakan yang disinkronkan lainnya. Untuk melacak lagu, kita perlu beberapa variabel:

// Ketukan lagu per menit
// Lagu ini mencoba disinkronkan
public float songBpm;

// Jumlah detik untuk setiap beat lagu
secPerBeat float publik;

// Posisi lagu saat ini, dalam detik
Posisi lagu publik;

// Posisi lagu saat ini, dalam ketukan
public float songPositionInBeats;

// Berapa detik telah berlalu sejak lagu dimulai
float publik dspSongTime;

// Sumber Audio yang terlampir pada GameObject ini yang akan memutar musik.
sumber audioSumber publik musik;

Ketika adegan dimulai, kita perlu beberapa variabel, dan juga catatan, untuk referensi, waktu ketika audio dimulai.

batal Mulai ()
{
// Muat AudioSource yang terlampir pada Conductor GameObject
musicSource = GetComponent();

// Hitung jumlah detik dalam setiap ketukan
secPerBeat = 60f / songBpm;

// Catat waktu ketika musik dimulai
dspSongTime = (float) AudioSettings.dspTime;

// Mulai musik
musicSource.Play ();
}

Jika Anda membuat game kosong dengan skrip ini dilampirkan, dan menambahkan audio dan lagu dan memulai program, Anda dapat melihat skrip akan diperbarui dengan lagu dimulai, tetapi tidak banyak lagi yang akan terjadi. Anda juga harus memasukkan BPM musik yang Anda tambahkan ke Sumber Audio secara manual.

Konduktor menentukan waktu mulai dalam detik

Dengan nilai-nilai ini, kami sekarang dapat melacak lagu secara real time saat game diperbarui. Kami akan menentukan waktu lagu, pertama dalam detik, kemudian dalam ketukan. Beats adalah cara yang jauh lebih mudah untuk melacak lagu karena memungkinkan kita menambahkan tindakan dan waktu dalam lagu, katakan pada ketukan 1, 3, dan 5.5, tanpa harus menghitung detik di antara ketukan setiap kali. Tambahkan perhitungan berikut ke fungsi Pembaruan () Konduktor:

batal Pembaruan ()
{
// tentukan sejak lagu dimulai beberapa detik sejak itu
songPosition = (float) (AudioSettings.dspTime – dspSongTime);

// tentukan berapa banyak ketukan sejak lagu dimulai
songPositionInBeats = songPosition / secPerBeat;
}

ini memberi kita perbedaan antara waktu saat ini dan lagu, yang memiliki total jumlah detik lagu telah diputar, yang kami simpan di posisi variabel lagu.

Perhatikan bahwa musik dimulai dengan 1, dengan ketukan 1-2-3-4-dst., SongPositionInBeats dimulai pada 0 dan meningkat dari sana, sehingga lagu akan terjadi ketika songPositionInBeats sama dengan 2.0, bukan 3.0.

Pada titik ini, Anda dapat menelurkan catatan sesuai dengan irama yang Anda inginkan untuk membuat tarian tradisional, Anda dapat mengeja mereka untuk ditekan, menginterpolasi posisi mereka ke arah garis pemicu, kemudian merekam laguPosisiInBerkas ketika sebuah tombol ditekan dan membandingkannya dengan dimaksudkan mengalahkan not itu. Yu Chao memberikan contoh untuk membuat blognya di sini. Alih-alih mengulangi itu, saya akan membahas beberapa teknik lain yang berpotensi berguna yang dapat dibangun di atas kelas konduktor yang saya gunakan dalam membangun Beats Atom.

Jika Anda membuat musik sendiri untuk permainan ritme Anda, akan mudah untuk memastikan musiknya selesai, yang akan membuat SongPositionInBeats lagu Conductor menyelaraskan dengan benar ke lagu tersebut, selama BPM dimasukkan dengan benar.

Waveform Sempurna Disinkronkan

Namun, jika Anda menggunakan musik yang sudah ada sebelumnya, ada kemungkinan bagus bahwa ada periode hening yang kecil sebelum lagu dimulai. Tanpa memperhitungkan ini, songPositionInBeats akan mulai diputar terlebih dahulu saat klip pertama kali muncul. Apa pun yang Anda selaraskan dengan angka ketukan berikutnya akan lebih lama disinkronkan dengan musik saat permainan dimainkan.

Gelombang Offbeat

Untuk memperbaikinya, kita bisa menambahkan variabel ke akun untuk offset. Di kelas Konduktor, tambahkan berikut ini:

// Ketukan pertama mengalahkan lagu dalam hitungan detik
public float firstBeatOffset;

Di Pembaruan (), songPosisi berubah dari:

songPosition = (float) (AudioSettings.dspTime – dspSongTime);

untuk
songPosition = (float) (AudioSettings.dspTime – dspSongTime – firstBeatOffset);

Sekarang songPosisi akan menghitung posisi lagu dengan benar ketika ketukan pertama terjadi. Anda harus memasukkan offset secara manual ke beat pertama, karena ini akan unik untuk setiap file lagu. Ada juga jendela pendek selama offset itu di mana songPosition akan negatif. Ini tidak memengaruhi permainan Anda, tetapi beberapa kode yang mengandalkan Posisi lagu atau songPositionInBeatss mungkin tidak dapat memproses angka negatif selama waktu ini.

Pengaturan waktu lagu diperbaiki

Jika Anda memiliki lagu yang berjalan dari awal hingga akhir, hanya menggunakan konduktor di atas, tetapi jika Anda memiliki trek pendek yang berputar dan ingin bekerja dalam lingkaran itu, itu perlu untuk membuat loop ke konduktor Anda.

Dengan klip loop sempurna (misalnya jika beat adalah 120bpm, dan klip yang Anda ingin loop adalah 4 ketukan panjang, itu harus tepat 8,0 detik per beat) dimuat ke Conductor Audio Source, centang kotak loop. Konduktor akan bekerja sama seperti sebelumnya, memberikan total waktu sejak klip pertama kali dimulai sebagai SongPosition. Kami harus menyediakan cara bagi konduktor untuk mengetahui berapa banyak loop dalam satu loop, dan berapa banyak loop yang telah diselesaikan. Tambahkan variabel berikut ke kelas Konduktor:

// jumlah ketukan di setiap loop
beats float publikPerLoop;

// jumlah total loop yang diselesaikan sejak loop klip pertama kali dimulai
public int completeLoops = 0;

// Posisi lagu saat ini dalam loop dalam ketukan.
public float loopPositionInBeats;

Sekarang setiap kali SongPositionInBeats diperbarui, kami juga dapat memperbarui posisi loop di Pembaruan ()

// hitung posisi loop
if (songPositionInBeats> = (finishedLoops + 1) * beatsPerLoop)
selesaiLoops ++;
loopPositionInBeats = songPositionInBeats – completLoops * beatsPerLoop;

Ini akan memberi Anda penanda untuk berapa banyak ketukan melalui loop Anda dengan loopPositionInBeats, yang akan berguna untuk banyak item yang disinkronkan lainnya. Jangan lupa untuk memasukkan jumlah ketukan per loop pada Conductor GameObject.

Hal lain yang perlu diperhatikan di sini, sekali lagi, adalah penghitungan ketukan. Musik selalu dimulai pada 1, jadi ukuran 4 ketukan menjadi 1-2-3-4-, sementara di kelas ini, loopPositionInBeats dimulai pada 0,0 dan loop pada 4,0. Sebagai hasil dari ini, bagian tengah yang tepat dari loop, yang akan menjadi 3 ketika menghitung dengan beat musik, akan terjadi pada nilai loopPositionInBeats 2.0. Dimungkinkan untuk memodifikasi loopPositionInBats untuk memperhitungkan hal ini, tetapi itu akan meneruskan semua perhitungan lainnya, jadi berhati-hatilah bagaimana Anda memasukkan catatan.

Ini juga akan berguna untuk menambahkan hal-hal ke kelas Konduktor untuk alat yang tersisa. Yang pertama adalah versi analog dari LoopPositionInBeats, yang disebut LoopPositionInAnalog, yang mengukur lokasi dalam loop antara 0 dan 1.0. Yang kedua adalah turunan dari kelas Konduktor, dapat dengan mudah dipanggil dari kelas lain. Tambahkan variabel berikut ke kelas Konduktor:

// Posisi relatif saat ini dari lagu dalam loop antara 0 dan 1.
public float loopPositionInAnalog;

// Contoh konduktor
contoh Konduktor statis publik;

Dalam fungsi Sedarlah () tambahkan:

batal Sedarlah ()
{
instance = ini;
}

dan dalam fungsi Perbarui () tambahkan:

loopPositionInAnalog = loopPositionInBeats / beatsPerLoop;

Akan sangat berguna untuk memiliki gerakan atau rotasi yang harus dikalahkan agar berada di lokasi yang benar. Dalam Atomic beats, saya menggunakan ini untuk secara dinamis memutar not di sekitar poros tengah. Mereka awalnya ditempatkan di sekitar lingkaran sesuai dengan ketukan mereka dalam lingkaran, dan kemudian memainkan seluruh area sehingga catatan akan disejajarkan dengan garis pemicu pada ketukan yang dimaksudkan.

Untuk mencapai ini, buat skrip baru bernama SyncedRotation dan lampirkan ke GameObject yang ingin Anda putar. Dalam fungsi Pembaruan () dari Sinkronisasi Jarak Jauh tambahkan:

batal Pembaruan ()
{
this.gameObject.transform.rotation = Quaternion.Euler (0, 0, Mathf.Lerp (0, 360, Conductor.instance.loopPositionInAnalog));
}

Ini akan menginterpolasi rotasi objek game yang dilampirkan game ini antara 0 dan 360 derajat, memutarnya sehingga menyelesaikan satu rotasi pada akhir setiap loop. Ini berguna sebagai contoh, tetapi ingin tetap selaras dengan irama.

Fitur Animator di Unity sangat kuat, tetapi tidak selalu tepat. Dalam hal animasi untuk musik yang bisa diandalkan, saya berjuang dengan kelas Animator dan kecenderungannya untuk tidak sinkron dari ketukan seiring waktu. Juga sulit untuk menyesuaikan animasi yang sama untuk lagu yang berbeda tanpa harus mendefinisikan ulang kerangka kunci animasi untuk tempo itu. Sebagai gantinya, kita dapat langsung mengakses loop animasi, kami ingin menargetkan dan mengatur lokasi loop sesuai dengan posisi kami di loop Konduktor.

Pertama, buat kelas baru yang disebut SyncedAnimation, dan sertakan variabel berikut:

// Kontroler animator terpasang pada GameObject ini
animator publik animator;

// Merekam status animasi atau animasi tempat Animator saat ini
AnimatorStateInfo publik animatorStateInfo;

// Status saat ini di dalam Animator digunakan oleh fungsi Play ()
public int currentState;

Lampirkan ke Objek Game baru atau yang sudah ada yang ingin Anda menghidupkan. Untuk contoh ini, kami hanya akan memindahkan objek bolak-balik di layar, tetapi ini dapat diterapkan pada animasi apa pun, apakah itu menyesuaikan properti, atau animasi frame-by-frame. Tambahkan Animator ke GameObject, dan buat Pengontrol Animator baru bernama SyncedAnimController, dan Klip Animasi bernama BackAndForth. Pengontrol ke dalam kelas Animator yang melekat pada GameObject, dan menambahkan Animasi ke pohon Animator sebagai animasi default.

Misalnya, saya mengatur animasi untuk memindahkan objek ke kanan pertama sebanyak 6 unit, lalu ke -6, lalu kembali ke 0.

Kurva Animasi

Sekarang untuk menyinkronkan animasi, tambahkan kode berikut ke fungsi Mulai () dari Inisialisasi ke informasi tentang Animator:

batal Mulai ()
{
// Muat animator yang terpasang pada objek ini
animator = GetComponent();

// Dapatkan info tentang status animator saat ini
animatorStateInfo = animator.GetCurrentAnimatorStateInfo (0);

// Konversi nama negara saat ini menjadi hash integer untuk identifikasi
currentState = animatorStateInfo.fullPathHash;
}

Kemudian tambahkan kode berikut ke Pembaruan () untuk mengatur animasi:
batal Pembaruan ()
{
// Mulai mainkan loop saat ini dari konduktor saat ini
animator.Play (currentState, -1, (Conductor.instance.loopPositionInAnalog));
// Atur kecepatan ke 0 sehingga hanya akan mengubah bingkai saat Anda memperbaruinya berikutnya
animator.speed = 0;
}

Ini akan menjadi bingkai animasi yang tepat untuk satu loop lengkap. Jadi misalnya, menggunakan animasi di atas, jika Anda setengah jalan melalui loop, posisi GameObject hanya akan lewat. .

Perlu dicatat bahwa untuk membuat loop yang mulus dari beberapa animasi, perlu untuk menyesuaikan garis singgung dari masing-masing kerangka kunci animasi pada kurva animasi. Linear akan memberi Anda garis lurus, datang dari keyframe menuju keyframe berikutnya, memberinya gerakan tiba-tiba yang menghalangi.

Kerangka kunci singgungMeskipun berguna, metode ini juga akan mengganggu transisi animasi karena ia memaksa animasi. Sisa dari keadaan apa pun ketika script awalnya dijalankan. Ini berguna untuk objek yang hanya perlu menggunakan satu animasi yang disinkronkan selamanya, tetapi untuk melakukan objek yang lebih rumit dengan animasi yang berbeda, Anda harus memasukkan kode yang menangani transisi tersebut dan menyesuaikan keadaan variabel saat ini untuk disesuaikan dengan keadaan animasi yang dimaksud.

Itu hanya beberapa item yang saya temukan berguna untuk membuat Beats Atom. Beberapa orang dirakit dari sumber lain, atau dibuat karena kebutuhan, tetapi kebanyakan dari mereka tidak tersedia, mudah-mudahan Anda menemukan beberapa di antaranya berguna! Beberapa dari mereka mungkin berguna dalam proyek yang lebih besar karena kendala pada CPU atau sistem audio, tetapi mereka harus berfungsi sebagai awal yang baik untuk setiap permainan jam permainan atau proyek hobi.

Membuat permainan ritme, atau elemen-elemen permainan yang disinkronkan dengan musik bisa sulit dan membutuhkan beberapa kode yang rumit untuk memastikan semuanya mengikuti ketukan yang sama, tetapi sangat besar, dan benar-benar dapat menarik pemain. Ada lebih banyak hal yang dapat dilakukan di ruang ini daripada sekadar gaya tarian tradisional, dan semoga artikel ini membantu Anda dalam membuat beberapa di antaranya. Jika Anda memiliki kesempatan, silakan juga periksa Beats Atom. Saya membuatnya lebih dari satu bulan di Musim Semi 2019, dan itu termasuk 8 lagu pendek dan gratis, jadi lihatlah!

Leave a Comment