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 wikiKode sumber DOOMKode Sumber Chocolate DoomPersyaratan
- 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).
- Header - berisi informasi dasar tentang file WAD dan direktori offset.
- Benjolan - di sini adalah sumber daya permainan yang tersimpan, data peta, sprite, musik, dll.
- 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
Format Direktori
Tujuan
- Buat proyek.
- Buka file WAD.
- Baca tajuk utama.
- 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);
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.
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!
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];
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) {
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.
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 sumberData 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.
- 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.
- LINEDEFS: garis yang membentuk sambungan antara simpul dan dinding pembentuk. Tidak semua garis (dinding) berperilaku sama, ada bendera yang menentukan perilaku garis tersebut.
- 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).
- 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.
- 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 ...
- 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.
- 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.
- 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.
- 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.
- 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.
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.
Format linedef
Linedef berisi lebih banyak informasi, itu menggambarkan garis yang menghubungkan dua simpul dan sifat-sifat garis ini (yang nantinya akan menjadi tembok).
Nilai Bendera Linedef
Tidak semua garis (dinding) ditarik. Beberapa dari mereka memiliki perilaku khusus.
Tujuan
- Buat kelas Peta.
- Baca data titik.
- 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();
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.Hebat!
Bagaimana dengan 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.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!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.Berikut tautannya: E1M1 Plot VertexKode sumber
Kode sumberReferensi
Doom WikiZDoom Wiki