Pendahuluan
Jadi, Anda ingin atau mencoba membuat permainan ritme, tetapi elemen-elemen permainan dan musik dengan cepat tidak sinkron, dan sekarang Anda tidak tahu apa yang harus dilakukan. Artikel ini akan membantu Anda dengan ini. Saya memainkan permainan ritme dari sekolah menengah dan sering nongkrong di DDR di ruang arcade lokal. Hari ini saya selalu mencari permainan baru dari genre ini, dan proyek-proyek seperti
Crypt of the Necrodancer atau
Bit.Trip.Runner menunjukkan bahwa lebih banyak yang dapat dilakukan dalam genre ini. Saya bekerja sedikit pada prototipe permainan ritme di Unity, dan sebagai hasilnya saya menghabiskan waktu satu bulan untuk membuat permainan ritme pendek / puzzle
Atomic Beats . Pada artikel ini, saya akan berbicara tentang teknik pembuatan kode paling berguna yang saya pelajari dalam membuat game ini. Saya tidak dapat menemukan informasi tentang mereka di tempat lain, atau disajikan dengan kurang detail.
Pertama, saya harus mengucapkan terima kasih yang
mendalam kepada Yu Chao untuk posisi
Music Syncing di Rhythm Games [
terjemahan ke Habré ]. Yu meninjau dasar-dasar sinkronisasi waktu audio dengan mesin game di Unity dan mengunggah kode sumber untuk game Boots-Cut-nya, yang banyak membantu saya dalam menciptakan proyek saya. Anda dapat mempelajari posisinya jika Anda ingin mempelajari pengantar singkat tentang sinkronisasi musik Unity, tetapi saya akan membahas topik ini secara lebih rinci dan lebih luas. Kode saya secara aktif menggunakan informasi dari artikel dan kode Boots-Cut.
Inti dari setiap permainan ritme adalah pengaturan waktu. Orang-orang sangat sensitif terhadap distorsi dalam pengaturan ritme, jadi sangat penting bahwa semua aksi, gerakan, dan input dalam permainan ritme disinkronkan secara langsung dengan musik. Sayangnya, metode pelacakan waktu Unity tradisional seperti
Time.timeSinceLevelLoad dan
Time.time dengan cepat kehilangan sinkronisasi dengan suara yang sedang diputar. Oleh karena itu, kita akan mengakses sistem audio secara langsung menggunakan
AudioSettings.dspTime , yang menggunakan jumlah sebenarnya sampel audio yang diproses oleh sistem audio. Berkat ini, ia selalu menjaga sinkronisasi dengan musik yang diputar ulang (mungkin ini tidak terjadi dengan file audio yang sangat panjang, ketika efek sampling ikut bermain, tetapi dalam kasus lagu-lagu dengan panjang normal, sistem harus bekerja dengan sempurna). Fungsi ini akan menjadi inti dari pelacakan waktu komposisi kami, dan berdasarkan itu kami akan membuat kelas utama.
Konduktor kelas
Kelas Konduktor adalah kelas manajemen komposisi utama yang akan digunakan untuk membangun sisa permainan ritme. Dengan itu, kami akan melacak posisi komposisi dan mengelola semua tindakan yang disinkronkan lainnya. Untuk melacak komposisi kita perlu beberapa variabel
Saat memulai adegan, kita perlu melakukan perhitungan untuk menentukan variabel, dan juga merekam untuk referensi waktu mulai komposisi.
void Start() {
Jika Anda membuat GameObject kosong dengan skrip seperti itu, dan kemudian menambahkan
Sumber Audio dengan komposisi dan menjalankan program, Anda akan melihat bahwa skrip akan mencatat waktu mulai komposisi, tetapi tidak ada hal lain yang akan terjadi. Kita juga perlu memasukkan BPM musik secara manual yang kita tambahkan ke Sumber Audio.
Berkat semua nilai ini, kami dapat melacak posisi dalam komposisi secara real time saat memperbarui game. Kami akan menentukan waktu komposisi, pertama dalam detik, kemudian dalam fraksi. Pecahan adalah cara yang jauh lebih mudah untuk melacak komposisi, karena mereka memungkinkan kita untuk menambahkan tindakan dan waktu tepat waktu bersamaan dengan komposisi, katakanlah, dalam pecahan 1, 3 dan 5,5, tanpa perlu menghitung detik antara pecahan. Tambahkan perhitungan berikut ke fungsi Pembaruan () dari kelas konduktor:
void Update() {
Jadi kita mendapatkan perbedaan antara waktu saat ini sesuai dengan sistem audio dan waktu mulai komposisi, yang memberikan total jumlah detik komposisi dimainkan. Kami akan menyimpannya dalam variabel SongPosition.
Perhatikan bahwa skor dalam musik biasanya dimulai dengan unit dengan fraksi 1-2-3-4 dan seterusnya, dan songPositionInBeats dimulai pada 0 dan meningkat dari nilai ini, sehingga bagian ketiga dari komposisi akan sesuai dengan songPositionInBeats, yaitu 2.0, bukan 3.0.
Pada titik ini, jika Anda ingin membuat gim bergaya Dance Dance Revolution tradisional, maka Anda perlu membuat catatan sesuai dengan fraksi yang Anda perlukan untuk menekannya, menginterpolasi posisi mereka relatif terhadap garis klik, dan kemudian merekam songPositionInBeats ketika kunci ditekan, dan Bandingkan nilainya dengan proporsi catatan yang diinginkan. Yu Chao membahas contoh skema semacam itu dalam
artikelnya . Agar tidak mengulang sendiri, saya akan mempertimbangkan teknik lain yang berpotensi bermanfaat yang dapat dibangun di atas kelas Konduktor. Saya menggunakannya saat membuat
Beats Atom .
Kami beradaptasi dengan bagian awal
Jika Anda membuat musik sendiri untuk permainan ritme, mudah untuk membuat ketukan pertama persis dengan permulaan musik, yang, jika ditentukan dengan benar, akan secara andil mengikat lagu kelas ConductorPosisiInBeats dengan komposisi.
Namun, jika Anda menggunakan musik yang sudah jadi, maka ada kemungkinan besar bahwa ada sedikit jeda sebelum awal komposisi. Jika ini tidak diperhitungkan, maka lagu kelas ConductorPosisiInBeats akan berpikir bahwa ketukan pertama dimulai ketika lagu mulai diputar, dan bukan ketukan sekarang. Segala sesuatu yang akan lebih terikat dengan nilai-nilai saham tidak disinkronkan dengan musik.
Untuk memperbaikinya, Anda dapat menambahkan variabel yang mempertimbangkan offset ini ke akun. Tambahkan berikut ini ke kelas Konduktor:
Dalam Pembaruan (), variabel SongPosition:
songPosition = (float)(AudioSettings.dspTime - dspSongTime);
diganti oleh:
songPosition = (float)(AudioSettings.dspTime - dspSongTime - firstBeatOffset);
Sekarang songPosition akan menghitung posisi dalam lagu dengan benar, dengan mempertimbangkan beat pertama yang sebenarnya. Namun, Anda harus memasukkan offset secara manual ke beat pertama, jadi untuk setiap file itu akan unik. Selain itu, selama perubahan ini akan ada jendela pendek di mana songPosition akan berubah menjadi negatif. Ini mungkin tidak memengaruhi gim, tetapi beberapa kode, tergantung pada nilai songPosition atau songPositionInBeats, mungkin tidak dapat memproses angka negatif saat ini.

Pengulangan
Jika Anda bekerja dengan komposisi yang diputar dari awal hingga akhir, maka kelas Konduktor yang ditunjukkan di atas akan cukup untuk melacak posisi. Tetapi jika Anda memiliki trek pendek yang dilingkarkan dan Anda ingin bekerja dengan loop ini, Anda perlu membangun dukungan Repeater di Konduktor.
Jika Anda memiliki fragmen loop sempurna (misalnya, jika tempo lagu 120bpm, dan fragmen loop memiliki panjang 4 ketukan, maka itu harus tepat 8,0 detik pada 2,0 detik per saham) dimuat ke kelas Audio Source dari kelas Conductor, lalu centang kotak loop. Konduktor akan bekerja dengan cara yang sama seperti sebelumnya, dan mentransfer total waktu ke SongPosition setelah awal
pertama klip. Untuk menentukan posisi loop, kita perlu memberi tahu konduktor berapa banyak share dalam satu loop dan berapa banyak loop yang sudah dimainkan. Tambahkan variabel berikut ke kelas Konduktor:
Sekarang dengan setiap pembaruan ke SongPositionInBeats, kami juga dapat memperbarui posisi Pembaruan () dari loop.
Ini memberi kami penanda yang memberi tahu loopPositionInBeats berapa banyak saham yang kami lewati, yang berguna untuk banyak item yang disinkronkan lainnya. Ingatlah untuk memasukkan jumlah saham loop di GameObject Conductor.
Kami juga harus hati-hati mempertimbangkan perhitungan saham. Musik selalu dimulai pada 1, sehingga pengukuran 4-bagian mengambil bentuk 1-2-3-4-, dan di loop kelas kamiPosisiInBeats dimulai pada 0,0 dan loop lebih dari 4,0. Oleh karena itu, tepat tengah loop, yang ketika menghitung proporsi musik akan menjadi 3, di loopPositionInBeats akan memiliki nilai 2,0. Anda dapat memodifikasi loopPositionInBeats untuk memperhitungkan ini, tetapi ini akan memengaruhi semua perhitungan lainnya, jadi berhati-hatilah saat memasukkan catatan.
Juga untuk alat yang tersisa, akan berguna untuk menambahkan dua aspek lagi ke kelas Konduktor. Pertama, versi analog dari LoopPositionInBeats disebut LoopPositionInAnalog, yang mengukur posisi dalam loop dalam rentang dari 0 hingga 1.0. Yang kedua adalah turunan dari kelas Konduktor untuk panggilan yang nyaman dari kelas lain. Tambahkan variabel berikut ke kelas Konduktor:
Dalam fungsi Awake (), tambahkan:
void Awake() { instance = this; }
dan tambahkan ke fungsi Pembaruan ():
loopPositionInAnalog = loopPositionInBeats / beatsPerLoop;
Putar Sinkronisasi
Akan sangat berguna untuk menyinkronkan gerakan atau rotasi dengan lobus sehingga elemen-elemen berada di tempat yang tepat. Dalam permainan Atomic Beats saya, saya menggunakan ini untuk secara dinamis memutar catatan di sekitar poros tengah. Awalnya, mereka ditempatkan di sekitar lingkar sesuai dengan bagian mereka di dalam lingkaran, dan kemudian seluruh area bermain diputar sehingga catatan disesuaikan dengan garis depresi di bagian mereka.
Untuk mencapai ini, buat skrip baru bernama SyncedRotation, dan lampirkan ke GameObject yang ingin Anda putar. Tambahkan ke fungsi Pembaruan () dari skrip SyncedRotation:
void Update() { this.gameObject.transform.rotation = Quaternion.Euler(0, 0, Mathf.Lerp(0, 360, Conductor.instance.loopPositionInAnalog)); }
Kode ini akan menginterpolasi rotasi GameObject yang mengikat game ini dalam interval dari 0 hingga 360 derajat, memutarnya sehingga menyelesaikan satu revolusi penuh pada akhir setiap loop. Ini berguna sebagai contoh, tetapi untuk animasi looping atau frame-by-frame, akan lebih berguna untuk menyinkronkan animasi loop sehingga cocok dengan tempo.
Sinkronisasi Animasi
Unity
Animator sangat kuat, tetapi tidak selalu akurat. Untuk penyelarasan animasi dan musik yang andal, saya harus bersaing dengan kelas Animator dan kecenderungannya untuk secara bertahap menyinkronkan dengan langkahnya. Selain itu, sulit untuk menyesuaikan animasi yang sama dengan tempo yang berbeda, sehingga ketika beralih di antara komposisi, Anda tidak perlu mendefinisikan ulang bingkai kunci animasi ke tempo saat ini. Sebagai gantinya, kita dapat langsung menuju loop animasi, dan mengatur posisi dalam loop ini sesuai dengan posisi kita dalam loop dari kelas Konduktor.
Pertama, buat kelas baru bernama SyncedAnimation, dan tambahkan variabel berikut ke dalamnya:
Lampirkan ke GameObject baru atau yang sudah ada yang ingin Anda menghidupkan. Dalam contoh ini, kita hanya akan memindahkan objek bolak-balik melintasi layar, tetapi prinsip yang sama dapat diterapkan pada animasi apa pun, baik itu sebelum mengatur properti, atau animasi frame-by-frame. Tambahkan elemen Animator ke GameObject dan buat
Pengendali Animator baru bernama SyncedAnimController, serta
Klip Animasi yang disebut BackAndForth. Kami memuat pengontrol ke dalam kelas Animator yang melekat pada GameObject, dan menambahkan Animasi ke pohon animasi sebagai animasi default.
Sebagai contoh, saya mengatur animasi sehingga pertama-tama memindahkan objek ke kanan sebanyak 6 unit, lalu ke kiri sebesar -6, dan kemudian kembali ke 0.
Sekarang, untuk menyinkronkan animasi, tambahkan kode berikut ke fungsi Mulai () dari kelas SyncedAnimation, yang menginisialisasi informasi tentang Animator:
void Start() {
Kemudian tambahkan kode berikut ke Pembaruan () untuk mengatur animasi:
void Update() {
Jadi kami memposisikan animasi dalam bingkai yang tepat dari animasi relatif terhadap satu loop penuh. Misalnya, jika Anda menggunakan animasi di atas, ketika Anda berada di tengah-tengah loop, posisi GameObject hanya akan menyeberang 0. Ini dapat diterapkan untuk setiap animasi yang Anda buat yang ingin Anda sinkronkan dengan tempo Konduktor.
Perlu juga dicatat bahwa untuk membuat loop animasi yang mulus, Anda perlu mengonfigurasi garis singgung dari masing-masing
kerangka kunci animasi pada kurva animasi. Pengaturan Linear akan membuat garis lurus dari satu frame kunci ke yang berikutnya, dan Constant akan menjaga animasi dalam satu nilai sampai frame kunci berikutnya, yang akan memberikan gerakan tersentak-sentak dan tajam.
Meskipun metode ini berguna, ini mempengaruhi semua transisi animasi, karena menyebabkan
animasiState tetap dalam keadaan di mana itu ketika skrip awalnya dijalankan. Metode ini berguna untuk objek yang hanya perlu menggunakan satu animasi yang disinkronkan, tetapi untuk membuat objek yang lebih kompleks dengan animasi tersinkronisasi yang berbeda, Anda perlu menambahkan kode yang memproses transisi ini dan mengatur variabel CurrentState sesuai dengan keadaan animasi yang diinginkan.
Kesimpulan
Ini hanya beberapa aspek yang telah membantu saya dalam menciptakan Beats Atom. Beberapa dari mereka dikumpulkan dari sumber lain atau dibuat karena kebutuhan, tetapi kebanyakan dari mereka tidak dapat saya temukan dalam bentuk yang sudah jadi, jadi saya harap ini berguna! Mungkin bagian dari sistem saya tidak lagi berguna dalam proyek-proyek besar karena keterbatasan CPU atau sistem audio, tetapi itu akan menjadi dasar yang baik untuk memainkan permainan macet atau proyek hobi.

Membuat gim ritme, atau elemen gim yang disinkronkan dengan musik, bisa jadi sulit. Untuk menjaga semuanya pada kecepatan yang konsisten, Anda mungkin memerlukan kode yang rumit, hasil yang memungkinkan Anda bermain dengan kecepatan konstan bisa sangat menarik bagi pemain. Jauh lebih banyak yang dapat dilakukan dalam genre ini daripada permainan dalam gaya Dance Dance Revolution tradisional, dan saya harap artikel ini membantu Anda mewujudkan proyek-proyek tersebut. Saya juga merekomendasikan, jika mungkin, mengevaluasi permainan
Atomic Beats saya. Saya membuatnya dalam satu bulan di musim semi tahun ini, ia memiliki 8 trek pendek dan gratis!