Menulis klon mesin Doom: membaca informasi peta

gambar

Pendahuluan


Tujuan dari proyek ini adalah untuk membuat tiruan dari mesin DOOM menggunakan sumber daya yang dirilis dengan Ultimate DOOM ( versi dari Steam ).

Ini akan disajikan dalam bentuk tutorial - Saya tidak ingin mencapai kinerja maksimum dalam kode, tetapi hanya membuat versi yang berfungsi, dan kemudian saya akan mulai meningkatkan dan mengoptimalkannya.

Saya tidak memiliki pengalaman dalam membuat game atau mesin game, dan sedikit pengalaman dalam menulis artikel, sehingga Anda dapat menyarankan perubahan Anda sendiri atau bahkan sepenuhnya menulis ulang kode.

Berikut adalah daftar sumber daya dan tautan.

Buku Mesin Game Buku Hitam: DOOM Fabien Sanglar . Salah satu buku terbaik tentang internal DOOM.

Doom wiki

Kode sumber DOOM

Kode Sumber Chocolate Doom

Persyaratan


  • Visual Studio: setiap IDE akan melakukan; Saya akan bekerja di Visual Studio 2017.
  • SDL2: perpustakaan.
  • DOOM: Salinan versi Steam dari Ultimate DOOM, kita hanya perlu file WAD darinya.

Opsional


  • Slade3: alat yang bagus untuk menguji pekerjaan kami.

Pikiran


Saya tidak tahu, saya bisa menyelesaikan proyek ini, tetapi saya akan melakukan yang terbaik untuk ini.

Windows akan menjadi platform target saya, tetapi karena saya menggunakan SDL, itu hanya akan membuat mesin bekerja di bawah platform lain.

Sementara itu, instal Visual Studio!

Proyek ini berganti nama dari Handmade DOOM menjadi Do It Yourself Doom dengan SLD (DIY Doom) sehingga tidak akan bingung dengan proyek lain yang disebut "Handmade". Ada beberapa tangkapan layar dalam tutorial yang masih disebut Handmade DOOM.

File WAD


Sebelum memulai pengkodean, mari tetapkan tujuan dan pikirkan apa yang ingin kita capai.

Pertama, mari kita periksa apakah kita dapat membaca file sumber daya DOOM. Semua sumber daya DOOM ada di file WAD.

Apa itu file WAD?


"Di mana semua data saya"? ("Di mana semua data saya?") Mereka ada di WAD! WAD adalah arsip semua sumber daya DOOM (dan game berbasis DOOM) yang terletak di satu file.

Pengembang malapetaka datang dengan format ini untuk menyederhanakan pembuatan modifikasi game.

WAD File Anatomy


File WAD terdiri dari tiga bagian utama: header (header), "pieces" (benjolan), dan direktori (direktori).

  1. Header - berisi informasi dasar tentang file WAD dan direktori offset.
  2. Benjolan - di sini adalah sumber daya permainan yang tersimpan, data peta, sprite, musik, dll.
  3. Direktori - Struktur organisasi untuk menemukan data di bagian lump.


  <---- 32 bits ----> /------------------\ ---> 0x00 | ASCII WAD Type | 0X03 | |------------------| Header -| 0x04 | # of directories | 0x07 | |------------------| ---> 0x08 | directory offset | 0x0B -- ---> |------------------| <-- | | 0x0C | Lump Data | | | | |------------------| | | Lumps - | | . | | | | | . | | | | | . | | | ---> | . | | | ---> |------------------| <--|--- | | Lump offset | | | |------------------| | Directory -| | directory offset | --- List | |------------------| | | Lump Name | | |------------------| | | . | | | . | | | . | ---> \------------------/ 

Format tajuk


Ukuran bidangTipe dataKonten
0x00-0x034 karakter ASCIIString ASCII (dengan nilai "IWAD" atau "PWAD").
0x04-0x07unsigned intNomor item direktori.
0x08-0x0bunsigned intNilai offset direktori dalam file WAD.

Format Direktori


Ukuran bidangTipe dataKonten
0x00-0x03unsigned intNilai offset di awal data lump di file WAD.
0x04-0x07unsigned intUkuran "benjolan" dalam byte.
0x08-0x0f8 karakter ASCIIASCII berisi nama "bagian."

Tujuan


  1. Buat proyek.
  2. Buka file WAD.
  3. Baca tajuk utama.
  4. Baca semua direktori dan tampilkan.

Arsitektur


Jangan menyulitkan apa pun. Buat kelas yang baru saja membuka dan memuat WAD, dan menyebutnya WADLoader. Kemudian kami menulis kelas yang bertanggung jawab untuk membaca data tergantung pada format mereka, dan menyebutnya WADReader. Kita juga membutuhkan fungsi main sederhana yang memanggil kelas-kelas ini.

Catatan: arsitektur ini mungkin tidak optimal, dan jika perlu kami akan mengubahnya.

Mendapatkan kode


Mari kita mulai dengan membuat proyek C ++ kosong. Di Visual Studio, klik File-> New -> Project. Sebut saja DIYDoom.


Mari kita tambahkan dua kelas baru: WADLoader dan WADReader. Mari kita mulai dengan implementasi WADLoader.

 class WADLoader { public: WADLoader(std::string sWADFilePath); // We always want to make sure a WAD file is passed bool LoadWAD(); // Will call other helper functions to open and load the WAD file ~WADLoader(); // Clean up! protected: bool OpenAndLoad(); // Open the file and load it to memory bool ReadDirectories(); // A function what will iterate though the directory section std::string m_sWADFilePath; // Sore the file name passed to the constructor std::ifstream m_WADFile; // The file stream that will pint to the WAD file. uint8_t *m_WADData; // let's load the file and keep it in memory! It is just a few MBs! std::vector<Directory> m_WADDirectories; //let's store all the directories in this vector. }; 

Menerapkan konstruktor akan sederhana: menginisialisasi penunjuk data dan menyimpan salinan jalur yang ditransfer ke file WAD.

 WADLoader::WADLoader(string sWADFilePath) : m_WADData(NULL), m_sWADFilePath(sWADFilePath) { } 

Sekarang mari kita turun ke implementasi fungsi bantu memuat OpenAndLoad : coba saja buka file sebagai biner dan jika terjadi kegagalan kita akan menampilkan kesalahan.

 m_WADFile.open(m_sWADFilePath, ifstream::binary); if (!m_WADFile.is_open()) { cout << "Error: Failed to open WAD file" << m_sWADFilePath << endl; return false; } 

Jika semuanya berjalan dengan baik, dan kita dapat menemukan dan membuka file, maka kita perlu mengetahui ukuran file untuk mengalokasikan memori untuk menyalin file ke dalamnya.

 m_WADFile.seekg(0, m_WADFile.end); size_t length = m_WADFile.tellg(); 

Sekarang kita tahu berapa banyak ruang yang dibutuhkan WAD penuh, dan kami akan mengalokasikan jumlah memori yang diperlukan.

 m_WADData = new uint8_t[length]; 

Salin konten file ke memori ini.

 // remember to know the file size we had to move the file pointer all the way to the end! We need to move it back to the beginning. m_WADFile.seekg(ifstream::beg); m_WADFile.read((char *)m_WADData, length); // read the file and place it in m_WADData m_WADFile.close(); 

Anda mungkin memperhatikan bahwa saya menggunakan tipe m_WADData sebagai tipe data untuk unint8_t . Ini berarti bahwa saya memerlukan array tepat 1 byte (panjang 1 byte *). Menggunakan unint8_t memastikan bahwa ukurannya sama dengan byte (8 bit, yang dapat dipahami dari nama tipe). Jika kita ingin mengalokasikan 2 byte (16 bit), kita akan menggunakan unint16_t, yang akan kita bicarakan nanti. Dengan menggunakan jenis kode ini, kode tersebut menjadi platform independen. Saya akan menjelaskan: jika kita menggunakan "int", maka ukuran yang tepat dari int dalam memori akan tergantung pada sistem. Jika kita mengompilasi "int" dalam konfigurasi 32-bit, kita mendapatkan ukuran memori 4 byte (32 bit), dan ketika mengkompilasi kode yang sama dalam konfigurasi 64-bit, kita mendapatkan ukuran memori 8 byte (64 bit)! Lebih buruk lagi, mengkompilasi kode pada platform 16-bit (Anda mungkin penggemar DOS) akan memberi kita 2 byte (16 bit)!

Mari kita periksa kodenya secara singkat dan memastikan semuanya berfungsi. Tetapi pertama-tama kita perlu mengimplementasikan LoadWAD. Sementara LoadWAD akan memanggil "OpenAndLoad"

 bool WADLoader::LoadWAD() { if (!OpenAndLoad()) { return false; } return true; } 

Dan mari kita tambahkan ke kode fungsi utama yang membuat instance kelas dan mencoba memuat WAD

 int main() { WADLoader wadloader("D:\\SDKs\\Assets\\Doom\\DOOM.WAD"); wadloader.LoadWAD(); return 0; } 

Anda harus memasukkan jalur yang benar ke file WAD Anda. Ayo jalankan!

Aduh! Kami mendapat jendela konsol yang hanya terbuka selama beberapa detik! Tidak ada yang sangat berguna ... apakah program ini bekerja? Idenya! Mari kita lihat ingatannya dan lihat apa yang ada di dalamnya! Mungkin di sana kita akan menemukan sesuatu yang istimewa! Pertama, letakkan breakpoint dengan mengklik dua kali di sebelah kiri nomor baris. Anda harus melihat sesuatu seperti ini:


Saya menempatkan breakpoint segera setelah membaca semua data dari file untuk melihat array memori dan melihat apa yang dimuat ke dalamnya. Sekarang jalankan kodenya lagi! Di jendela otomatis, saya melihat beberapa byte pertama. 4 byte pertama mengatakan "IWAD"! Bagus, itu berhasil! Saya tidak pernah berpikir bahwa hari ini akan datang! Jadi, oke, Anda harus tenang, masih banyak pekerjaan di depan!

Debug

Baca tajuk


Ukuran total header adalah 12 byte (dari 0x00 hingga 0x0b), 12 byte ini dibagi menjadi 3 grup. 4 byte pertama adalah jenis WAD, biasanya "IWAD" atau "PWAD". IWAD harus menjadi WAD resmi yang dirilis oleh ID Software, "PWAD" harus digunakan untuk mod. Dengan kata lain, ini hanya cara untuk menentukan apakah file WAD adalah rilis resmi, atau dirilis oleh modder. Perhatikan bahwa string tidak diakhiri NULL, jadi berhati-hatilah! 4 byte berikutnya adalah unsigned int, yang berisi jumlah total direktori di akhir file. 4 byte berikutnya menunjukkan offset dari direktori pertama.

Mari kita tambahkan struktur yang akan menyimpan informasi. Saya akan menambahkan file header baru dan beri nama "DataTypes.h". Di dalamnya kita akan menjelaskan semua struct yang kita butuhkan.

 struct Header { char WADType[5]; // I added an extra character to add the NULL uint32_t DirectoryCount; //uint32_t is 4 bytes (32 bits) uint32_t DirectoryOffset; // The offset where the first directory is located. }; 

Sekarang kita perlu mengimplementasikan kelas WADReader, yang akan membaca data dari array byte WAD yang dimuat. Aduh! Ada trik di sini - file WAD berada dalam format big-endian, yaitu, kita perlu menggeser byte untuk menjadikannya little-endian (hari ini, kebanyakan sistem menggunakan little endian). Untuk melakukan ini, kita akan menambahkan dua fungsi, satu untuk memproses 2 byte (16 bit), yang lain untuk memproses 4 byte (32 bit); jika kita hanya perlu membaca 1 byte, maka tidak ada yang perlu dilakukan.

 uint16_t WADReader::bytesToShort(const uint8_t *pWADData, int offset) { return (pWADData[offset + 1] << 8) | pWADData[offset]; } uint32_t WADReader::bytesToInteger(const uint8_t *pWADData, int offset) { return (pWADData[offset + 3] << 24) | (pWADData[offset + 2] << 16) | (pWADData[offset + 1] << 8) | pWADData[offset]; } 

Sekarang kita siap untuk membaca tajuk: hitung empat byte pertama sebagai char, dan kemudian tambahkan NULL untuk menyederhanakan pekerjaan kita. Dalam hal jumlah direktori dan ofsetnya, Anda cukup menggunakan fungsi bantu untuk mengubahnya menjadi format yang benar.

 void WADReader::ReadHeaderData(const uint8_t *pWADData, int offset, Header &header) { //0x00 to 0x03 header.WADType[0] = pWADData[offset]; header.WADType[1] = pWADData[offset + 1]; header.WADType[2] = pWADData[offset + 2]; header.WADType[3] = pWADData[offset + 3]; header.WADType[4] = '\0'; //0x04 to 0x07 header.DirectoryCount = bytesToInteger(pWADData, offset + 4); //0x08 to 0x0b header.DirectoryOffset = bytesToInteger(pWADData, offset + 8); } 

Mari kita satukan semuanya, panggil fungsi-fungsi ini dan cetak hasilnya

 bool WADLoader::ReadDirectories() { WADReader reader; Header header; reader.ReadHeaderData(m_WADData, 0, header); std::cout << header.WADType << std::endl; std::cout << header.DirectoryCount << std::endl; std::cout << header.DirectoryOffset << std::endl; std::cout << std::endl << std::endl; return true; } 

Jalankan program dan lihat apakah semuanya berfungsi!


Hebat! Garis IWAD terlihat jelas, tetapi apakah dua angka lainnya benar? Mari kita coba membaca direktori menggunakan offset ini dan lihat apakah itu berfungsi!

Kita perlu menambahkan struct baru untuk menangani direktori yang sesuai dengan opsi di atas.

 struct Directory { uint32_t LumpOffset; uint32_t LumpSize; char LumpName[9]; }; 

Sekarang mari kita tambahkan fungsi ReadDirectories: hitung offset dan outputnya!

Dalam setiap iterasi, kita mengalikan i * 16 untuk menuju peningkatan offset direktori berikutnya.

 Directory directory; for (unsigned int i = 0; i < header.DirectoryCount; ++i) { reader.ReadDirectoryData(m_WADData, header.DirectoryOffset + i * 16, directory); m_WADDirectories.push_back(directory); std::cout << directory.LumpOffset << std::endl; std::cout << directory.LumpSize << std::endl; std::cout << directory.LumpName << std::endl; std::cout << std::endl; } 

Jalankan kode dan lihat apa yang terjadi. Wow! Daftar besar direktori.

Jalankan 2

Menilai dengan nama gumpalan, kita dapat mengasumsikan bahwa kita berhasil membaca data dengan benar, tetapi mungkin ada cara yang lebih baik untuk memeriksa ini. Kami akan melihat entri Direktori WAD menggunakan Slade3.


Tampaknya nama dan ukuran benjolan sesuai dengan data yang diperoleh dengan menggunakan kode kami. Hari ini kami melakukan pekerjaan yang hebat!

Catatan lain


  • Pada titik tertentu, saya pikir akan lebih baik menggunakan vektor untuk menyimpan direktori. Mengapa tidak menggunakan Peta? Ini akan lebih cepat daripada mendapatkan data dengan pencarian vektor linier. Ini ide yang buruk. Saat menggunakan peta, urutan entri direktori tidak akan dilacak, tetapi kami membutuhkan informasi ini untuk mendapatkan data yang benar.

    Dan kesalahpahaman lain: Peta di C ++ diimplementasikan sebagai pohon merah-hitam dengan waktu pencarian O (log N), dan iterasi pada peta selalu memberikan urutan kunci yang meningkat. Jika Anda membutuhkan struktur data yang memberikan waktu rata-rata O (1) dan waktu terburuk O (N), maka Anda harus menggunakan peta yang tidak terurut.
  • Memuat semua file WAD ke dalam memori bukan metode implementasi yang optimal. Akan lebih logis untuk hanya membaca direktori ke header memori, dan kemudian kembali ke file WAD dan memuat sumber daya dari disk. Semoga suatu hari nanti kita akan belajar lebih banyak tentang caching.

    DOOMReboot : sama sekali tidak setuju. 15 MB RAM saat ini adalah hal yang agak lengkap, dan membaca dari memori akan jauh lebih cepat daripada fseek yang besar, yang harus digunakan setelah mengunduh semua yang diperlukan untuk level tersebut. Ini akan menambah waktu pengunduhan tidak kurang dari satu hingga dua detik (saya butuh waktu kurang dari 20 mdetik untuk mengunduh sepanjang waktu). fseek menggunakan OS. File mana yang paling mungkin dalam cache RAM, tetapi mungkin tidak. Tetapi bahkan jika dia ada di sana, itu adalah pemborosan sumber daya yang besar dan operasi ini akan membingungkan banyak pembacaan WAD dalam hal cache CPU. Yang terbaik adalah Anda dapat membuat metode boot hybrid dan menyimpan data WAD untuk level yang sesuai dengan cache L3 prosesor modern, di mana penghematannya akan luar biasa.

Kode sumber


Kode sumber

Data Kartu Dasar


Setelah belajar membaca file WAD, mari kita coba menggunakan data baca. Akan luar biasa mempelajari cara membaca data misi (dunia / tingkat) dan menerapkannya. "Potongan" dari misi ini (Mission Lumps) harus menjadi sesuatu yang rumit dan rumit. Karena itu, kita perlu memindahkan dan mengembangkan pengetahuan secara bertahap. Sebagai langkah kecil pertama, mari kita buat sesuatu seperti fitur Automap: rencana dua dimensi peta dengan tampilan atas. Pertama, mari kita lihat apa yang ada di dalam Mission Lump.

Anatomi kartu


Mari kita mulai lagi: deskripsi level DOOM sangat mirip dengan gambar 2D, yang dindingnya ditandai dengan garis. Namun, untuk mendapatkan koordinat 3D, setiap dinding membutuhkan ketinggian lantai dan langit-langit (XY adalah bidang di mana kita bergerak secara horizontal, dan Z adalah ketinggian yang memungkinkan kita untuk bergerak ke atas dan ke bawah, misalnya, dengan mengangkat lift atau melompat dari platform. Ketiga ini komponen koordinat digunakan untuk membuat misi sebagai dunia 3D, namun, untuk memastikan kinerja yang baik, mesin memiliki batasan tertentu: tidak ada kamar yang terletak satu di atas yang lain di tingkat dan pemain tidak dapat melihat ke atas dan ke bawah. Fitur menarik lainnya: kerang dan Rock, misalnya, roket, naik secara vertikal untuk mengenai target yang terletak pada platform yang lebih tinggi.

Fitur-fitur yang aneh ini telah menyebabkan holival tak berujung tentang apakah DOOM adalah mesin 2D atau 3D. Secara bertahap, kompromi diplomatik tercapai, yang menyelamatkan banyak nyawa: para pihak menyetujui penunjukan "2.5D" yang dapat diterima oleh keduanya.

Untuk menyederhanakan tugas dan kembali ke topik, mari kita coba membaca data 2D ini dan melihat apakah itu dapat digunakan entah bagaimana. Nanti kita akan mencoba membuatnya dalam 3D, tetapi untuk sekarang kita perlu memahami bagaimana masing-masing bagian mesin bekerja bersama.

Setelah melakukan penelitian, saya menemukan bahwa setiap misi terdiri dari satu set "potongan". "Benjolan" ini selalu direpresentasikan dalam file WAD game DOOM dalam urutan yang sama.

  1. Vertex: Titik akhir dinding dalam 2D. Dua VERTEX yang terhubung membentuk satu LINEDEF. Tiga VERTEX yang terhubung membentuk dua dinding / LINEDEF, dan seterusnya. Mereka hanya dapat dianggap sebagai titik koneksi dari dua dinding atau lebih. (Ya, kebanyakan orang lebih suka β€œVertices” jamak, tetapi John Carmack tidak menyukainya. Menurut merriam-webster , kedua opsi berlaku.
  2. LINEDEFS: garis yang membentuk sambungan antara simpul dan dinding pembentuk. Tidak semua garis (dinding) berperilaku sama, ada bendera yang menentukan perilaku garis tersebut.
  3. SIDEDDEFS: dalam kehidupan nyata, dinding memiliki dua sisi - kita melihat satu, yang kedua di sisi lain. Kedua belah pihak dapat memiliki tekstur yang berbeda, dan SIDEDEFS adalah benjolan yang berisi informasi tekstur untuk dinding (LINEDEF).
  4. SEKTOR: sektor adalah "kamar" yang diperoleh oleh bergabung dengan LINEDEF. Setiap sektor berisi informasi seperti ketinggian lantai dan langit-langit, tekstur, nilai pencahayaan, tindakan khusus, seperti lantai / platform / elevator yang bergerak. Beberapa parameter ini juga mempengaruhi cara dinding diberikan, misalnya, tingkat pencahayaan dan perhitungan koordinat pemetaan tekstur.
  5. SSECTORS: (subsektor) membentuk area cembung dalam suatu sektor yang digunakan dalam rendering bersama dengan bypass BSP, dan juga membantu menentukan di mana pemain berada pada level tertentu. Mereka sangat berguna dan sering digunakan untuk menentukan posisi vertikal pemain. Setiap SSECTOR terdiri dari bagian-bagian sektor yang terhubung, misalnya, dinding yang membentuk sudut. Bagian dinding seperti itu, atau "ruas," disimpan di Lump mereka sendiri yang disebut ...
  6. SEGS: bagian dinding / LINEDEF; dengan kata lain, ini adalah "segmen" dari dinding / LINEDEF. Dunia diberikan melewati pohon BSP untuk menentukan dinding mana yang akan digambar pertama (yang paling pertama adalah yang paling dekat). Meskipun sistem bekerja dengan sangat baik, ini menyebabkan linedefs sering terpecah menjadi dua atau lebih SEG. SEG seperti itu kemudian digunakan untuk membuat dinding daripada LINEDEF. Geometri setiap SSECTOR ditentukan oleh segmen yang terkandung di dalamnya.
  7. NODES: Node BSP adalah node dari struktur pohon biner yang menyimpan data subsektor. Ini digunakan untuk dengan cepat menentukan SSECTOR (dan SEG) yang ada di depan pemain. Menghilangkan SEG yang terletak di belakang pemain, dan karenanya tidak terlihat, memungkinkan engine untuk fokus pada SEG yang berpotensi terlihat, yang secara signifikan mengurangi waktu render.
  8. HAL: Benjolan HAL yang disebut adalah daftar pemandangan dan misi aktor (musuh, senjata, dll). Setiap elemen dari benjolan ini berisi informasi tentang satu instance dari aktor / set, misalnya, jenis objek, titik penciptaan, arah, dan sebagainya.
  9. Tolak: benjolan ini berisi data tentang sektor mana yang terlihat dari sektor lain. Ini digunakan untuk menentukan kapan monster mengetahui tentang kehadiran pemain. Ini juga digunakan untuk menentukan rentang distribusi suara yang dibuat oleh pemain, misalnya, bidikan. Ketika suara seperti itu dapat ditransmisikan ke sektor monster, dia bisa belajar tentang pemain. Tabel REJECT juga dapat digunakan untuk mempercepat pengenalan tumbukan peluru senjata.
  10. BLOCKMAP: informasi pengenalan tabrakan pemain dan gerakan HAL. Terdiri dari kisi yang mencakup geometri seluruh misi. Setiap sel kisi berisi daftar LINEDEF yang ada di dalam atau memotongnya. Ini digunakan untuk secara signifikan mempercepat pengenalan tabrakan: pemeriksaan tabrakan diperlukan hanya untuk beberapa LINEDEF untuk setiap pemain / HAL, yang secara signifikan menghemat daya komputasi.

Saat membuat peta 2D kami, kami akan fokus pada VERTEX dan LINEDEFS. Jika kita bisa menggambar simpul dan menghubungkannya dengan garis yang diberikan oleh linedef, maka kita perlu membuat model peta 2D.

Demo peta

Kartu demo yang ditunjukkan di atas memiliki karakteristik sebagai berikut:

  • 4 puncak
    • simpul 1 dalam (10.10)
    • 2 teratas di (10.100)
    • 3 teratas di (100, 10)
    • puncak 4 dalam (100.100)
  • 4 baris
    • baris dari atas 1 ke 2
    • garis dari atas 1 ke 3
    • garis dari atas 2 ke 4
    • baris dari 3 ke 4 teratas

Format vertex


Seperti yang Anda harapkan, data vertex sangat sederhana - hanya x dan y (titik) dari beberapa koordinat.

Ukuran bidangTipe dataKonten
0x00-0x01Ditandatangani pendekPosisi X
0x02-0x03Ditandatangani pendekPosisi Y

Format linedef


Linedef berisi lebih banyak informasi, itu menggambarkan garis yang menghubungkan dua simpul dan sifat-sifat garis ini (yang nantinya akan menjadi tembok).

Ukuran bidangTipe dataKonten
0x00-0x01Tidak bertanda pendekMulai puncak
0x02-0x03Tidak bertanda pendekPuncak tertinggi
0x04-0x05Tidak bertanda pendekBendera (lihat di bawah untuk detail lebih lanjut)
0x06-0x07Tidak bertanda pendekJenis Garis / Tindakan
0x08-0x09Tidak bertanda pendekLabel sektor
0x10-0x11Tidak bertanda pendekSisi depan (0xFFFF - tidak ada sisi)
0x12-0x13Tidak bertanda pendekSisi belakang (0xFFFF - tidak ada sisi)

Nilai Bendera Linedef


Tidak semua garis (dinding) ditarik. Beberapa dari mereka memiliki perilaku khusus.

SedikitDeskripsi
0Memblokir jalan bagi pemain dan monster
1Blokir monster
2Dua sisi
3Tekstur atas dinonaktifkan (kami akan membicarakannya nanti)
4Tekstur bawah dinonaktifkan (kita akan membicarakan ini nanti)
5Rahasia (ditampilkan di peta sebagai dinding satu sisi)
6Menghambat suara
7Tidak pernah ditampilkan di autocard
8Selalu ditampilkan di autocard

Tujuan


  1. Buat kelas Peta.
  2. Baca data titik.
  3. Baca data linedef.

Arsitektur


Pertama, mari kita buat kelas dan menyebutnya peta. Di dalamnya kami akan menyimpan semua data yang terkait dengan kartu.

Untuk saat ini, saya berencana untuk hanya menyimpan simpul dan garis sebagai vektor, sehingga saya bisa menerapkannya nanti.

Juga, mari kita melengkapi WADLoader dan WADReader sehingga kita dapat membaca dua informasi baru ini.

Coding


Kode akan mirip dengan kode pembacaan WAD, kami hanya akan menambahkan beberapa struktur lagi, dan kemudian mengisinya dengan data dari WAD. Mari kita mulai dengan menambahkan kelas baru dan meneruskan nama peta.

 class Map { public: Map(std::string sName); ~Map(); std::string GetName(); // Incase someone need to know the map name void AddVertex(Vertex &v); // Wrapper class to append to the vertexes vector void AddLinedef(Linedef &l); // Wrapper class to append to the linedef vector protected: std::string m_sName; std::vector<Vertex> m_Vertexes; std::vector<Linedef> m_Linedef; }; 

Sekarang tambahkan struktur untuk membaca bidang baru ini. Karena kita sudah melakukan ini beberapa kali, cukup tambahkan semuanya sekaligus.

 struct Vertex { int16_t XPosition; int16_t YPosition; }; struct Linedef { uint16_t StartVertex; uint16_t EndVertex; uint16_t Flags; uint16_t LineType; uint16_t SectorTag; uint16_t FrontSidedef; uint16_t BackSidedef; }; 

Selanjutnya, kita perlu fungsi untuk membacanya dari WADReader, itu akan dekat dengan apa yang kita lakukan sebelumnya.

 void WADReader::ReadVertexData(const uint8_t *pWADData, int offset, Vertex &vertex) { vertex.XPosition = Read2Bytes(pWADData, offset); vertex.YPosition = Read2Bytes(pWADData, offset + 2); } void WADReader::ReadLinedefData(const uint8_t *pWADData, int offset, Linedef &linedef) { linedef.StartVertex = Read2Bytes(pWADData, offset); linedef.EndVertex = Read2Bytes(pWADData, offset + 2); linedef.Flags = Read2Bytes(pWADData, offset + 4); linedef.LineType = Read2Bytes(pWADData, offset + 6); linedef.SectorTag = Read2Bytes(pWADData, offset + 8); linedef.FrontSidedef = Read2Bytes(pWADData, offset + 10); linedef.BackSidedef = Read2Bytes(pWADData, offset + 12); } 

Saya pikir tidak ada yang baru untuk Anda di sini. Dan sekarang kita perlu memanggil fungsi-fungsi ini dari kelas WADLoader. Biarkan saya nyatakan faktanya: urutan benjolan itu penting di sini, kita akan menemukan nama peta di direktori benjolan, diikuti oleh semua benjolan yang terkait dengan peta dalam urutan yang diberikan. Untuk menyederhanakan tugas kami dan tidak melacak indeks gumpalan secara terpisah, kami akan menambahkan enumerasi yang memungkinkan kami untuk menghilangkan angka ajaib.

 enum EMAPLUMPSINDEX { eTHINGS = 1, eLINEDEFS, eSIDEDDEFS, eVERTEXES, eSEAGS, eSSECTORS, eNODES, eSECTORS, eREJECT, eBLOCKMAP, eCOUNT }; 

Saya juga akan menambahkan fungsi untuk mencari peta dengan namanya di daftar direktori. Nantinya, kita cenderung meningkatkan kinerja langkah ini dengan menggunakan struktur data peta, karena ada sejumlah besar catatan di sini, dan kita harus sering melewatinya, terutama pada awal pemuatan sumber daya seperti tekstur, sprite, suara, dll.

 int WADLoader::FindMapIndex(Map &map) { for (int i = 0; i < m_WADDirectories.size(); ++i) { if (m_WADDirectories[i].LumpName == map.GetName()) { return i; } } return -1; } 

Wow, kita hampir selesai! Sekarang, mari kita hitung VERTEX! Saya ulangi, kami sudah melakukan ini sebelumnya, sekarang Anda harus mengerti ini.

 bool WADLoader::ReadMapVertex(Map &map) { int iMapIndex = FindMapIndex(map); if (iMapIndex == -1) { return false; } iMapIndex += EMAPLUMPSINDEX::eVERTEXES; if (strcmp(m_WADDirectories[iMapIndex].LumpName, "VERTEXES") != 0) { return false; } int iVertexSizeInBytes = sizeof(Vertex); int iVertexesCount = m_WADDirectories[iMapIndex].LumpSize / iVertexSizeInBytes; Vertex vertex; for (int i = 0; i < iVertexesCount; ++i) { m_Reader.ReadVertexData(m_WADData, m_WADDirectories[iMapIndex].LumpOffset + i * iVertexSizeInBytes, vertex); map.AddVertex(vertex); cout << vertex.XPosition << endl; cout << vertex.YPosition << endl; std::cout << std::endl; } return true; } 

Hmm, sepertinya kita terus-menerus menyalin kode yang sama; Anda mungkin harus mengoptimalkannya di masa mendatang, tetapi untuk saat ini Anda akan mengimplementasikan ReadMapLinedef sendiri (atau melihat kode sumber dari tautan).

Sentuhan akhir - kita perlu memanggil fungsi ini dan meneruskan objek peta ke sana.

 bool WADLoader::LoadMapData(Map &map) { if (!ReadMapVertex(map)) { cout << "Error: Failed to load map vertex data MAP: " << map.GetName() << endl; return false; } if (!ReadMapLinedef(map)) { cout << "Error: Failed to load map linedef data MAP: " << map.GetName() << endl; return false; } return true; } 

Sekarang mari kita ubah fungsi utama dan lihat apakah semuanya berfungsi. Saya ingin memuat peta "E1M1", yang akan saya transfer ke objek peta.

  Map map("E1M1"); wadloader.LoadMapData(map); 

Sekarang mari kita jalankan semuanya. Wow, banyak nomor yang menarik, tetapi apakah itu benar? Mari kita periksa!

Mari kita lihat apakah slade dapat membantu kita dengan ini.

Kita dapat menemukan peta di menu slade dan melihat detail gumpalan. Mari kita bandingkan jumlahnya.

Vertex

Hebat!

Bagaimana dengan Linedef?

Linedef

Saya juga menambahkan enumerasi ini, yang akan kami coba gunakan saat merender peta.

 enum ELINEDEFFLAGS { eBLOCKING = 0, eBLOCKMONSTERS = 1, eTWOSIDED = 2, eDONTPEGTOP = 4, eDONTPEGBOTTOM = 8, eSECRET = 16, eSOUNDBLOCK = 32, eDONTDRAW = 64, eDRAW = 128 }; 

Catatan lain


Dalam proses penulisan kode, saya keliru membaca lebih banyak byte daripada yang diperlukan, dan menerima nilai yang salah. Untuk debugging, saya mulai melihat offset WAD dalam memori untuk melihat apakah saya berada di offset yang tepat. Ini dapat dilakukan dengan menggunakan jendela memori Visual Studio, yang merupakan alat yang sangat berguna untuk melacak byte atau memori (Anda juga dapat mengatur breakpoint di jendela ini).

Jika Anda tidak melihat jendela memori, buka Debug> Memori> Memori.


Sekarang kita melihat nilai dalam memori dalam heksadesimal. Nilai-nilai ini dapat dibandingkan dengan tampilan hex dalam slade dengan mengklik kanan pada setiap benjolan dan menampilkannya sebagai hex.

Slade

Bandingkan mereka dengan alamat WAD yang dimuat ke dalam memori.


Dan hal terakhir untuk hari ini: kami melihat semua nilai titik ini, tetapi apakah ada cara mudah untuk memvisualisasikannya tanpa menulis kode? Saya tidak ingin membuang waktu untuk hal ini, hanya untuk mengetahui bahwa kita bergerak ke arah yang salah.

Tentunya seseorang sudah membuat plotter. Saya mencari "menggambar poin pada grafik" di Google dan hasil pertama adalah situs Plot Points - Desmos . Di atasnya, Anda dapat menempelkan angka dari clipboard, dan dia akan menggambarnya. Mereka harus dalam format "(x, y)". Untuk mendapatkannya, cukup ubah fungsi output ke layar.

 cout << "(" << vertex.XPosition << "," << vertex.YPosition << ")" << endl; 

Wow! Itu sudah terlihat seperti E1M1! Kami telah mencapai sesuatu!

Poin Plot E1M1

Jika Anda malas melakukan ini, berikut adalah tautan ke bagan bertitik: Plot Vertex .

Tapi mari kita ambil satu langkah lagi: setelah sedikit kerja, kita bisa menghubungkan titik-titik ini berdasarkan linedefs.

E1M1 Plot Vertex

Berikut tautannya: E1M1 Plot Vertex

Kode sumber


Kode sumber

Referensi


Doom Wiki

ZDoom Wiki

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


All Articles