Einführung
Das Ziel dieses Projekts ist es, einen Klon der DOOM-Engine unter Verwendung von Ressourcen zu erstellen, die mit Ultimate DOOM (
Version von Steam ) veröffentlicht wurden.
Es wird in Form eines Tutorials präsentiert. Ich möchte keine maximale Leistung im Code erzielen, sondern nur eine funktionierende Version erstellen. Später werde ich damit beginnen, sie zu verbessern und zu optimieren.
Ich habe keine Erfahrung mit dem Erstellen von Spielen oder Game-Engines und wenig Erfahrung mit dem Schreiben von Artikeln. Sie können also Ihre eigenen Änderungen vorschlagen oder den Code sogar komplett neu schreiben.
Hier ist eine Liste von Ressourcen und Links.
Book Game Engine Schwarzes Buch: DOOM Fabien Sanglar . Eines der besten Bücher über DOOM-Interna.
Doom WikiDOOM-QuellcodeQuellcode Chocolate DoomAnforderungen
- Visual Studio: Jede IDE reicht aus. Ich werde in Visual Studio 2017 arbeiten.
- SDL2: Bibliotheken.
- DOOM: Als Kopie der Steam-Version von Ultimate DOOM benötigen wir nur eine WAD-Datei.
Optional
- Slade3: Ein gutes Werkzeug, um unsere Arbeit zu testen.
Gedanken
Ich weiß nicht, ich kann dieses Projekt abschließen, aber ich werde mein Bestes geben.
Windows wird meine Zielplattform sein, aber da ich SDL verwende, funktioniert die Engine nur unter jeder anderen Plattform.
Installieren Sie in der Zwischenzeit Visual Studio!
Das Projekt wurde von Handmade DOOM in Do It Yourself Doom mit SLD (DIY Doom) umbenannt, damit es nicht mit anderen Projekten namens „Handmade“ verwechselt wird. Es gibt einige Screenshots im Tutorial, in denen es immer noch Handmade DOOM heißt.
WAD-Dateien
Bevor wir mit dem Codieren beginnen, setzen wir uns Ziele und überlegen, was wir erreichen wollen.
Lassen Sie uns zunächst prüfen, ob wir DOOM-Ressourcendateien lesen können. Alle DOOM-Ressourcen befinden sich in der WAD-Datei.
Was ist eine WAD-Datei?
"Wo sind alle meine Daten?" ("Wo sind alle meine Daten?") Sie sind in WAD! WAD ist ein Archiv aller DOOM-Ressourcen (und DOOM-basierten Spiele) in einer einzigen Datei.
Doom-Entwickler haben dieses Format entwickelt, um die Erstellung von Spielmodifikationen zu vereinfachen.
WAD-Dateianatomie
Die WAD-Datei besteht aus drei Hauptteilen: dem Header (Header), "Stücken" (Klumpen) und Verzeichnissen (Verzeichnissen).
- Header - enthält grundlegende Informationen zur WAD-Datei und zum Verzeichnisoffset.
- Klumpen - hier sind gespeicherte Spielressourcen, Daten von Karten, Sprites, Musik usw. gespeichert.
- Verzeichnisse - Die Organisationsstruktur zum Auffinden von Daten im Pauschalabschnitt.
<---- 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 | | |------------------| | | . | | | . | | | . | ---> \------------------/
Header-Format
Verzeichnisformat
Ziele
- Erstellen Sie ein Projekt.
- Öffnen Sie die WAD-Datei.
- Lesen Sie die Überschrift.
- Lesen Sie alle Verzeichnisse und zeigen Sie sie an.
Architektur
Lassen Sie uns noch nichts komplizieren. Erstellen Sie eine Klasse, die WAD gerade öffnet und lädt, und nennen Sie sie WADLoader. Dann schreiben wir eine Klasse, die abhängig von ihrem Format für das Lesen von Daten verantwortlich ist, und nennen sie WADReader. Wir brauchen auch eine einfache Hauptfunktion, die diese Klassen aufruft.
Hinweis: Diese Architektur ist möglicherweise nicht optimal. Falls erforderlich, werden wir sie ändern.
Zum Code gelangen
Beginnen wir mit der Erstellung eines leeren C ++ - Projekts. Klicken Sie in Visual Studio auf Datei-> Neu -> Projekt. Nennen wir es DIYDoom.
Fügen wir zwei neue Klassen hinzu: WADLoader und WADReader. Beginnen wir mit der Implementierung von WADLoader.
class WADLoader { public: WADLoader(std::string sWADFilePath);
Die Implementierung des Konstruktors ist einfach: Initialisieren Sie den Datenzeiger und speichern Sie eine Kopie des übertragenen Pfads in der WAD-Datei.
WADLoader::WADLoader(string sWADFilePath) : m_WADData(NULL), m_sWADFilePath(sWADFilePath) { }
OpenAndLoad
wir nun zur Implementierung der Hilfsfunktion zum Laden von
OpenAndLoad
: Wir versuchen nur, die Datei als Binärdatei zu öffnen und im Fehlerfall einen Fehler anzuzeigen.
m_WADFile.open(m_sWADFilePath, ifstream::binary); if (!m_WADFile.is_open()) { cout << "Error: Failed to open WAD file" << m_sWADFilePath << endl; return false; }
Wenn alles gut geht und wir die Datei finden und öffnen können, müssen wir die Dateigröße kennen, um Speicher zum Kopieren der Datei zuzuweisen.
m_WADFile.seekg(0, m_WADFile.end); size_t length = m_WADFile.tellg();
Jetzt wissen wir, wie viel Speicherplatz ein vollständiger WAD benötigt, und wir werden die erforderliche Speichermenge zuweisen.
m_WADData = new uint8_t[length];
Kopieren Sie den Inhalt der Datei in diesen Speicher.
Möglicherweise haben Sie bemerkt, dass ich den Typ
m_WADData
als Datentyp für
unint8_t
. Dies bedeutet, dass ich ein genaues Array von 1 Byte (1 Byte * Länge) benötige. Die Verwendung von unint8_t stellt sicher, dass die Größe einem Byte entspricht (8 Bit, was aus dem Typnamen ersichtlich ist). Wenn wir 2 Bytes (16 Bit) zuweisen wollten, würden wir unint16_t verwenden, worüber wir später sprechen werden. Durch die Verwendung dieser Codetypen wird der Code plattformunabhängig. Ich werde erklären: Wenn wir "int" verwenden, hängt die genaue Größe von int im Speicher vom System ab. Wenn wir "int" in einer 32-Bit-Konfiguration kompilieren, erhalten wir eine Speichergröße von 4 Bytes (32 Bit), und wenn wir denselben Code in einer 64-Bit-Konfiguration kompilieren, erhalten wir eine Speichergröße von 8 Bytes (64 Bit)! Schlimmer noch, wenn Sie den Code auf einer 16-Bit-Plattform kompilieren (Sie könnten ein DOS-Fan sein), erhalten Sie 2 Bytes (16 Bit)!
Lassen Sie uns den Code kurz überprüfen und sicherstellen, dass alles funktioniert. Aber zuerst müssen wir LoadWAD implementieren. Während LoadWAD "OpenAndLoad" aufruft
bool WADLoader::LoadWAD() { if (!OpenAndLoad()) { return false; } return true; }
Fügen wir dem Hauptfunktionscode hinzu, der eine Instanz der Klasse erstellt und versucht, WAD zu laden
int main() { WADLoader wadloader("D:\\SDKs\\Assets\\Doom\\DOOM.WAD"); wadloader.LoadWAD(); return 0; }
Sie müssen den richtigen Pfad zu Ihrer WAD-Datei eingeben. Lass es uns laufen!
Autsch! Wir haben ein Konsolenfenster, das sich nur für ein paar Sekunden öffnet! Nichts besonders Nützliches ... funktioniert das Programm? Die Idee! Werfen wir einen Blick auf die Erinnerung und sehen, was darin enthalten ist! Vielleicht finden wir dort etwas Besonderes! Platzieren Sie zunächst einen Haltepunkt, indem Sie links neben der Zeilennummer doppelklicken. Sie sollten so etwas sehen:
Ich habe unmittelbar nach dem Lesen aller Daten aus der Datei einen Haltepunkt gesetzt, um das Speicherarray zu überprüfen und festzustellen, was in die Datei geladen wurde. Führen Sie nun den Code erneut aus! Im automatischen Fenster sehe ich die ersten paar Bytes. Die ersten 4 Bytes sagen "IWAD"! Großartig, es funktioniert! Ich hätte nie gedacht, dass dieser Tag kommen würde! Also, okay, du musst dich beruhigen, es liegt noch viel Arbeit vor dir!
Header lesen
Die Gesamtgröße des Headers beträgt 12 Bytes (von 0x00 bis 0x0b). Diese 12 Bytes sind in 3 Gruppen unterteilt. Die ersten 4 Bytes sind eine Art von WAD, normalerweise "IWAD" oder "PWAD". IWAD sollte das offizielle WAD sein, das von ID Software veröffentlicht wurde, "PWAD" sollte für Mods verwendet werden. Mit anderen Worten, dies ist nur eine Möglichkeit festzustellen, ob die WAD-Datei eine offizielle Version ist oder von Moddern veröffentlicht wurde. Beachten Sie, dass die Zeichenfolge nicht mit NULL abgeschlossen ist. Seien Sie also vorsichtig! Die nächsten 4 Bytes sind int ohne Vorzeichen, das die Gesamtzahl der Verzeichnisse am Ende der Datei enthält. Die nächsten 4 Bytes geben den Offset des ersten Verzeichnisses an.
Fügen wir eine Struktur hinzu, in der Informationen gespeichert werden. Ich werde eine neue Header-Datei hinzufügen und sie "DataTypes.h" nennen. Darin werden wir alle Strukturen beschreiben, die wir brauchen.
struct Header { char WADType[5];
Jetzt müssen wir die WADReader-Klasse implementieren, die Daten aus dem geladenen WAD-Byte-Array liest. Autsch! Hier gibt es einen Trick: WAD-Dateien sind im Big-Endian-Format, dh wir müssen die Bytes verschieben, um sie zu Little-Endian zu machen (heutzutage verwenden die meisten Systeme Little-Endian). Dazu fügen wir zwei Funktionen hinzu, eine für die Verarbeitung von 2 Bytes (16 Bit) und eine für die Verarbeitung von 4 Bytes (32 Bit). Wenn wir nur 1 Byte lesen müssen, muss nichts getan werden.
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]; }
Jetzt können wir den Header lesen: Zählen Sie die ersten vier Bytes als Zeichen und fügen Sie dann NULL hinzu, um unsere Arbeit zu vereinfachen. Bei der Anzahl der Verzeichnisse und deren Versatz können Sie diese einfach mit Hilfsfunktionen in das richtige Format konvertieren.
void WADReader::ReadHeaderData(const uint8_t *pWADData, int offset, Header &header) {
Lassen Sie uns alles zusammenfassen, diese Funktionen aufrufen und die Ergebnisse drucken
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; }
Führen Sie das Programm aus und prüfen Sie, ob alles funktioniert!
Großartig! Die IWAD-Linie ist deutlich sichtbar, aber sind die beiden anderen Zahlen korrekt? Versuchen wir, Verzeichnisse mit diesen Offsets zu lesen und zu sehen, ob es funktioniert!
Wir müssen eine neue Struktur hinzufügen, um das Verzeichnis zu verarbeiten, das den obigen Optionen entspricht.
struct Directory { uint32_t LumpOffset; uint32_t LumpSize; char LumpName[9]; };
Fügen wir nun die ReadDirectories-Funktion hinzu: Zählen Sie den Offset und geben Sie sie aus!
In jeder Iteration multiplizieren wir i * 16, um zum Offset-Inkrement des nächsten Verzeichnisses zu gelangen.
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; }
Führen Sie den Code aus und sehen Sie, was passiert. Wow! Eine große Liste von Verzeichnissen.
Dem Namensklumpen nach zu urteilen, können wir davon ausgehen, dass wir die Daten korrekt gelesen haben, aber vielleicht gibt es einen besseren Weg, dies zu überprüfen. Wir werden uns die WAD-Verzeichniseinträge mit Slade3 ansehen.
Es scheint, dass der Name und die Größe des Klumpens den Daten entsprechen, die mit unserem Code erhalten wurden. Heute haben wir einen tollen Job gemacht!
Sonstige Hinweise
- Irgendwann dachte ich, es wäre gut, Vektor zum Speichern von Verzeichnissen zu verwenden. Warum nicht Map verwenden? Dies ist schneller als das Abrufen von Daten durch lineare Vektorsuche. Das ist eine schlechte Idee. Bei Verwendung von map wird die Reihenfolge der Verzeichniseinträge nicht verfolgt, aber wir benötigen diese Informationen, um die richtigen Daten zu erhalten.
Und noch ein Missverständnis: Map in C ++ wird als rot-schwarze Bäume mit O (log N) Suchzeit implementiert, und Iterationen über Map ergeben immer eine zunehmende Reihenfolge der Schlüssel. Wenn Sie eine Datenstruktur benötigen, die die durchschnittliche Zeit O (1) und die schlechteste Zeit O (N) angibt, müssen Sie eine ungeordnete Karte verwenden. Das Laden aller WAD-Dateien in den Speicher ist keine optimale Implementierungsmethode. Es wäre logischer, die Verzeichnisse einfach in den Speicherheader einzulesen und dann zur WAD-Datei zurückzukehren und Ressourcen von der Festplatte zu laden. Hoffentlich lernen wir eines Tages mehr über das Caching.
DOOMReboot : stimme überhaupt nicht zu. 15 MB RAM sind heutzutage eine Kleinigkeit, und das Lesen aus dem Speicher ist viel schneller als die umfangreiche Suche, die verwendet werden muss, nachdem alles heruntergeladen wurde, was für das Level erforderlich ist. Dies erhöht die Downloadzeit um nicht weniger als ein bis zwei Sekunden (ich brauche weniger als 20 ms, um die ganze Zeit herunterzuladen). Verwenden Sie das Betriebssystem. Welche Datei befindet sich am wahrscheinlichsten im RAM-Cache, aber möglicherweise nicht. Aber selbst wenn er dort ist, ist es eine große Verschwendung von Ressourcen und diese Operationen werden viele WAD-Messwerte in Bezug auf den CPU-Cache verwirren. Das Beste ist, dass Sie hybride Startmethoden erstellen und WAD-Daten für eine Ebene speichern können, die in den L3-Cache moderner Prozessoren passt, wo die Einsparungen erstaunlich sind.
Quellcode
QuellcodeGrundlegende Kartendaten
Nachdem wir gelernt haben, die WAD-Datei zu lesen, versuchen wir, die gelesenen Daten zu verwenden. Es wird großartig sein zu lernen, wie man Missionsdaten (Welt / Ebene) liest und anwendet. Die „Brocken“ dieser Missionen (Mission Lumps) sollten komplex und knifflig sein. Daher müssen wir das Wissen schrittweise bewegen und weiterentwickeln. Als ersten kleinen Schritt erstellen wir so etwas wie eine Automap-Funktion: einen zweidimensionalen Plan einer Karte mit einer Draufsicht. Lassen Sie uns zuerst sehen, was sich im Missionsklumpen befindet.
Kartenanatomie
Fangen wir noch einmal an: Die Beschreibung der DOOM-Ebenen ist der 2D-Zeichnung sehr ähnlich, auf der die Wände mit Linien markiert sind. Um 3D-Koordinaten zu erhalten, nimmt jede Wand die Höhe des Bodens und der Decke an (XY ist die Ebene, entlang der wir uns horizontal bewegen, und Z ist die Höhe, die es uns ermöglicht, uns auf und ab zu bewegen, beispielsweise durch Anheben mit einem Aufzug oder Abspringen von einer Plattform. Diese drei Die Koordinatenkomponenten werden verwendet, um die Mission als 3D-Welt zu rendern. Um jedoch eine gute Leistung zu gewährleisten, weist die Engine bestimmte Einschränkungen auf: Auf Ebenen befinden sich keine übereinander liegenden Räume, und der Spieler kann nicht nach oben und unten schauen. Ein weiteres interessantes Merkmal: Muscheln und Rock, zum Beispiel Raketen, steigen vertikal auf, um ein Ziel zu treffen, das sich auf einer höheren Plattform befindet.
Diese merkwürdigen Eigenschaften haben endlose Probleme damit verursacht, ob das DOOM eine 2D- oder eine 3D-Engine ist. Allmählich wurde ein diplomatischer Kompromiss erzielt, der viele Leben rettete: Die Parteien einigten sich auf die für beide akzeptable Bezeichnung „2.5D“.
Um die Aufgabe zu vereinfachen und zum Thema zurückzukehren, versuchen wir einfach, diese 2D-Daten zu lesen und zu prüfen, ob sie irgendwie verwendet werden können. Später werden wir versuchen, sie in 3D zu rendern, aber jetzt müssen wir verstehen, wie die einzelnen Teile der Engine zusammenarbeiten.
Nach Recherchen fand ich heraus, dass jede Mission aus einer Reihe von "Teilen" besteht. Diese „Klumpen“ werden in der WAD-Datei eines DOOM-Spiels immer in derselben Reihenfolge dargestellt.
- Scheitelpunkte: Die Endpunkte von Wänden in 2D. Zwei verbundene VERTEXs bilden einen LINEDEF. Drei verbundene VERTEX bilden zwei Wände / LINEDEF und so weiter. Sie können einfach als Verbindungspunkte von zwei oder mehr Wänden wahrgenommen werden. (Ja, die meisten Menschen bevorzugen den Plural „Vertices“, aber John Carmack hat ihn nicht gefallen. Laut Merriam-Webster gelten beide Optionen.
- LINEDEFS: Linien, die Fugen zwischen Eckpunkten und Wänden bilden. Nicht alle Linien (Wände) verhalten sich gleich. Es gibt Flags, die das Verhalten solcher Linien angeben.
- SIDEDDEFS: Im wirklichen Leben haben die Wände zwei Seiten - wir betrachten eine, die zweite ist auf der anderen Seite. Die beiden Seiten können unterschiedliche Texturen haben, und SIDEDEFS ist der Klumpen, der die Texturinformationen für die Wand (LINEDEF) enthält.
- SEKTOREN: Sektoren sind „Räume“, die vom LINEDEF-Join erhalten wurden. Jeder Sektor enthält Informationen wie Boden- und Deckenhöhen, Texturen, Beleuchtungswerte, Sonderaktionen wie das Bewegen von Böden / Plattformen / Aufzügen. Einige dieser Parameter wirken sich auch auf die Art und Weise aus, wie Wände gerendert werden, z. B. die Beleuchtungsstärke und die Berechnung der Texturabbildungskoordinaten.
- SSECTORS: (Teilsektoren) bilden konvexe Bereiche innerhalb eines Sektors, die beim Rendern in Verbindung mit dem BSP-Bypass verwendet werden, und helfen auch dabei, festzustellen, wo sich ein Spieler auf einer bestimmten Ebene befindet. Sie sind sehr nützlich und werden oft verwendet, um die vertikale Position eines Spielers zu bestimmen. Jeder SSECTOR besteht aus verbundenen Teilen eines Sektors, beispielsweise aus Wänden, die einen Winkel bilden. Solche Teile der Wände oder "Segmente" werden in einem eigenen Klumpen namens ...
- SEGS: Wandteile / LINEDEF; Mit anderen Worten, dies sind die „Segmente“ der Wand / LINEDEF. Die Welt wird umgangen, indem der BSP-Baum umgangen wird, um zu bestimmen, welche Wände zuerst gezeichnet werden sollen (die allerersten sind die nächsten). Obwohl das System sehr gut funktioniert, werden Linedefs häufig in zwei oder mehr SEGs aufgeteilt. Solche SEGs werden dann verwendet, um Wände anstelle von LINEDEF zu rendern. Die Geometrie jedes SSECTOR wird durch die darin enthaltenen Segmente bestimmt.
- NODES: Ein BSP-Knoten ist ein Knoten einer binären Baumstruktur, in der Teilsektordaten gespeichert sind. Es wird verwendet, um schnell festzustellen, welche SSECTOR (und SEG) sich vor dem Player befinden. Durch das Eliminieren von SEGs, die sich hinter dem Player befinden und daher unsichtbar sind, kann sich die Engine auf potenziell sichtbare SEGs konzentrieren, was die Renderzeit erheblich verkürzt.
- DINGE: Klumpen namens DINGE ist eine Liste von Landschafts- und Missionsakteuren (Feinde, Waffen usw.). Jedes Element dieses Knotens enthält Informationen zu einer Instanz des Akteurs / der Gruppe, z. B. den Objekttyp, den Erstellungspunkt, die Richtung usw.
- ABLEHNEN: Dieser Knoten enthält Daten darüber, welche Sektoren von anderen Sektoren aus sichtbar sind. Es wird verwendet, um zu bestimmen, wann ein Monster von der Anwesenheit eines Spielers erfährt. Es wird auch verwendet, um den Verteilungsbereich der vom Player erzeugten Sounds zu bestimmen, z. B. Aufnahmen. Wenn ein solcher Ton auf den Sektor des Monsters übertragen werden kann, kann er etwas über den Spieler erfahren. Die REJECT-Tabelle kann auch verwendet werden, um die Erkennung von Kollisionen von Waffenpatronen zu beschleunigen.
- BLOCKMAP: Informationen zur Erkennung von Spielerkollisionen und DING-Bewegung. Besteht aus einem Raster, das die Geometrie der gesamten Mission abdeckt. Jede Gitterzelle enthält eine Liste von LINEDEFs, die sich darin befinden oder diese schneiden. Es wird verwendet, um die Erkennung von Kollisionen erheblich zu beschleunigen: Kollisionsprüfungen sind nur für wenige LINEDEF pro Spieler / THING erforderlich, wodurch Rechenleistung erheblich gespart wird.
Bei der Erstellung unserer 2D-Karte konzentrieren wir uns auf VERTEXES und LINEDEFS. Wenn wir die Eckpunkte zeichnen und mit den durch linedef angegebenen Linien verbinden können, müssen wir ein 2D-Modell der Karte erstellen.
Die oben gezeigte Demokarte weist die folgenden Eigenschaften auf:
- 4 Spitzen
- Scheitelpunkt 1 in (10.10)
- Top 2 bei (10.100)
- Top 3 bei (100, 10)
- Peak 4 in (100.100)
- 4 Zeilen
- Linie von oben 1 bis 2
- Linie von oben 1 bis 3
- Linie von oben 2 bis 4
- Linie von oben 3 bis 4
Scheitelpunktformat
Wie zu erwarten ist, sind die Scheitelpunktdaten sehr einfach - nur x und y (Punkt) einiger Koordinaten.
Linedef-Format
Linedef enthält weitere Informationen: Es beschreibt die Linie, die die beiden Eckpunkte verbindet, und die Eigenschaften dieser Linie (die später zu einer Wand wird).
Linedef-Flag-Werte
Nicht alle Linien (Wände) werden gezeichnet. Einige von ihnen haben ein besonderes Verhalten.
Ziele
- Erstellen Sie eine Map-Klasse.
- Scheitelpunktdaten lesen.
- Lesen Sie die Linedef-Daten.
Architektur
Zuerst erstellen wir eine Klasse und nennen sie Map. Darin speichern wir alle mit der Karte verknüpften Daten.
Im Moment plane ich, nur Scheitelpunkte und Linedefs als Vektor zu speichern, damit ich sie später anwenden kann.
Ergänzen wir auch WADLoader und WADReader, damit wir diese beiden neuen Informationen lesen können.
Codierung
Der Code ähnelt dem WAD-Lesecode. Wir werden nur einige weitere Strukturen hinzufügen und diese dann mit Daten aus WAD füllen. Beginnen wir mit dem Hinzufügen einer neuen Klasse und der Übergabe des Kartennamens.
class Map { public: Map(std::string sName); ~Map(); std::string GetName();
Fügen Sie nun Strukturen hinzu, um diese neuen Felder zu lesen. Da wir dies bereits mehrmals getan haben, fügen Sie einfach alle auf einmal hinzu.
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; };
Als nächstes benötigen wir eine Funktion, um sie aus WADReader zu lesen. Sie wird in etwa dem entsprechen, was wir zuvor getan haben. 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); }
Ich denke, hier gibt es nichts Neues für dich. Und jetzt müssen wir diese Funktionen aus der WADLoader-Klasse aufrufen. Lassen Sie mich die Fakten darlegen: Die Reihenfolge der Klumpen ist hier wichtig. Wir finden den Namen der Karte im Klumpenverzeichnis, gefolgt von allen Klumpen, die den Karten in der angegebenen Reihenfolge zugeordnet sind. Um unsere Aufgabe zu vereinfachen und die Klumpenindizes nicht separat zu verfolgen, fügen wir eine Aufzählung hinzu, mit der wir magische Zahlen loswerden können. enum EMAPLUMPSINDEX { eTHINGS = 1, eLINEDEFS, eSIDEDDEFS, eVERTEXES, eSEAGS, eSSECTORS, eNODES, eSECTORS, eREJECT, eBLOCKMAP, eCOUNT };
Ich werde auch eine Funktion hinzufügen, um nach einer Karte anhand ihres Namens in der Verzeichnisliste zu suchen. Später werden wir wahrscheinlich die Leistung dieses Schritts durch Verwendung der Kartendatenstruktur steigern, da hier eine erhebliche Anzahl von Datensätzen vorhanden ist und wir diese häufig durchlaufen müssen, insbesondere zu Beginn des Ladens von Ressourcen wie Texturen, Sprites, Sounds usw. 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, wir sind fast fertig! Jetzt zählen wir einfach VERTEXES! Ich wiederhole, wir haben das schon einmal gemacht, jetzt müssen Sie das verstehen. 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, es sieht so aus, als würden wir ständig denselben Code kopieren. Möglicherweise müssen Sie es in Zukunft optimieren, aber vorerst implementieren Sie ReadMapLinedef selbst (oder sehen Sie sich den Quellcode über den Link an).Letzte Berührungen - wir müssen diese Funktion aufrufen und das Kartenobjekt an sie übergeben. 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; }
Jetzt ändern wir die Hauptfunktion und sehen, ob alles funktioniert. Ich möchte die Karte „E1M1“ laden, die ich auf das Kartenobjekt übertragen werde. Map map("E1M1"); wadloader.LoadMapData(map);
Lassen Sie uns jetzt alles laufen. Wow, eine Menge interessanter Zahlen, aber sind sie wahr? Lass es uns ausprobieren!Mal sehen, ob Slade uns dabei helfen kann.Wir können die Karte im Slade-Menü finden und uns die Details der Klumpen ansehen. Vergleichen wir die Zahlen.Großartig!
Was ist mit Linedef?Ich habe auch diese Aufzählung hinzugefügt, die wir beim Rendern der Karte verwenden werden. enum ELINEDEFFLAGS { eBLOCKING = 0, eBLOCKMONSTERS = 1, eTWOSIDED = 2, eDONTPEGTOP = 4, eDONTPEGBOTTOM = 8, eSECRET = 16, eSOUNDBLOCK = 32, eDONTDRAW = 64, eDRAW = 128 };
Sonstige Hinweise
Beim Schreiben des Codes habe ich fälschlicherweise mehr Bytes als nötig gelesen und falsche Werte erhalten. Zum Debuggen habe ich angefangen, den WAD-Offset im Speicher zu untersuchen, um festzustellen, ob ich den richtigen Offset hatte. Dies kann mithilfe des Visual Studio-Speicherfensters erfolgen, das ein sehr nützliches Tool zum Verfolgen von Bytes oder Speicher ist (Sie können in diesem Fenster auch Haltepunkte festlegen).Wenn das Speicherfenster nicht angezeigt wird, gehen Sie zu Debug> Speicher> Speicher.Jetzt sehen wir die Werte im Speicher hexadezimal. Diese Werte können mit der Hex-Anzeige in Slade verglichen werden, indem Sie mit der rechten Maustaste auf einen Klumpen klicken und ihn als Hex anzeigen.Vergleichen Sie sie mit der Adresse des in den Speicher geladenen WAD.Und das Letzte für heute: Wir haben all diese Scheitelpunktwerte gesehen, aber gibt es eine einfache Möglichkeit, sie zu visualisieren, ohne Code zu schreiben? Ich möchte keine Zeit damit verschwenden, nur um herauszufinden, dass wir uns in die falsche Richtung bewegen.Sicherlich hat schon jemand einen Plotter erstellt. Ich googelte „Punkte in einem Diagramm zeichnen“ und das erste Ergebnis war die Website „ Plot Points“ - Desmos . Darauf können Sie Zahlen aus der Zwischenablage einfügen, und er wird sie zeichnen. Sie müssen das Format "(x, y)" haben. Um es zu bekommen, ändern Sie einfach die Ausgabefunktion auf dem Bildschirm. cout << "(" << vertex.XPosition << "," << vertex.YPosition << ")" << endl;
Wow! Es sieht schon aus wie ein E1M1! Wir haben etwas erreicht!Wenn Sie dazu faul sind, finden Sie hier einen Link zu einem gepunkteten Diagramm: Plot Vertex .Aber machen wir noch einen Schritt: Nach ein wenig Arbeit können wir diese Punkte basierend auf Linedefs verbinden.Hier ist der Link: E1M1 Plot VertexQuellcode
QuellcodeReferenzen
Doom WikiZDoom Wiki