Beberapa minggu yang lalu, saya memutuskan untuk mengerjakan game untuk Game Boy, kreasi yang memberi saya kesenangan besar. Nama kerjanya adalah Aqua dan Abu. Game ini memiliki sumber terbuka dan diposting di
GitHub . Bagian sebelumnya dari artikel ada di
sini .
Sprite fantastis dan di mana mereka tinggal
Pada bagian terakhir, saya selesai merender beberapa sprite di layar. Ini dilakukan dengan cara yang sangat sewenang-wenang dan kacau. Bahkan, saya harus menunjukkan dalam kode apa dan di mana saya ingin ditampilkan. Ini membuat pembuatan animasi hampir mustahil, menghabiskan banyak waktu CPU dan dukungan kode yang rumit. Saya membutuhkan cara yang lebih baik.
Secara khusus, saya membutuhkan sistem di mana saya bisa dengan mudah mengulangi nomor animasi, nomor bingkai, dan timer untuk setiap animasi individu. Jika saya perlu mengubah animasi, saya hanya akan mengubah animasi dan mereset penghitung bingkai. Prosedur animasi yang dilakukan di setiap frame cukup dengan memilih sprite yang sesuai untuk ditampilkan dan melemparkannya di layar tanpa usaha apa pun dari saya.
Dan ternyata, tugas ini secara praktis diselesaikan. Apa yang saya cari disebut
pemetaan sprite . Peta sprite adalah struktur data yang (secara kasar) berisi daftar sprite. Setiap peta sprite berisi semua sprite untuk merender objek tunggal. Juga terkait dengan mereka adalah
peta animasi (pemetaan animasi) , yang merupakan daftar peta sprite dengan informasi tentang cara loop.
Cukup lucu bahwa pada bulan Mei, saya menambahkan editor peta animasi ke editor peta sprite yang sudah jadi untuk game Sonic 16-bit tentang Sonic. (Dia ada di
sini , kamu bisa belajar) Itu belum selesai, karena agak kasar, sangat lambat dan tidak nyaman untuk digunakan. Tetapi dari sudut pandang teknis, ini berhasil. Dan menurut
saya itu cukup keren ... (Salah satu alasan dari kekasarannya adalah saya pertama kali bekerja dengan kerangka JavaScript.) Sonic adalah game lama, jadi ini ideal sebagai fondasi untuk game baru-lama saya.
Format Kartu Sonic 2
Saya bermaksud menggunakan editor di Sonic 2 karena saya ingin membuat retasan untuk Genesis. Sonic 1 dan 3K pada dasarnya hampir sama, tetapi agar tidak menyulitkan, saya akan membatasi diri pada cerita tentang bagian kedua.
Pertama, mari kita lihat peta sprite. Inilah sprite Tail yang cukup khas, bagian dari animasi blink.
Konsol Genesis menciptakan sprite sedikit berbeda. Genteng Genesis (kebanyakan programmer menyebutnya "pola") adalah 8x8, sama seperti pada Game Boy. Sprite terdiri dari kotak hingga 4x4 ubin, mirip dengan mode sprite 8x16 pada Game Boy, tetapi lebih fleksibel. Kuncinya di sini adalah bahwa dalam memori ubin ini harus bersebelahan. Pengembang Sonic 2 ingin menggunakan kembali ubin sebanyak mungkin untuk bingkai Tails yang berkedip dari bingkai Tails yang berdiri. Oleh karena itu, Tails dibagi menjadi 2 sprite perangkat keras, terdiri dari ubin 3x2 - satu untuk kepala, yang lain untuk tubuh. Mereka ditunjukkan pada gambar di bawah ini.

Bagian atas kotak dialog ini adalah atribut sprite perangkat keras. Ini berisi posisi mereka relatif terhadap titik awal (angka negatif terputus; pada kenyataannya, ini adalah -16 dan -12 untuk sprite pertama dan -12 untuk yang kedua), ubin awal yang digunakan dalam VRAM, lebar dan tinggi sprite, serta berbagai bit status untuk gambar cermin sprite dan palet.
Ubin ditampilkan di bagian bawah karena diambil dari ROM ke VRAM. Tidak ada ruang yang cukup untuk menyimpan semua sprite Tails di VRAM, jadi ubin yang diperlukan harus disalin ke memori di setiap frame. Mereka disebut
Dynamic Load Load Isues . Namun, sementara kita dapat melewati mereka, karena mereka hampir tidak bergantung pada peta sprite, dan karena itu mereka dapat dengan mudah ditambahkan nanti.
Sedangkan untuk animasi, semuanya di sini sedikit lebih mudah. Peta animasi di Sonic adalah daftar peta sprite dengan dua potong metadata - nilai kecepatan dan tindakan yang harus diambil setelah animasi selesai. Tiga tindakan yang paling umum digunakan adalah: loop atas semua frame, loop atas frame N terakhir, atau transisi ke animasi yang sama sekali berbeda (misalnya, ketika beralih dari animasi Sonic berdiri ke animasi keinginannya menginjak-injak kakinya). Ada beberapa perintah yang menentukan flag internal di memori objek, tetapi tidak banyak objek yang menggunakannya. (Sekarang terpikir oleh saya bahwa Anda dapat mengatur bit dalam RAM objek ke nilai ketika mengulang animasi. Ini akan berguna untuk efek suara dan hal-hal lain.)
Jika Anda melihat kode
Sonic 1 yang dibongkar (kode Sonic 2 terlalu besar untuk ditautkan), Anda akan melihat bahwa tautan ke animasi tidak dibuat oleh ID apa pun. Setiap objek diberi daftar animasi, dan indeks animasi disimpan dalam memori. Untuk membuat animasi tertentu, gim ini mengambil indeks, mencarinya di daftar animasi, lalu merendernya. Ini membuat pekerjaan sedikit lebih mudah, karena Anda tidak perlu memindai animasi untuk menemukan yang Anda butuhkan.
Kami membersihkan sup dari struktur
Mari kita lihat jenis kartu:
- Peta sprite: daftar sprite yang terdiri dari ubin awal, jumlah ubin, posisi, keadaan refleksi (sprite dicerminkan atau tidak) dan palet.
- DPLC: daftar ubin ROM yang perlu dimuat ke VRAM. Setiap item dalam DPLC terdiri dari ubin awal dan panjang; setiap item ditempatkan di VRAM setelah yang terakhir.
- Peta Animasi: Daftar animasi yang terdiri dari daftar peta sprite, nilai kecepatan, dan tindakan siklus.
- Daftar animasi: Daftar petunjuk untuk tindakan setiap animasi.
Karena kami bekerja dengan Game Boy, beberapa penyederhanaan dapat dilakukan. Kita tahu bahwa dalam peta sprite dalam sprite 8x16 akan selalu ada dua ubin. Namun, semua yang lain harus dilestarikan. Untuk saat ini, kami dapat sepenuhnya meninggalkan DPLC dan menyimpan semuanya di VRAM. Ini adalah solusi sementara, tetapi, seperti yang saya katakan, masalah ini akan mudah dipecahkan. Akhirnya, kita dapat membuang nilai kecepatan jika kita mengasumsikan bahwa setiap animasi bekerja pada kecepatan yang sama.
Mari kita mulai mencari tahu bagaimana menerapkan sistem serupa di game saya.
Periksa dengan komit
2e5e5b7 !
Mari kita mulai dengan peta sprite. Setiap elemen dalam peta harus mencerminkan OAM (Object Attribute Memory - sprite VRAM) dan dengan demikian loop dan memcpy sederhana akan cukup untuk menampilkan objek. Biarkan saya mengingatkan Anda bahwa
elemen dalam OAM terdiri dari Y, X, ubin awal dan byte atribut . Saya hanya perlu membuat daftar mereka. Dengan menggunakan pseudo-operator EQU, saya menyiapkan byte atribut terlebih dahulu sehingga saya memiliki nama yang dapat dibaca untuk setiap kombinasi atribut yang mungkin. (Anda dapat melihat bahwa di komit sebelumnya, saya mengganti ubin Y / X di kartu. Ini terjadi karena saya membaca spesifikasi OAM secara tidak sengaja. Saya juga menambahkan penghitung sprite untuk mengetahui berapa lama pengulangan harus dilakukan.)
Anda akan melihat bahwa tubuh dan ekor rubah kutub disimpan secara terpisah. Jika mereka disimpan bersama, maka akan ada
banyak redundansi, karena setiap animasi harus diduplikasi untuk setiap keadaan ekor. Dan skala redundansi akan meningkat dengan cepat. Dalam Sonic 2, masalah yang sama muncul dengan Tails. Mereka menyelesaikannya di sana, membuat Tails menjadi objek terpisah dengan keadaan animasi dan timer sendiri. Saya tidak ingin melakukan ini karena saya tidak mencoba memecahkan masalah mempertahankan posisi ekor yang benar relatif terhadap rubah.
Saya memecahkan masalah melalui peta animasi. Jika Anda melihat peta animasi (tunggal) saya, maka ada tiga bagian metadata di dalamnya. Ini menunjukkan jumlah kartu animasi, jadi saya tahu kapan mereka akan berakhir. (Dalam Sonic, diperiksa bahwa animasi berikut tidak valid, mirip dengan konsep nol byte pada baris C. Solusi dari Sonic membebaskan case, tetapi menambahkan perbandingan yang akan bekerja terhadap saya.) Tentu saja, masih ada aksi loop. (Saya mengubah sirkuit Sonic 2-byte menjadi angka 1-byte di mana bit 7 adalah bit mode.) Tetapi saya juga memiliki jumlah
kartu sprite , tetapi tidak dalam Sonic. Memiliki beberapa peta sprite per bingkai animasi memungkinkan saya menggunakan kembali animasi dalam beberapa animasi, yang, menurut saya, akan menghemat banyak ruang berharga. Anda juga dapat melihat bahwa animasi digandakan untuk setiap arah. Ini karena peta untuk setiap arah berbeda, dan Anda perlu menambahkannya.
Menari dengan register
Lihat
file ini di 1713848.
Mari kita mulai dengan menggambar satu sprite di layar. Jadi, saya akui, saya berbohong. Biarkan saya mengingatkan Anda bahwa kami tidak dapat merekam pada layar di luar VBlank. Dan seluruh proses ini terlalu panjang untuk muat di VBlank. Oleh karena itu, kita perlu merekam area memori yang akan kita alokasikan untuk DMA. Pada akhirnya, itu tidak mengubah apa pun, penting untuk merekam di tempat yang tepat.
Mari kita mulai menghitung register. Prosesor GBZ80 memiliki 6 register, dari A ke E, H dan L. H dan L adalah register khusus, sehingga mereka sangat cocok untuk melakukan iterasi dari memori. (Karena mereka digunakan bersama, mereka disebut HL.) Dalam satu opcode, saya dapat menulis ke alamat memori yang terkandung dalam HL dan menambahkan satu ke dalamnya. Ini sulit ditangani. Anda dapat menggunakannya sebagai sumber atau sebagai tujuan. Saya menggunakannya sebagai alamat, dan kombinasi register BC sebagai sumber, karena itu paling nyaman. Kami hanya memiliki A, D dan E. Saya perlu mendaftar A untuk operasi matematika dan sejenisnya. Untuk apa DE bisa digunakan? Saya menggunakan D sebagai penghitung lingkaran, dan E sebagai ruang kerja. Dan di sinilah register berakhir.
Katakanlah kita memiliki 4 sprite. Kami mengatur register D (penghitung siklus) ke 4, register HL (tujuan) alamat penyangga OAM, dan BC (sumber) lokasi dalam ROM tempat kartu kami disimpan. Sekarang saya ingin memanggil memcpy. Namun, masalah kecil muncul. Ingat koordinat X dan Y? Mereka ditunjukkan relatif ke titik awal, pusat objek digunakan untuk tabrakan dan sejenisnya. Jika kita merekamnya apa adanya, maka setiap objek akan ditampilkan di sudut kiri atas layar. Ini tidak cocok untuk kita. Untuk memperbaiki ini, kita perlu menambahkan koordinat X dan Y dari objek ke X dan Y dari sprite.
Catatan singkat: Saya berbicara tentang "objek", tetapi saya tidak menjelaskan konsep ini kepada Anda. Sebuah objek hanyalah sekumpulan atribut yang terkait dengan objek dalam sebuah game. Atribut adalah posisi, kecepatan, arah. deskripsi barang, dll. Saya membicarakan hal ini karena saya perlu mengekstrak data X dan Y dari objek-objek ini.Untuk melakukan ini, kita memerlukan set register ketiga yang menunjuk ke tempat di RAM dari objek di mana koordinat berada. Dan kemudian kita perlu menyimpan X dan Y di suatu tempat.Hal yang sama berlaku untuk arah, karena membantu kita menentukan ke arah mana sprite mencari. Selain itu, kita perlu me
- render
semua objek, sehingga mereka juga membutuhkan counter loop. Dan kita belum sampai ke animasinya! Semuanya dengan cepat di luar kendali ...
Peninjauan keputusan
Jadi, saya berlari terlalu jauh ke depan. Mari kita kembali dan memikirkan setiap bagian data yang perlu saya lacak, dan di mana harus menulisnya.
Untuk memulai, mari bagi ini menjadi "langkah-langkah." Setiap langkah hanya akan menerima data untuk langkah berikutnya, kecuali yang terakhir yang melakukan penyalinan.
- Object (loop) - mencari tahu apakah objek harus dirender, dan merendernya.
- Daftar animasi - menentukan animasi yang akan ditampilkan. Juga mendapat atribut dari suatu objek.
- Animasi (loop) - menentukan daftar peta mana yang akan digunakan, dan membuat setiap peta darinya.
- Peta (siklus) - berulang melalui setiap sprite dalam daftar sprite
- Sprite - menyalin atribut sprite ke buffer OAM
Untuk setiap tahap saya telah membuat daftar variabel yang mereka butuhkan, peran yang mereka mainkan dan tempat untuk menyimpannya. Tabel ini terlihat seperti ini.
Deskripsi | Ukuran | Panggung | Gunakan | Dari mana | Tempat | Ke mana |
---|
Buffer OAM | 2 | Sprite | Pointer | Hl | Hl | |
Sumber peta | 2 | Sprite | Pointer | SM | SM | |
Byte saat ini | 1 | Sprite | Ruang kerja | Sumber peta | E | |
X | 1 | Sprite | Variabel | Hiram | A | |
Y | 1 | Sprite | Variabel | Hiram | A | |
|
Mulai dari peta animasi | 2 | Peta sprite | Pointer | Tumpukan3 | DE | |
Sumber peta | 2 | Peta sprite | Pointer | [DE] | SM | |
Sprite yang tersisa | 1 | Peta sprite | Gores | Sumber peta | D | |
Buffer OAM | 1 | Peta sprite | Pointer | Hl | Hl | Tumpukan1 |
|
Mulai dari peta animasi | 2 | Animasi | Ruang kerja | BC / Stack3 | SM | Tumpukan3 |
Kartu yang tersisa | 1 | Animasi | Ruang kerja | Animasi dimulai | Hiram | |
Total jumlah kartu | 1 | Animasi | Variabel | Animasi dimulai | Hiram | |
Arah Objek | 1 | Animasi | Variabel | Hiram | Hiram | |
Kartu per bingkai | 1 | Animasi | Variabel | Animasi dimulai | TIDAK DIGUNAKAN !!! | |
Nomor bingkai | 1 | Animasi | Variabel | Hiram | A | |
Penunjuk peta | 2 | Animasi | Pointer | AnimStart + Dir * TMC + MpF * F # | SM | DE |
Buffer OAM | 2 | Animasi | Pointer | Tumpukan1 | Hl | |
|
Mulai dari tabel animasi | 2 | Daftar Animasi | Ruang kerja | Sulit diatur | DE | |
Sumber Obyek | 2 | Daftar Animasi | Pointer | Hl | Hl | Tumpukan2 |
Nomor bingkai | 1 | Daftar Animasi | Variabel | Sumber Obyek | Hiram | |
Nomor animasi | 1 | Daftar Animasi | Ruang kerja | Sumber Obyek | A | |
Objek X | 1 | Daftar benda | Variabel | Sumber Obyek | Hiram | |
Objek Y | 1 | Daftar Animasi | Variabel | Sumber Obyek | Hiram | |
Arah Objek | 1 | Daftar Animasi | Variabel | Obj src | Hiram | |
Mulai dari peta animasi | 2 | Daftar Animasi | Pointer | [Tabel Animasi + Animasi #] | SM | |
Buffer OAM | 2 | Daftar Animasi | Pointer | DE | Tumpukan1 | |
|
Sumber Obyek | 2 | Siklus objek | Plang | Hard Set / Stack2 | Hl | |
Benda yang tersisa | 1 | Siklus objek | Variabel | Dihitung | B | |
Bidang bit aktif suatu objek | 1 | Siklus objek | Variabel | Dihitung | C | |
Buffer OAM | 2 | Siklus objek | Pointer | Sulit diatur | DE | |
Ya, sangat membingungkan. Sejujurnya, saya membuat tabel ini hanya untuk posting, untuk menjelaskan lebih jelas, tetapi sudah mulai bermanfaat. Saya akan mencoba menjelaskannya. Mari kita mulai dari akhir dan mulai dari awal. Anda akan melihat setiap bagian data yang saya mulai dengan: sumber objek, buffer OAM, dan variabel loop yang dikomputasi. Dalam setiap siklus, kita mulai dengan ini dan hanya ini, kecuali bahwa sumber objek diperbarui di setiap siklus.
Untuk setiap objek yang kami render, perlu untuk menentukan animasi yang ditampilkan. Sementara kita melakukan ini, kita juga dapat menyimpan atribut X, Y, Frame #, dan Direction sebelum menambahkan pointer objek ke objek berikutnya dan menyimpannya ke stack untuk mengambil kembali ketika keluar. Kami menggunakan nomor animasi dalam kombinasi dengan tabel animasi yang dikodekan dalam kode untuk menentukan di mana peta animasi dimulai. (Di sini saya menyederhanakan, dengan asumsi bahwa setiap objek memiliki tabel animasi yang sama. Ini membatasi saya untuk 256 animasi per game, tetapi saya tidak mungkin melebihi nilai ini.) Kami juga dapat menulis buffer OAM untuk menyimpan beberapa register.
Setelah mengekstraksi peta animasi, kita perlu menemukan di mana daftar peta sprite untuk frame dan arah yang diberikan berada, serta berapa banyak peta yang perlu dirender. Anda mungkin memperhatikan bahwa variabel kartu per bingkai tidak digunakan. Itu terjadi karena saya tidak berpikir dan menetapkan nilai konstan 2. Saya harus memperbaikinya. Kita juga perlu mengekstrak buffer OAM dari stack. Anda juga mungkin melihat kurangnya kontrol siklus. Hal ini dilakukan dalam sub-prosedur terpisah, jauh lebih sederhana, yang memungkinkan Anda untuk menyingkirkan juggling dengan register.
Setelah itu, semuanya menjadi sangat sederhana. Peta adalah sekelompok sprite, jadi kami memutarnya dalam satu lingkaran dan menggambar berdasarkan koordinat X dan Y yang disimpan. Namun, kami kembali menyimpan pointer OAM ke akhir daftar sprite sehingga peta berikutnya dimulai dari tempat kami selesai.
Apa hasil akhir dari semua ini? Persis sama seperti sebelumnya: rubah kutub melambaikan ekornya dalam gelap. Tetapi menambahkan animasi atau sprite baru sekarang jauh lebih mudah. Pada bagian selanjutnya, saya akan berbicara tentang latar belakang yang kompleks dan scroll paralaks.
Bagian 4. Latar Belakang Parallax
Biarkan saya mengingatkan Anda, pada tahap saat ini, kami memiliki animasi sprite pada latar belakang hitam pekat. Jika saya tidak berencana membuat game arcade tahun 70-an, maka ini jelas tidak akan cukup. Saya perlu semacam gambar latar belakang.
Pada bagian pertama, ketika saya menggambar grafik, saya juga membuat beberapa ubin latar belakang. Sudah saatnya menggunakannya. Kami akan memiliki tiga jenis ubin "dasar" (langit, rumput, dan bumi) dan dua ubin transisi. Semuanya dimuat ke dalam VRAM dan siap digunakan. Sekarang kita hanya perlu menuliskannya di latar belakang.
Latar belakang
Latar belakang pada Game Boy disimpan dalam memori dalam array 32x32 8x8 ubin. Setiap 32 byte sesuai dengan satu baris ubin.
Sejauh ini, saya berencana untuk mengulangi
kolom ubin yang sama di seluruh ruang 32x32. Ini hebat, tetapi ini menciptakan masalah kecil: Saya perlu mengatur
setiap ubin 32 kali berturut-turut. Ini akan menjadi waktu yang lama untuk menulis.
Secara naluriah, saya memutuskan untuk menggunakan perintah REPT untuk menambahkan 32 byte / baris, dan kemudian menggunakan memcpy untuk menyalin latar belakang ke dalam VRAM.
REPT 32 db BG_SKY ENDR REPT 32 db BG_GRASS ENDR ...
Namun, ini berarti Anda harus mengalokasikan 256 byte hanya untuk satu latar belakang, yang cukup banyak. Masalah ini diperburuk jika Anda ingat bahwa menyalin peta latar belakang yang dibuat sebelumnya dengan memcpy tidak akan memungkinkan Anda untuk menambahkan jenis kolom lainnya (misalnya, gerbang, hambatan) tanpa kerumitan yang signifikan dan tumpukan kartrid ROM yang terbuang.
Jadi sebagai gantinya, saya memutuskan untuk membuat satu kolom sebagai berikut:
db BG_SKY, BG_SKY, BG_SKY, ..., BG_GRASS
dan kemudian gunakan loop sederhana untuk menyalin setiap item dalam daftar ini sebanyak 32 kali. (lihat
LoadGFX
file LoadGFX
dari commit 739986a .)
Kemudahan dari pendekatan ini adalah nantinya saya dapat menambahkan antrian untuk menulis sesuatu seperti ini:
BGCOL_Field: db BG_SKY, ... BGCOL_LeftGoal: db BG_SKY, ... BGCOL_RightGoal: db BG_SKY, ... ... BGMAP_overview: db 1 dw BGCOL_LeftGoal db 30 dw BGCOL_Field db 1 dw BGCOL_RightGoal db $FF
Jika saya memutuskan untuk merender BGMAP_overview, maka itu akan menggambar 1 kolom LeftGoal, setelah itu akan ada 30 kolom Field dan 1 kolom RightGoal. Jika
BGMAP_overview
menggunakan RAM, maka saya dapat mengubahnya dengan cepat tergantung pada posisi kamera di X.
Kamera dan posisi
Oh ya, kameranya. Ini adalah konsep penting yang belum saya bicarakan. Di sini kita berhadapan dengan banyak koordinat, jadi sebelum berbicara tentang kamera, pertama-tama kita akan menganalisis semua ini.
Kita perlu bekerja dengan dua sistem koordinat. Yang pertama adalah
koordinat layar . Ini adalah area 256x256 yang dapat dimuat dalam VRAM konsol Game Boy. Kita dapat menggulir bagian layar yang terlihat dalam 256x256 ini, tetapi ketika kita melampaui batas, kita runtuh.
Lebar, saya perlu lebih dari 256 piksel, jadi saya menambahkan
koordinat dunia , yang dalam game ini akan memiliki dimensi 65536x256. (Saya tidak perlu ketinggian ekstra dalam Y, karena permainan berlangsung di bidang datar.) Sistem ini benar-benar terpisah dari sistem koordinat layar. Semua fisika dan tabrakan harus dilakukan dalam koordinat dunia, karena jika tidak, objek akan bertabrakan dengan objek di layar lain.
Perbandingan layar dan koordinat duniaKarena posisi semua objek direpresentasikan dalam koordinat dunia, mereka harus dikonversi menjadi koordinat layar sebelum rendering. Di ujung paling kiri dunia, koordinat dunia bertepatan dengan koordinat layar. Jika kita perlu menampilkan hal-hal di kanan di layar, maka kita perlu mengambil semua yang ada di koordinat dunia dan memindahkannya ke kiri sehingga mereka berada di koordinat layar.
Untuk melakukan ini, kita akan mengatur variabel "kamera X", yang didefinisikan sebagai batas kiri layar di dunia. Misalnya, jika
camera X
adalah 1000, maka kita dapat melihat koordinat dunia 1000-1192, karena layar yang terlihat memiliki lebar 192 piksel.
Untuk memproses objek, kita cukup mengambil posisi mereka di X (misalnya, 1002), kurangi posisi kamera sama dengan 1000, dan gambar objek pada posisi yang diberikan oleh perbedaan (dalam kasus kami, 2). Untuk latar belakang yang
tidak ada dalam koordinat dunia, tetapi sudah dijelaskan dalam koordinat layar, kami mengatur posisi sama dengan byte bawah dari variabel
camera X
. Berkat ini, latar belakang akan gulir ke kiri dan kanan dengan kamera.
Paralaks
Sistem yang kami buat terlihat agak datar. Setiap lapisan latar belakang bergerak dengan kecepatan yang sama. Itu tidak terasa tiga dimensi, dan kita perlu memperbaikinya.
Cara sederhana untuk menambahkan simulasi 3D disebut parallax scrolling. Bayangkan Anda mengemudi di jalan dan sangat lelah. Game Boy telah kehabisan baterai dan Anda harus melihat keluar dari jendela mobil. Jika Anda melihat tanah di sebelah Anda, Anda akan melihat. bahwa dia bergerak dengan kecepatan 70 mil per jam. Namun, jika Anda melihat bidang di kejauhan, tampaknya mereka bergerak jauh lebih lambat. Dan jika Anda melihat gunung yang sangat jauh, mereka tampaknya hampir tidak bergerak.
Kita dapat mensimulasikan efek ini dengan tiga lembar kertas. Jika Anda menggambar barisan gunung pada satu lembar, bidang pada lembar kedua, dan jalan pada lembar ketiga, dan meletakkannya di atas satu sama lain seperti ini. sehingga setiap lapisan terlihat, itu akan menjadi tiruan dari apa yang kita lihat dari jendela mobil. Jika kita ingin memindahkan "mobil" ke kiri, maka kita memindahkan lembar paling atas (dengan jalan) jauh ke kanan, yang berikutnya sedikit ke kanan, dan yang terakhir sedikit ke kanan.
Namun, ketika menerapkan sistem seperti itu di Game Boy, masalah kecil muncul. Konsol hanya memiliki satu lapisan latar belakang. Ini mirip dengan fakta bahwa kita hanya memiliki satu lembar kertas. Anda tidak dapat membuat efek paralaks hanya dengan satu lembar kertas. Atau mungkinkah?
B-kosong
Layar Game Boy diberikan baris demi baris. Sebagai hasil dari meniru perilaku
TV CRT lama, ada sedikit keterlambatan antara setiap baris. Bagaimana jika kita bisa menggunakannya entah bagaimana? Ternyata Game Boy memiliki interupsi perangkat keras khusus untuk tujuan ini.
Mirip dengan interupsi VBlank, yang kami terus gunakan untuk menunggu sampai akhir frame untuk merekam dalam VRAM, ada interupsi HBlank. Dengan mengatur bit 6 register pada
$FF41
, menyalakan interupsi
LCD STAT
, dan menulis nomor baris pada
$FF45
, kita bisa memberi tahu Game Boy untuk memulai interupsi
LCD STAT
ketika akan menarik garis yang ditentukan (dan ketika berada di HBlank-nya).
Selama ini, kita dapat mengubah variabel VRAM. Ini tidak
banyak waktu, jadi kami tidak dapat mengubah lebih dari beberapa register, tetapi kami masih memiliki beberapa kemungkinan. Kami ingin mengubah daftar gulir horizontal pada
$FF43
. Dalam hal ini, semua yang ada di layar di bawah garis yang ditentukan akan bergerak dengan jumlah shift tertentu, menciptakan efek paralaks.
Jika Anda kembali ke contoh gunung, Anda dapat melihat masalah potensial. Gunung, awan, dan bunga bukanlah garis datar! Kami tidak dapat memindahkan garis yang dipilih ke atas dan ke bawah selama proses rendering; jika kita memilihnya, maka itu tetap sama setidaknya sampai HBlank berikutnya. Artinya, kita hanya bisa memotong garis lurus.
Untuk mengatasi masalah ini, kita harus melakukan sedikit lebih pintar. Kami dapat mendeklarasikan beberapa garis di latar belakang sebagai garis yang tidak dapat dilewati siapa pun, yang berarti mengubah mode objek di atas dan di bawahnya, dan pemain tidak akan dapat melihat apa pun. Misalnya, di sinilah garis-garis ini berada dalam adegan dengan gunung.
Di sini saya membuat irisan tepat di atas dan di bawah gunung. Segala sesuatu dari atas ke baris pertama bergerak lambat, semuanya ke baris kedua bergerak dengan kecepatan rata-rata, dan semua yang di bawah garis ini bergerak cepat. Ini adalah trik sederhana namun cerdas. Dan mempelajarinya, Anda bisa melihatnya di banyak game retro, terutama untuk Genesis / Mega Drive, tetapi juga di konsol lain. Salah satu contoh yang paling jelas adalah
bagian dari gua dari Mickey Mania. Anda dapat melihat bahwa stalagmit dan stalaktit di latar belakang dipisahkan
persis sepanjang garis horizontal dengan batas hitam yang jelas di antara lapisan.
Saya menyadari hal yang sama di latar belakang saya. Namun, ada satu trik. Misalkan latar depan bergerak dengan kecepatan satu lawan satu bersamaan dengan pergerakan kamera, dan kecepatan latar belakang adalah sepertiga dari pergerakan piksel kamera, yaitu latar belakang bergerak seperti sepertiga latar depan. Tapi, tentu saja, sepertiga dari piksel tidak ada. Oleh karena itu, saya perlu memindahkan latar belakang satu piksel untuk setiap tiga piksel gerakan.
Jika Anda bekerja dengan komputer yang mampu perhitungan matematis, Anda akan mengambil posisi kamera, membaginya dengan 3 dan menjadikan nilai ini sebagai pengimbang latar belakang. Sayangnya, Game Boy tidak mampu melakukan pembagian, belum lagi fakta bahwa pembagian program adalah proses yang sangat lambat dan menyakitkan. Menambahkan perangkat untuk membagi (atau mengalikan) ke CPU yang lemah untuk konsol hiburan portabel di tahun 80-an sepertinya bukan langkah yang hemat biaya, jadi kami harus menemukan cara lain.
Dalam kode tersebut, saya melakukan hal berikut: alih-alih membaca posisi kamera dari suatu variabel, saya menuntutnya untuk menambah atau mengurangi. Berkat ini, dengan setiap kenaikan ketiga, saya dapat melakukan peningkatan posisi latar belakang, dan dengan setiap kenaikan pertama - peningkatan posisi latar depan. Ini menyulitkan sedikit pengguliran ke posisi dari ujung bidang yang lain (cara termudah adalah dengan hanya mengatur ulang posisi lapisan setelah transisi tertentu), tetapi menyelamatkan kita dari kebutuhan untuk membagi.
Hasil
Setelah
semua ini, saya mendapat yang berikut:
Untuk game di Game Boy, ini sebenarnya cukup keren. Sejauh yang saya tahu, tidak semua dari mereka memiliki paralaks yang diimplementasikan seperti ini.