Kebalikan dari Neuromancer. Bagian 3: Render selesai, buat game


Hai, ini adalah bagian ketiga dari serangkaian publikasi saya yang ditujukan untuk pengembangan terbalik Neuromancer - perwujudan video game dari novel dengan nama yang sama oleh William Gibson.


Kebalikan dari Neuromancer. Bagian 1: Sprite
Kebalikan dari Neuromancer. Bagian 2: Render Font

Bagian ini mungkin tampak agak kacau. Faktanya adalah bahwa sebagian besar dari apa yang dijelaskan di sini sudah siap pada saat penulisan yang sebelumnya . Sejak dua bulan telah berlalu sejak itu, dan, sayangnya, saya tidak punya kebiasaan menyimpan catatan kerja, saya hanya lupa beberapa detail. Tapi apa adanya, ayo pergi.




[Setelah saya belajar untuk mencetak garis, akan logis untuk terus membalikkan pembangunan kotak dialog. Tetapi, untuk beberapa alasan keluar dari saya, alih-alih saya benar-benar pergi ke dalam analisis sistem rendering.] Sekali lagi, berjalan di sepanjang main , saya dapat melokalisasi panggilan yang pertama kali menampilkan sesuatu di layar: seg000:0159: call sub_1D0B2 . "Apa pun", dalam hal ini, adalah kursor dan gambar latar belakang dari menu utama:




Perlu dicatat bahwa fungsi sub_1D0B2 [selanjutnya - render ] tidak memiliki argumen, namun panggilan pertamanya didahului oleh dua, bagian kode yang hampir identik:


 loc_100E5: loc_10123: mov ax, 2 mov ax, 2 mov dx, seg seg009 mov dx, seg seg010 push dx push dx push ax push ax mov ax, 506Ah mov ax, 5076h ; "cursors.imh", "title.imh" push ax push ax call load_imh call load_imh ; load_imh(res, offt, seg) add sp, 6 add sp, 6 sub ax, ax sub ax, 0Ah push ax push ax call sub_123F8 call sub_123F8 ; sub_123F8(0), sub_123F8(10) add sp, 2 add sp, 2 cmp word_5AA92, 0 mov ax, 1 jz short loc_10123 push ax sub ax, ax mov ax, 2 push ax mov dx, seg seg010 mov ax, 2 push dx mov dx, seg seg009 push ax push dx sub ax, ax push ax push ax mov ax, 64h push ax push ax mov ax 0Ah mov ax, 0A0h push ax push ax call sub_1CF5B ; sub_1CF5B(10, 0, 0, 2, seg010, 1) sub ax, ax add sp, 0Ch push ax call render call sub_1CF5B ; sub_1CF5B(0, 160, 100, 2, seg009, 0) add sp, 0Ch 

Sebelum memanggil render , kursor ( cursors.imh ) dan latar belakang ( title.imh ) dibongkar ke dalam memori ( load_imh adalah load_imh yang diubah namanya dari bagian pertama ), masing-masing, ke segmen kesembilan dan kesepuluh. Sebuah studi superfisial dari fungsi sub_123F8 tidak memberi saya informasi baru, tetapi, hanya dengan melihat argumen dari sub_1CF5B , saya membuat kesimpulan berikut:


  • argumen 4 dan 5, secara bersama-sama, mewakili alamat sprite yang dikompresi ( segment:offset );
  • argumen 2 dan 3 mungkin koordinat, karena angka-angka ini berkorelasi dengan gambar yang ditampilkan setelah memanggil render ;
  • argumen terakhir mungkin adalah bendera opacity dari latar belakang sprite, karena sprite yang dibongkar memiliki latar belakang hitam, dan kita melihat kursor di layar tanpa itu.

Dengan argumen pertama [dan bersamaan dengan rendering secara umum], semuanya menjadi jelas setelah melacak sub_1CF5B . Faktanya adalah bahwa di segmen data, dimulai dengan alamat 0x3BD4 , array 11 struktur dari tipe berikut berada:


 typedef struct sprite_layer_t { uint8_t flags; uint8_t update; uint16_t left; uint16_t top; uint16_t dleft; uint16_t dtop; imh_hdr_t sprite_hdr; uint16_t sprite_segment; uint16_t sprite_pixels; imh_hdr_t _sprite_hdr; uint16_t _sprite_segment; uint16_t _sprite_pixels; } sprite_layer_t; 

Saya menyebut konsep rantai sprite ini. Bahkan, fungsi sub_1CF5B (selanjutnya add_sprite_to_chain ) menambahkan sprite yang dipilih ke rantai. Pada mesin 16-bit, itu akan memiliki sekitar tanda tangan berikut:


 sprite_layer_t g_sprite_chain[11]; void add_sprite_to_chain(int index, uint16_t left, uint16_t top, uint16_t offset, uint16_t segment, uint8_t opaque); 

Ini berfungsi seperti ini:


  • argumen pertama adalah indeks dalam array g_sprite_chain ;
  • argumen left dan top -masing ditulis ke g_sprite_chain[index].left dan g_sprite_chain[index].top masing-masing;
  • header sprite (8 byte pertama yang terletak di segment:offset ) disalin ke bidang g_sprite_chain[index].sprite_hdr , ketik imh_hdr_t (diganti nama rle_hdr_t dari bagian pertama):

 typedef struct imh_hdr_t { uint32_t unknown; uint16_t width; uint16_t height; } imh_hdr_t; 

  • bidang g_sprite_chain[index].sprite_segment mencatat nilai segment ;
  • di bidang g_sprite_chain[index].sprite_pixels , nilai yang sama dengan offset + 8 ditulis, jadi sprite_segment:sprite_pixels adalah alamat bitmap dari sprite yang ditambahkan;
  • bidang sprite_hdr , sprite_segment , dan sprite_pixels digandakan dalam _sprite_hdr , _sprite_segment , dan _sprite_pixels masing-masing [mengapa? - Saya tidak tahu, dan ini bukan satu-satunya kasus duplikasi bidang tersebut] ;
  • di bidang g_sprite_chain[index].flags nilai sama dengan 1 + (opaque << 4) ditulis 1 + (opaque << 4) . Catatan ini berarti bahwa bit pertama dari nilai flags menunjukkan "aktivitas" dari layer "saat ini", dan bit kelima menunjukkan opacity dari latar belakangnya. [Keraguan saya tentang bendera transparansi dihapus setelah saya menguji efeknya pada gambar yang ditampilkan. Mengubah nilai bit kelima saat runtime, kita dapat mengamati artefak ini]:


Seperti yang sudah saya sebutkan, fungsi render tidak memiliki argumen, tetapi tidak memerlukannya - ia berfungsi langsung dengan array g_sprite_chain , secara bergantian mentransfer "lapisan" ke memori VGA , dari yang terakhir ( g_sprite_chain[10] - latar belakang) ke yang pertama ( g_sprite_chain[0] - foreground). Struktur sprite_layer_t memiliki semua yang diperlukan untuk ini dan bahkan lebih. Saya sedang berbicara tentang update bidang yang tidak ditinjau, dleft , dan dtop .


Bahkan, fungsi render redraws TIDAK SEMUA sprite di setiap frame. Nilai bukan-nol dari bidang g_sprite_chain.update menunjukkan bahwa sprite saat ini perlu digambar ulang. Misalkan kita memindahkan kursor ( g_sprite_chain[0] ), maka sesuatu seperti ini akan terjadi pada penangan gerakan mouse:


 void mouse_move_handler(...) { ... g_sprite_chain[0].update = 1; g_sprite_chain[0].dleft = mouse_x - g_sprite_chain[0].left; g_sprite_chain[0].dtop = mouse_y - g_sprite_chain[0].top; } 

Ketika kontrol beralih ke fungsi render , yang terakhir, setelah mencapai lapisan g_sprite_chain[0] , melihat bahwa itu perlu diperbarui. Lalu:


  • persimpangan area yang ditempati oleh sprite kursor sebelum memperbarui dengan semua lapisan sebelumnya akan dihitung dan digambar;
  • koordinat sprite akan diperbarui:

 g_sprite_chain[0].update = 0; g_sprite_chain[0].left += g_sprite_chain[0].dleft g_sprite_chain[0].dleft = 0; g_sprite_chain[0].top += g_sprite_chain[0].dtop g_sprite_chain[0].dtop = 0; 

  • sprite akan diambil pada koordinat yang diperbarui.

Ini meminimalkan jumlah operasi yang dilakukan oleh fungsi render .




Tidak sulit untuk mengimplementasikan logika ini, walaupun saya cukup menyederhanakannya. Dengan mempertimbangkan kekuatan komputasi komputer modern, kita mampu menggambar ulang semua 11 rantai sprite di setiap frame, karena ini bidang g_sprite_chain.update , .dleft , .dtop , dan semua proses yang terkait dengannya .dtop . Penyederhanaan lain menyangkut penanganan bendera opacity. Dalam kode asli, untuk setiap piksel transparan dalam sprite, persimpangan dengan piksel buram pertama di lapisan bawah dicari. Tapi saya menggunakan mode video 32-bit, dan karena itu saya bisa mengubah nilai byte transparansi dalam skema RGBA . Akibatnya, saya mendapat fungsi seperti menambahkan (menghapus) sprite ke (dari) rantai:


Kode
 typedef struct sprite_layer_t { uint8_t flags; uint16_t left; uint16_t top; imh_hdr_t sprite_hdr; uint8_t *sprite_pixels; imh_hdr_t _sprite_hdr; uint8_t *_sprite_pixels; } sprite_layer_t; sprite_layer_t g_sprite_chain[11]; void add_sprite_to_chain(int n, uint32_t left, uint32_t top, uint8_t *sprite, int opaque) { assert(n <= 10); sprite_layer_t *layer = &g_sprite_chain[n]; memset(layer, 0, sizeof(sprite_layer_t)); layer->left = left; layer->top = top; memmove(&layer->sprite_hdr, sprite, sizeof(imh_hdr_t)); layer->sprite_pixels = sprite + sizeof(imh_hdr_t); memmove(&layer->_sprite_hdr, &layer->sprite_hdr, sizeof(imh_hdr_t) + sizeof(uint8_t*)); layer->flags = ((opaque << 4) & 16) | 1; } void remove_sprite_from_chain(int n) { assert(n <= 10); sprite_layer_t *layer = &g_sprite_chain[n]; memset(layer, 0, sizeof(sprite_layer_t)); } 

Fungsi mentransfer lapisan ke buffer VGA adalah sebagai berikut:


 void draw_to_vga(int left, int top, uint32_t w, uint32_t h, uint8_t *pixels, int bg_transparency); void draw_sprite_to_vga(sprite_layer_t *sprite) { int32_t top = sprite->top; int32_t left = sprite->left; uint32_t w = sprite->sprite_hdr.width * 2; uint32_t h = sprite->sprite_hdr.height; uint32_t bg_transparency = ((sprite->flags >> 4) == 0); uint8_t *pixels = sprite->sprite_pixels; draw_to_vga(left, top, w, h, pixels, bg_transparency); } 

Fungsi draw_to_vga adalah fungsi dari nama yang sama yang dijelaskan di bagian kedua , tetapi dengan argumen tambahan yang menunjukkan transparansi latar belakang gambar. Tambahkan draw_sprite_to_vga panggilan ke awal fungsi render (sisa isinya dimigrasi dari bagian kedua ):


 static void render() { for (int i = 10; i >= 0; i--) { if (!(g_sprite_chain[i].flags & 1)) { continue; } draw_sprite_to_vga(&g_sprite_chain[i]); } ... } 

Saya juga menulis fungsi yang memperbarui posisi sprite kursor, tergantung pada posisi pointer mouse saat ini ( update_cursor ), dan manajer sumber daya sederhana. Kami membuat semua ini bekerja bersama:


 typedef enum spite_chain_index_t { SCI_CURSOR = 0, SCI_BACKGRND = 10, SCI_TOTAL = 11 } spite_chain_index_t; uint8_t g_cursors[399]; /* seg009 */ uint8_t g_background[32063]; /* seg010 */ int main(int argc, char *argv[]) { ... assert(resource_manager_load("CURSORS.IMH", g_cursors)); add_sprite_to_chain(SCI_CURSOR, 160, 100, g_cursors, 0); assert(resource_manager_load("TITLE.IMH", g_background)); add_sprite_to_chain(SCI_BACKGRND, 0, 0, g_background, 1); while (sfRenderWindow_isOpen(g_window)) { ... update_cursor(); render(); } ... } 

Kursor.GIF



Oke, untuk menu utama yang lengkap, menu itu sendiri sebenarnya tidak cukup. Saatnya untuk kembali ke pembalikan kotak dialog. [Terakhir kali, saya draw_frame fungsi draw_frame , yang membentuk kotak dialog, dan, sebagian, fungsi draw_string , hanya mengambil logika rendering teks dari sana.] Melihat pada draw_frame baru, saya melihat bahwa fungsi add_sprite_to_chain digunakan di sana - tidak ada yang mengejutkan, hanya menambahkan kotak dialog dalam rantai sprite. Itu perlu untuk berurusan dengan posisi teks di dalam kotak dialog. Biarkan saya mengingatkan Anda seperti apa panggilan ke draw_string :


  sub ax, ax push ax mov ax, 1 push ax mov ax, 5098h ; "New/Load" push ax call draw_string ; draw_string("New/Load", 1, 0) 

dan struktur mengisi draw_frame [di sini sedikit di depan, karena saya mengganti nama sebagian besar elemen setelah saya benar-benar tahu draw_string . Omong-omong, di sini, seperti dalam kasus sprite_layer_t , ada duplikasi bidang] :


 typedef struct neuro_dialog_t { uint16_t left; // word[0x65FA]: 0x20 uint16_t top; // word[0x65FC]: 0x98 uint16_t right; // word[0x65FE]: 0x7F uint16_t bottom; // word[0x6600]: 0xAF uint16_t inner_left; // word[0x6602]: 0x28 uint16_t inner_top; // word[0x6604]: 0xA0 uint16_t inner_right; // word[0x6604]: 0xA0 uint16_t inner_bottom; // word[0x6608]: 0xA7 uint16_t _inner_left; // word[0x660A]: 0x28 uint16_t _inner_top; // word[0x660C]: 0xA0 uint16_t _inner_right; // word[0x660E]: 0x77 uint16_t _inner_bottom; // word[0x6610]: 0xA7 uint16_t flags; // word[0x6612]: 0x06 uint16_t unknown; // word[0x6614]: 0x00 uint8_t padding[192] // ... uint16_t width; // word[0x66D6]: 0x30 uint16_t pixels_offset; // word[0x66D8]: 0x02 uint16_t pixels_segment; // word[0x66DA]: 0x22FB } neuro_dialog_t; 

Alih-alih menjelaskan apa yang ada di sini, bagaimana dan mengapa, saya hanya meninggalkan gambar ini:



Variabel x_offt dan y_offt masing-masing adalah argumen kedua dan ketiga untuk fungsi draw_string . Berdasarkan informasi ini, tidaklah sulit untuk membuat versi draw_frame dan draw_text Anda sendiri, setelah mengubah nama mereka menjadi build_dialog_frame dan build_dialog_text :


 void build_dialog_frame(neuro_dialog_t *dialog, uint16_t left, uint16_t top, uint16_t w, uint16_t h, uint16_t flags, uint8_t *pixels); void build_dialog_text(neuro_dialog_t *dialog, char *text, uint16_t x_offt, uint16_t y_offt); ... typedef enum spite_chain_index_t { SCI_CURSOR = 0, SCI_DIALOG = 2, ... } spite_chain_index_t; ... uint8_t *g_dialog = NULL; neuro_dialog_t g_menu_dialog; int main(int argc, char *argv[]) { ... assert(g_dialog = calloc(8192, 1)); build_dialog_frame(&g_menu_dialog, 32, 152, 96, 24, 6, g_dialog); build_dialog_text(&g_menu_dialog, "New/Load", 8, 0); add_sprite_to_chain(SCI_DIALOG, 32, 152, g_dialog, 1); ... } 


Perbedaan utama antara versi saya dan yang asli adalah bahwa saya menggunakan nilai absolut ukuran piksel - lebih mudah.




Bahkan kemudian, saya yakin bahwa bagian kode segera setelah panggilan ke build_dialog_text bertanggung jawab untuk membuat tombol:


  ... mov ax, 5098h ; "New/Load" push ax call build_dialog_text ; build_dialog_text("New/Load", 1, 0) add sp, 6 mov ax, 6Eh ; 'n' -  push ax sub ax, ax push ax mov ax, 3 push ax sub ax, ax push ax mov ax, 1 push ax call sub_181A3 ; sub_181A3(1, 0, 3, 0, 'n') add sp, 0Ah mov ax, 6Ch ; 'l' -      push ax mov ax, 1 push ax mov ax, 4 push ax sub ax, ax push ax mov ax, 5 push ax call sub_181A3 ; sub_181A3(5, 0, 4, 1, 'l') 

Semuanya tentang komentar yang dihasilkan ini - 'n' dan 'l' , yang jelas merupakan huruf pertama dalam kata "New" dan "load" . Lebih lanjut, jika kita berdebat dengan analogi dengan build_dialog_text , maka empat argumen pertama dari sub_181A3 (selanjutnya - build_dialog_item ) dapat menjadi faktor dari koordinat dan ukuran [pada kenyataannya, tiga argumen pertama, yang keempat, ternyata, tentang yang lain] . Semuanya menyatu jika Anda overlay nilai-nilai ini pada gambar sebagai berikut:



Variabel x_offt , y_offt dan width dalam gambar, masing-masing, adalah tiga argumen pertama dari fungsi build_dialog_item . Ketinggian persegi panjang ini selalu sama dengan ketinggian simbol - delapan. Setelah melihat sangat dekat pada build_dialog_item , saya menemukan bahwa apa yang saya tunjuk dalam struktur neuro_dialog_t sebagai padding (sekarang items ) adalah array 16 struktur dengan bentuk berikut:


 typedef struct dialog_item_t { uint16_t left; uint16_t top; uint16_t right; uint16_t bottom; uint16_t unknown; /* index? */ char letter; } dialog_item_t; 

Dan bidang neuro_dialog_t.unknown (sekarang - neuro_dialog_t.items_count ) adalah penghitung jumlah item dalam menu:


 typedef struct neuro_dialog_t { ... uint16_t flags; uint16_t items_count; dialog_item_t items[16]; ... } neuro_dialog_t; 

Bidang dialog_item_t.unknown diinisialisasi dengan argumen keempat ke fungsi build_dialog_item . Mungkin ini adalah indeks elemen dalam array, tetapi tampaknya ini tidak selalu terjadi, dan karenanya unknown . Bidang dialog_item_t.letter diinisialisasi dengan argumen kelima ke fungsi build_dialog_item . Sekali lagi, ada kemungkinan bahwa di penangan klik kiri permainan memeriksa koordinat penunjuk mouse di area salah satu item (misalnya mengurutkannya, misalnya), dan jika ada klik, maka penangan yang diinginkan untuk mengklik tombol tertentu dipilih dari bidang ini. [Saya tidak tahu bagaimana ini sebenarnya dilakukan, tetapi saya menerapkan logika itu dengan tepat.]


Ini cukup untuk membuat menu utama lengkap, tidak melihat kembali kode asli, tetapi hanya mengulangi perilakunya yang diamati dalam permainan.


Main_Menu.GIF



Jika Anda menonton gif sebelumnya hingga akhir, Anda mungkin memperhatikan layar permulaan game pada frame terakhir. Sebenarnya, saya sudah memiliki segalanya untuk menggambarnya. Ambil saja dan unduh sprite yang diperlukan dan tambahkan ke rantai sprite. Namun, dengan menempatkan sprite karakter utama di atas panggung, saya membuat satu penemuan penting terkait dengan struktur imh_hdr_t .


Dalam kode asli, fungsi add_sprite_to_chain , yang menambahkan gambar protagonis ke rantai, disebut dengan koordinat 156 dan 110. Inilah yang saya lihat, ulangi sendiri:



Setelah mengetahui apa yang terjadi, saya mendapatkan jenis struktur imh_hdr_t :


 typedef struct imh_hdr_t { uint16_t dx; uint16_t dy; uint16_t width; uint16_t height; } imh_hdr_t; 

Apa yang dulu merupakan bidang yang unknown ternyata merupakan nilai offset yang dikurangi dari koordinat yang sesuai (selama rendering) yang disimpan dalam rantai sprite.




Dengan demikian, koordinat nyata dari sudut kiri atas sprite yang ditarik dihitung kira-kira seperti ini:


 left = sprite_layer_t.left - sprite_layer_t.sprite_hdr.dx top = sprite_layer_t.top - sprite_layer_t.sprite_hdr.dy 

Menerapkan ini dalam kode saya, saya mendapatkan gambar yang benar, dan setelah itu saya mulai menghidupkan kembali karakter utama. Bahkan, saya menulis semua kode yang berkaitan dengan kontrol karakter (mouse dan keyboard), animasi dan gerakannya sendiri, tanpa melihat kembali ke aslinya.


Moonwalk.gif



Punya pengantar teks untuk tingkat pertama. Biarkan saya mengingatkan Anda bahwa sumber daya string disimpan dalam file .BIH . .BIH terdiri dari header ukuran variabel dan urutan string yang diakhiri dengan null. Meneliti kode asli yang memainkan intro, saya menemukan bahwa offset awal bagian teks dalam file .BIH terkandung dalam judul keempat. Baris pertama adalah intro:


 typedef struct bih_hdr_t { uint16_t unknown[3]; uint16_t text_offset; } bih_hdr_t; ... uint8_t r1_bih[12288]; assert(resource_manager_load("R1.BIH", r1_bih)); bih_hdr_t *hdr = (bih_hdr_t*)r1_bih; char *intro = r1_bih + hdr->text_offset; 

Lebih lanjut, dengan mengandalkan yang asli, saya menerapkan pemisahan string asli ke dalam substring sehingga mereka cocok di area untuk output teks, menggulir melalui baris-baris ini, dan menunggu input sebelum mengeluarkan batch berikutnya.


Intro.GIF



Pada saat publikasi, selain dari apa yang telah dijelaskan dalam tiga bagian, saya menemukan reproduksi suara. Sejauh ini ini hanya ada di kepala saya dan perlu beberapa waktu untuk menyadari hal ini di proyek saya. Jadi bagian keempat cenderung sepenuhnya tentang suara. Saya juga berencana untuk menceritakan sedikit tentang arsitektur proyek, tetapi mari kita lihat bagaimana kelanjutannya.


Kebalikan dari Neuromancer. Bagian 4: Suara, Animasi, Huffman, Github

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


All Articles