Game NES modern ditulis dalam bahasa mirip Lisp

What Remains adalah game petualangan naratif untuk konsol video game NES 8-bit, dirilis pada Maret 2019 sebagai ROM gratis yang berjalan di emulator. Itu dibuat oleh tim kecil Iodine Dynamics selama dua tahun secara intermiten. Saat ini, permainan sedang dalam tahap implementasi di perangkat keras: kami sedang membuat sejumlah kartrid terbatas dari bagian daur ulang.


Gim ini memiliki 6 level di mana pemain berjalan di sepanjang beberapa adegan dengan kartu gulir empat arah, berkomunikasi dengan NPC, mengumpulkan petunjuk, mengenal dunia mereka, memainkan mini-game dan menyelesaikan teka-teki sederhana. Saya adalah insinyur kepala proyek, jadi saya menemui banyak kesulitan dalam mewujudkan visi tim. Mengingat keterbatasan serius peralatan NES, cukup sulit untuk membuat game apa pun untuknya, belum lagi proyek dengan konten sebanyak di What Remains. Hanya berkat subsistem berguna yang dibuat yang memungkinkan kami menyembunyikan kompleksitas ini dan mengelolanya, kami dapat bekerja sebagai tim dan menyelesaikan permainan.


Pada artikel ini saya akan berbicara tentang beberapa detail teknis dari setiap bagian dari mesin game. Saya harap pengembang lain akan menemukan mereka berguna atau paling tidak ingin tahu.

Peralatan NES


Sebelum memulai kode, saya akan memberi tahu Anda sedikit tentang spesifikasi peralatan yang kami gunakan. NES adalah konsol game yang dirilis pada tahun 1983 (Jepang, 1985 - Amerika). Di dalamnya ada 8-bit CPU 6502 [1] dengan frekuensi 1,79 MHz. Karena konsol menghasilkan 60 frame per detik, ada sekitar 30 ribu siklus CPU per frame, dan ini cukup kecil untuk menghitung semua yang terjadi dalam siklus gameplay utama.

Selain itu, konsol memiliki total 2048 byte RAM (yang dapat diperluas menjadi 10.240 byte menggunakan RAM tambahan, yang tidak kami lakukan). Itu juga dapat mengatasi 32 KB ROM pada suatu waktu, yang dapat diperluas dengan berpindah bank (Apa yang Tetap menggunakan ROM 512 KB). Switching bank adalah topik kompleks [2] yang tidak ditangani oleh programmer modern. Singkatnya, ruang alamat yang tersedia untuk CPU kurang dari data yang terkandung dalam ROM, yaitu, ketika diaktifkan secara manual, seluruh blok memori tetap tidak dapat diakses. Apakah Anda ingin memanggil beberapa fungsi? Tidak sampai Anda mengganti bank dengan memanggil perintah switching bank. Jika ini tidak dilakukan, maka ketika fungsi dipanggil, program akan macet.

Bahkan, hal yang paling sulit ketika mengembangkan game untuk NES adalah untuk mempertimbangkan semua ini pada saat yang sama. Mengoptimalkan satu aspek dari kode, seperti penggunaan memori, seringkali dapat memengaruhi sesuatu yang lain, seperti kinerja CPU. Kode harus efektif dan pada saat yang sama nyaman dalam dukungan. Biasanya game diprogram dalam bahasa assembly.

Co2


Tetapi dalam kasus kami, tidak demikian. Sebaliknya, tandem dengan game akan mengembangkan bahasanya sendiri. Co2 adalah bahasa mirip Lisp yang dibangun di atas Racket Scheme dan dikompilasi menjadi assembler 6502. Awalnya, bahasa tersebut diciptakan oleh Dave Griffiths untuk membangun demo What Remains, dan saya memutuskan untuk menggunakannya untuk seluruh proyek.

Co2 memungkinkan Anda untuk menulis kode assembler bawaan jika perlu, tetapi juga memiliki kemampuan tingkat tinggi yang menyederhanakan beberapa tugas. Ini mengimplementasikan variabel lokal yang efektif baik dalam hal konsumsi RAM dan kecepatan akses [2]. Ini memiliki sistem makro yang sangat sederhana yang memungkinkan Anda untuk menulis yang mudah dibaca dan pada saat yang sama kode efisien [3]. Yang paling penting, karena homo-conicity dari Lisp, sangat menyederhanakan tampilan data secara langsung di sumbernya.

Menulis alat Anda sendiri cukup luas dalam pengembangan game, tetapi membuat seluruh bahasa pemrograman jauh lebih umum. Namun, kami berhasil. Tidak terlalu jelas apakah kerumitan pengembangan dan dukungan Co2 terbukti dengan sendirinya, tetapi pasti memiliki kelebihan yang membantu kami. Dalam posting saya tidak akan berbicara secara rinci tentang karya Co2 (ini pantas artikel terpisah), tetapi saya akan terus-menerus menyebutkannya, karena penggunaannya agak terkait erat dengan proses pengembangan.

Berikut adalah contoh kode Co2 yang menggambarkan latar belakang untuk adegan yang baru saja dimuat sebelum meredupkannya:

; Render the nametable for the scene at the camera position (defsub (create-initial-world) (camera-assign-cursor) (set! camera-cursor (+ camera-cursor 60)) (let ((preserve-camera-v)) (set! preserve-camera-v camera-v) (set! camera-v 0) (loop i 0 60 (set! delta-v #xff) (update-world-graphics) (when render-nt-span-has (set! render-nt-span-has #f) (apply-render-nt-span-buffer)) (when render-attr-span-has (set! render-attr-span-has #f) (apply-render-attr-span-buffer))) (set! camera-v preserve-camera-v)) (camera-assign-cursor)) 

Sistem Entitas



Gim real-time apa pun yang lebih kompleks daripada Tetris pada dasarnya adalah "sistem entitas". Ini adalah fungsi yang memungkinkan berbagai aktor independen untuk bertindak secara bersamaan dan bertanggung jawab atas kondisi mereka sendiri. Meskipun What Remains tidak berarti permainan aktif, ia masih memiliki banyak aktor independen dengan perilaku kompleks: mereka menghidupkan dan membuat sendiri, memeriksa tabrakan, dan menyebabkan dialog.

Implementasinya cukup khas: array besar berisi daftar entitas dalam adegan, setiap catatan berisi data terkait entitas bersama dengan label tipe. Fungsi pembaruan dalam siklus gameplay utama memotong semua entitas dan mengimplementasikan perilaku yang sesuai tergantung pada jenisnya.

 ; Called once per frame, to update each entity (defsub (update-entities) (when (not entity-npc-num) (return)) (loop k 0 entity-npc-num (let ((type)) (set! type (peek entity-npc-data (+ k entity-field-type))) (when (not (eq? type #xff)) (update-single-entity k type))))) 

Cara menyimpan data entitas lebih menarik. Secara umum, gim ini memiliki begitu banyak entitas unik sehingga penggunaan sejumlah besar ROM dapat menjadi masalah. Di sini Co2 menunjukkan kekuatannya, memungkinkan kami untuk menyajikan setiap esensi adegan dalam bentuk yang ringkas namun mudah dibaca - sebagai aliran pasangan nilai kunci. Selain data seperti posisi awal, hampir setiap kunci adalah opsional, yang memungkinkan mereka untuk dideklarasikan kepada entitas hanya jika diperlukan.

 (bytes npc-diner-a 172 108 prop-palette 1 prop-hflip prop-picture picture-smoker-c prop-animation simple-cycle-animation prop-anim-limit 6 prop-head hair-flip-head-tile 2 prop-dont-turn-around prop-dialog-a (2 progress-stage-4 on-my-third my-dietician) prop-dialog-a (2 progress-stage-3 have-you-tried-the-pasta the-real-deal) prop-dialog-a (2 progress-diner-is-clean omg-this-cherry-pie its-like-a-party) prop-dialog-a (2 progress-stage-1 cant-taste-food puff-poof) prop-dialog-b (1 progress-stage-4 tea-party-is-not) prop-dialog-b (1 progress-stage-3 newspaper-owned-by-dnycorp) prop-dialog-b (1 progress-stage-2 they-paid-a-pr-guy) prop-dialog-b (1 progress-stage-1 it-seems-difficult) prop-customize (progress-stage-2 stop-smoking) 0) 

Dalam kode ini, prop-palette mengatur prop-palette warna yang digunakan untuk entitas, prop-anim-limit menetapkan jumlah frame animasi, dan prop-dont-turn-around mencegah NPC berputar jika pemain mencoba untuk berbicara dengannya dari sisi lain. Ini juga menetapkan beberapa tanda kondisi yang mengubah perilaku entitas dalam proses melewati permainan oleh pemain.

Presentasi semacam ini sangat efektif untuk penyimpanan dalam ROM, tetapi sangat lambat saat diakses pada saat run time, dan akan terlalu tidak efisien untuk gameplay. Oleh karena itu, ketika pemain memasuki adegan baru, semua entitas dalam adegan ini dimuat ke dalam RAM dan memproses semua kondisi yang dapat mempengaruhi keadaan awal mereka. Tetapi Anda tidak dapat mengunduh detail apa pun untuk setiap entitas, karena akan memakan lebih banyak RAM daripada yang tersedia. Mesin memuat hanya yang paling diperlukan untuk masing-masing entitas, ditambah pointer ke struktur penuhnya dalam ROM, yang direferensikan dalam situasi seperti menangani dialog. Serangkaian kompromi khusus ini memungkinkan kami untuk memberikan tingkat kinerja yang memadai.

Portal



Permainan What Remains memiliki banyak lokasi berbeda, beberapa adegan di jalan dengan peta bergulir dan banyak adegan di ruangan yang tetap statis. Untuk berpindah dari satu ke yang lain, Anda perlu menentukan bahwa pemain telah mencapai pintu keluar, memuat adegan baru, dan kemudian menempatkan pemain pada titik yang diinginkan. Pada tahap awal pengembangan, transisi tersebut digambarkan dengan cara yang unik sebagai dua adegan yang terhubung, misalnya, "kota pertama" dan "kafe" dan data dalam pernyataan if tentang lokasi pintu di setiap adegan. Untuk menentukan di mana menempatkan pemain setelah mengubah adegan, Anda hanya perlu memeriksa di mana dia pergi dan ke mana, dan menempatkannya di sebelah pintu keluar yang sesuai.

Namun, ketika kami mulai mengisi adegan "kota kedua", yang menghubungkan ke kota pertama di dua tempat yang berbeda, sistem seperti itu mulai berantakan. Tiba-tiba, pasangan (_, _) tidak lagi cocok. Setelah memikirkan hal ini, kami menyadari bahwa koneksi itu sendiri sangat penting, yang di dalam kode permainan disebut "portal". Untuk menjelaskan perubahan ini, mesin telah ditulis ulang. yang membawa kami ke situasi seperti entitas. Portal dapat menyimpan daftar pasangan nilai kunci dan memuat di awal adegan. Saat memasuki portal, Anda dapat menggunakan informasi posisi yang sama seperti ketika pergi. Selain itu, penambahan kondisi disederhanakan, mirip dengan apa yang dimiliki entitas: pada titik-titik tertentu dalam permainan, kita dapat memodifikasi portal, misalnya, membuka atau menutup pintu.

 ; City A (bytes city-a-scene #x50 #x68 look-up portal-customize (progress-stage-5 remove-self) ; to Diner diner-scene #xc0 #xa0 look-down portal-width #x20 0) 

Ini juga menyederhanakan proses menambahkan "titik teleportasi," yang sering digunakan dalam sisipan sinematik, di mana pemain harus pindah ke yang lain dalam adegan, tergantung pada apa yang terjadi dalam plot.

Beginilah bentuk teleportasi di awal level 3:

 ; Jenny's home (bytes jenny-home-scene #x60 #xc0 look-up portal-teleport-only jenny-back-at-home-teleport 0) 

Perhatikan nilai pencarian, yang menunjukkan arah untuk "pintu masuk" ke portal ini. Saat meninggalkan portal, pemain akan melihat ke arah lain; dalam hal ini, Jenny (karakter utama permainan) ada di rumah, sambil melihat ke bawah.

Blok teks


Rendering blok teks ternyata menjadi salah satu bagian kode paling kompleks di seluruh proyek. Keterbatasan grafis NES terpaksa diakali. Untuk mulai dengan, NES hanya memiliki satu lapisan untuk data grafik, yaitu, untuk membebaskan ruang untuk blok teks, Anda perlu menghapus bagian dari peta dengan latar belakang, dan kemudian mengembalikannya setelah menutup blok teks.


Selain itu, palet untuk setiap adegan individual harus berisi warna hitam dan putih untuk rendering teks, yang memberlakukan batasan tambahan pada artis. Untuk menghindari konflik warna dengan latar belakang lainnya, blok teks harus disejajarkan dengan kisi 16 Γ— 16 [5]. Menggambar blok teks dalam adegan dengan ruangan jauh lebih sederhana daripada di jalan di mana kamera dapat bergerak, karena dalam hal ini Anda harus mempertimbangkan buffer grafis yang bergulir secara vertikal dan horizontal. Akhirnya, pesan layar jeda adalah kotak dialog standar yang sedikit dimodifikasi, karena menampilkan informasi yang berbeda, tetapi menggunakan kode yang hampir sama.

Setelah sejumlah versi kereta kode, saya akhirnya berhasil menemukan solusi di mana pekerjaan dibagi menjadi dua tahap. Pertama, semua perhitungan dilakukan yang menentukan di mana dan bagaimana cara menggambar blok teks, termasuk kode pemrosesan untuk semua kasus perbatasan. Dengan demikian, semua kesulitan ini dibawa ke satu tempat.

Kemudian, blok teks dengan pelestarian negara ditarik garis demi garis dan perhitungan dari tahap pertama digunakan agar tidak menyulitkan kode.

 ; Called once per frame as the text box is being rendered (defsub (text-box-update) (when (or (eq? tb-text-mode 0) (eq? tb-text-mode #xff)) (return #f)) (cond [(in-range tb-text-mode 1 4) (if (not is-paused) ; Draw text box for dialog. (text-box-draw-opening (- tb-text-mode 1)) ; Draw text box for pause. (text-box-draw-pausing (- tb-text-mode 1))) (inc tb-text-mode)] [(eq? tb-text-mode 4) ; Remove sprites in the way. (remove-sprites-in-the-way) (inc tb-text-mode)] [(eq? tb-text-mode 5) (if (not is-paused) ; Display dialog text. (when (not (crawl-text-update)) (inc tb-text-mode) (inc tb-text-mode)) ; Display paused text. (do (create-pause-message) (inc tb-text-mode)))] [(eq? tb-text-mode 6) ; This state is only used when paused. Nothing happens, and the caller ; has to invoke `text-box-try-exiting-pause` to continue. #t] [(and (>= tb-text-mode 7) (< tb-text-mode 10)) ; Erase text box. (if (is-scene-outside scene-id) (text-box-draw-closing (- tb-text-mode 7)) (text-box-draw-restoring (- tb-text-mode 7))) (inc tb-text-mode)] [(eq? tb-text-mode 10) ; Reset state to return to game. (set! text-displaying #f) (set! tb-text-mode 0)]) (return #t)) 

Jika Anda terbiasa dengan gaya Lisp, maka kode tersebut dibaca dengan cukup nyaman.

Sprite z-layers


Pada akhirnya, saya akan berbicara tentang detail kecil yang tidak terlalu mempengaruhi gameplay, tetapi menambahkan sentuhan yang bagus yang saya banggakan. NES hanya memiliki dua komponen grafis: tabel nama (nametable), yang digunakan untuk latar belakang statis dan grid-aligned, dan sprite - objek berukuran 8x8 piksel, yang dapat ditempatkan di tempat sewenang-wenang. Elemen-elemen seperti karakter pemain dan NPC biasanya dibuat sebagai sprite jika harus di atas grafik tabel nama.

Namun, peralatan NES juga menyediakan kemampuan untuk menentukan sebagian sprite yang dapat sepenuhnya ditempatkan di bawah tabel nama. Ini dengan mudah memungkinkan Anda untuk mewujudkan efek 3D yang keren.


Ini berfungsi sebagai berikut: palet yang digunakan untuk adegan saat ini menangani warna pada posisi 0 dengan cara khusus: itu adalah warna latar belakang global. Tabel nama digambar di atasnya, dan sprite dengan z-layer digambar di antara dua layer lainnya.

Inilah palet dari adegan ini:


Jadi, warna abu-abu gelap di sudut paling kiri digunakan sebagai warna latar belakang global.

Efek lapisan berfungsi sebagai berikut:


Di sebagian besar gim lain, ini semua berakhir, Namun, What Remains telah mengambil satu langkah lebih maju. Permainan tidak menempatkan Jenny sepenuhnya di depan atau di bawah grafik tabel nama - karakternya dibagi di antara mereka dengan cara yang benar. Seperti yang dapat Anda lihat, sprite berukuran 8x8 unit, dan grafik seluruh karakter terdiri dari beberapa sprite (dari 3 hingga 6, tergantung pada frame animasi). Setiap sprite dapat mengatur z-layer sendiri, yaitu, beberapa sprite akan berada di depan tabel nama, dan lainnya di belakangnya.

Berikut adalah contoh efek ini dalam aksi:


Algoritma untuk menerapkan efek ini cukup rumit. Pertama, data tabrakan yang mengelilingi pemain diperiksa, pada ubin tertentu, yang mungkin membutuhkan seluruh karakter untuk menggambar. Dalam diagram ini, ubin padat ditampilkan dalam kotak merah, dan ubin kuning menunjukkan bagian dengan z-layer.


Menggunakan berbagai heuristik, mereka digabungkan untuk membuat "titik referensi" dan topeng bit empat bit. Empat kuadran relatif terhadap titik referensi sesuai dengan empat bit: 0 berarti bahwa pemain harus berada di depan daftar nama, 1 - yang ada di belakangnya.


Ketika menempatkan masing-masing sprite untuk menampilkan pemain, posisinya dibandingkan dengan titik referensi untuk menentukan z-layer sprite khusus ini. Beberapa dari mereka berada di lapisan depan, yang lain di belakang.


Kesimpulan


Secara singkat saya berbicara tentang aspek-aspek berbeda dari cara kerja dalam game retro modern baru kami. Ada jauh lebih menarik dalam basis kode, tetapi saya telah menguraikan bagian penting dari apa yang membuat permainan bekerja.

Pelajaran paling penting yang saya pelajari dari proyek ini adalah manfaat yang bisa diperoleh dari mesin data driven. Beberapa kali saya berhasil mengganti beberapa logika unik dengan sebuah tabel dan mini-interpreter, dan berkat ini, kode menjadi lebih sederhana dan lebih mudah dibaca.

Saya harap Anda menikmati artikel ini!



Catatan


[1] Sebenarnya, semacam CPU 6502 yang disebut Ricoh 2A03 dipasang di NES.

[2] Faktanya, proyek ini meyakinkan saya bahwa menukar bank / mengelola ROM adalah batasan utama untuk setiap proyek SPN yang melebihi ukuran tertentu.

[3] Untuk ini, orang harus berterima kasih kepada "tumpukan yang dikompilasi" - sebuah konsep yang digunakan dalam pemrograman embedded system, walaupun saya hampir tidak berhasil menemukan literatur tentangnya. Singkatnya, Anda perlu membuat grafik lengkap panggilan proyek, mengurutkannya dari node daun ke root, dan kemudian menetapkan setiap node memori sama dengan kebutuhan + jumlah maksimum node anak.

[4] Makro ditambahkan pada tahap perkembangan agak terlambat, dan, terus terang, kami tidak dapat mengambil keuntungan khusus dari mereka.

[5] Anda dapat membaca lebih lanjut tentang grafik NES di seri artikel saya . Konflik warna disebabkan oleh atribut yang dijelaskan di bagian pertama.

Source: https://habr.com/ru/post/id467125/


All Articles