Écriture d'un clone de moteur Doom: lecture des informations cartographiques

image

Présentation


L'objectif de ce projet est de créer un clone du moteur DOOM en utilisant les ressources publiées avec Ultimate DOOM ( version de Steam ).

Il sera présenté sous la forme d'un tutoriel - je ne veux pas atteindre des performances maximales dans le code, mais juste créer une version de travail, et plus tard je commencerai à l'améliorer et à l'optimiser.

Je n'ai aucune expérience dans la création de jeux ou de moteurs de jeu, et peu d'expérience dans la rédaction d'articles, vous pouvez donc suggérer vos propres modifications ou même réécrire complètement le code.

Voici une liste de ressources et de liens.

Livre Game Engine Black Book: DOOM Fabien Sanglar . L'un des meilleurs livres sur les internes de DOOM.

Wiki Doom

Code source DOOM

Code source Chocolate Doom

Prérequis


  • Visual Studio: n'importe quel IDE fera l'affaire; Je travaillerai dans Visual Studio 2017.
  • SDL2: bibliothèques.
  • DOOM: une copie de la version Steam de Ultimate DOOM, nous n'avons besoin que d'un fichier WAD.

En option


  • Slade3: un bon outil pour tester notre travail.

Réflexions


Je ne sais pas, je peux terminer ce projet, mais je ferai de mon mieux pour cela.

Windows sera ma plate-forme cible, mais comme j'utilise SDL, cela fera simplement fonctionner le moteur sous n'importe quelle autre plate-forme.

En attendant, installez Visual Studio!

Le projet a été renommé de Handmade DOOM en Do It Yourself Doom avec SLD (DIY Doom) afin de ne pas être confondu avec d'autres projets appelés «Handmade». Il y a quelques captures d'écran dans le tutoriel où il s'appelle toujours Handmade DOOM.

Fichiers WAD


Avant de nous lancer dans le codage, fixons des objectifs et réfléchissons à ce que nous voulons réaliser.

Tout d'abord, vérifions si nous pouvons lire les fichiers de ressources DOOM. Toutes les ressources DOOM se trouvent dans le fichier WAD.

Qu'est-ce qu'un fichier WAD?


"Où sont toutes mes données"? ("Où sont toutes mes données?") Elles sont dans WAD! WAD est une archive de toutes les ressources DOOM (et des jeux basés sur DOOM) situées dans un seul fichier.

Les développeurs de Doom ont proposé ce format pour simplifier la création de modifications de jeu.

Anatomie du fichier WAD


Le fichier WAD se compose de trois parties principales: l'en-tête (en-tête), les «pièces» (grumeaux) et les répertoires (répertoires).

  1. En-tête - contient des informations de base sur le fichier WAD et le décalage de répertoire.
  2. Montants - voici des ressources de jeu stockées, des données de cartes, de sprites, de musique, etc.
  3. Répertoires - La structure organisationnelle pour rechercher des données dans la section forfaitaire.


  <---- 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 d'en-tête


Taille du champType de donnéesLe contenu
0x00-0x034 caractères ASCIIChaîne ASCII (avec les valeurs "IWAD" ou "PWAD").
0x04-0x07entier non signéNuméro d'élément du répertoire.
0x08-0x0bentier non signéValeur de décalage du répertoire dans le fichier WAD.

Format d'annuaire


Taille du champType de donnéesLe contenu
0x00-0x03entier non signéLa valeur de décalage au début des données forfaitaires dans le fichier WAD.
0x04-0x07entier non signéLa taille de la "pièce" (bloc) en octets.
0x08-0x0f8 caractères ASCIIASCII contenant le nom "pièce".

Buts


  1. Créez un projet.
  2. Ouvrez le fichier WAD.
  3. Lisez le titre.
  4. Lisez tous les répertoires et affichez-les.

L'architecture


Ne compliquons rien encore. Créez une classe qui ouvre et charge simplement WAD, et appelez-la WADLoader. Ensuite, nous écrivons une classe qui est responsable de la lecture des données en fonction de leur format, et nous l'appelons WADReader. Nous avons également besoin d'une fonction main simple qui appelle ces classes.

Remarque: cette architecture peut ne pas être optimale, et si nécessaire nous la changerons.

Accéder au code


Commençons par créer un projet C ++ vide. Dans Visual Studio, cliquez sur Fichier-> Nouveau -> Projet. Appelons cela DIYDoom.


Ajoutons deux nouvelles classes: WADLoader et WADReader. Commençons par l'implémentation de 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. }; 

L'implémentation du constructeur sera simple: initialisez le pointeur de données et stockez une copie du chemin transféré dans le fichier WAD.

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

Passons maintenant à l'implémentation de la fonction auxiliaire de chargement d' OpenAndLoad : essayez simplement d'ouvrir le fichier en binaire et en cas d'échec nous afficherons une erreur.

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

Si tout se passe bien et que nous pouvons trouver et ouvrir le fichier, nous devons connaître la taille du fichier afin d'allouer de la mémoire pour y copier le fichier.

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

Nous savons maintenant combien d'espace un WAD complet prend et nous allons allouer la quantité de mémoire nécessaire.

 m_WADData = new uint8_t[length]; 

Copiez le contenu du fichier dans cette mémoire.

 // 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(); 

Vous avez peut-être remarqué que j'ai utilisé le type m_WADData comme type de données pour unint8_t . Cela signifie que j'ai besoin d'un tableau exact de 1 octet (1 octet * de longueur). L'utilisation de unint8_t garantit que la taille est égale à un octet (8 bits, ce qui peut être compris à partir du nom du type). Si nous voulions allouer 2 octets (16 bits), nous utiliserions unint16_t, dont nous parlerons plus tard. En utilisant ces types de code, le code devient indépendant de la plateforme. Je vais expliquer: si nous utilisons "int", alors la taille exacte de int en mémoire dépendra du système. Si nous compilons «int» dans une configuration 32 bits, nous obtenons une taille de mémoire de 4 octets (32 bits), et lors de la compilation du même code dans une configuration 64 bits, nous obtenons une taille de mémoire de 8 octets (64 bits)! Pire encore, la compilation du code sur une plate-forme 16 bits (vous pourriez être un fan de DOS) nous donnera 2 octets (16 bits)!

Vérifions brièvement le code et vérifions que tout fonctionne. Mais nous devons d'abord implémenter LoadWAD. Alors que LoadWAD appellera "OpenAndLoad"

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

Et ajoutons au code de fonction principal qui crée une instance de la classe et essaie de charger WAD

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

Vous devrez entrer le chemin d'accès correct à votre fichier WAD. Lançons-le!

Aïe! Nous avons une fenêtre de console qui s'ouvre juste pendant quelques secondes! Rien de particulièrement utile ... le programme fonctionne-t-il? L'idée! Jetons un coup d'œil à la mémoire et voyons ce qu'elle contient! Peut-être y trouverons-nous quelque chose de spécial! Tout d'abord, placez un point d'arrêt en double-cliquant à gauche du numéro de ligne. Vous devriez voir quelque chose comme ceci:


J'ai placé un point d'arrêt immédiatement après avoir lu toutes les données du fichier pour regarder le tableau de mémoire et voir ce qui y était chargé. Maintenant, exécutez à nouveau le code! Dans la fenêtre automatique, je vois les premiers octets. Les 4 premiers octets disent "IWAD"! Génial, ça marche! Je n'ai jamais pensé que ce jour viendrait! Alors, d'accord, il faut se calmer, il y a encore beaucoup de travail à faire!

Déboguer

Lire l'en-tête


La taille totale de l'en-tête est de 12 octets (de 0x00 à 0x0b), ces 12 octets sont divisés en 3 groupes. Les 4 premiers octets sont un type de WAD, généralement «IWAD» ou «PWAD». IWAD devrait être le WAD officiel publié par ID Software, "PWAD" devrait être utilisé pour les mods. En d'autres termes, ce n'est qu'un moyen de déterminer si le fichier WAD est une version officielle ou s'il est publié par des moddeurs. Notez que la chaîne n'est pas terminée par NULL, alors faites attention! Les 4 octets suivants sont unsigned int, qui contient le nombre total de répertoires à la fin du fichier. Les 4 octets suivants indiquent le décalage du premier répertoire.

Ajoutons une structure qui stockera les informations. Je vais ajouter un nouveau fichier d'en-tête et le nommer "DataTypes.h". Nous y décrirons toutes les structures dont nous avons besoin.

 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. }; 

Nous devons maintenant implémenter la classe WADReader, qui lira les données du tableau d'octets WAD chargé. Aïe! Il y a une astuce ici - les fichiers WAD sont au format big-endian, c'est-à-dire que nous devrons déplacer les octets pour les rendre peu endian (aujourd'hui, la plupart des systèmes utilisent peu endian). Pour ce faire, nous ajouterons deux fonctions, l'une pour le traitement de 2 octets (16 bits), l'autre pour le traitement de 4 octets (32 bits); si nous devons lire seulement 1 octet, alors rien ne doit être fait.

 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]; } 

Nous sommes maintenant prêts à lire l'en-tête: comptez les quatre premiers octets comme char, puis ajoutez NULL pour simplifier notre travail. Dans le cas du nombre de répertoires et de leur décalage, vous pouvez simplement utiliser des fonctions auxiliaires pour les convertir au format correct.

 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); } 

Mettons tout cela ensemble, appelons ces fonctions et imprimons les résultats

 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; } 

Exécutez le programme et voyez si tout fonctionne!


Super! La ligne IWAD est clairement visible, mais les deux autres chiffres sont-ils corrects? Essayons de lire les répertoires en utilisant ces décalages et voyons si cela fonctionne!

Nous devons ajouter une nouvelle structure pour gérer le répertoire correspondant aux options ci-dessus.

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

Ajoutons maintenant la fonction ReadDirectories: comptez l'offset et sortez-les!

Dans chaque itération, nous multiplions i * 16 pour passer à l'incrément de décalage du répertoire suivant.

 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; } 

Exécutez le code et voyez ce qui se passe. Ouah! Une grande liste de répertoires.

Exécuter 2

À en juger par le nom du morceau, nous pouvons supposer que nous avons réussi à lire les données correctement, mais il existe peut-être une meilleure façon de vérifier cela. Nous examinerons les entrées du répertoire WAD à l'aide de Slade3.


Il semble que le nom et la taille du morceau correspondent aux données obtenues à l'aide de notre code. Aujourd'hui, nous avons fait un excellent travail!

Autres remarques


  • À un moment donné, j'ai pensé qu'il serait bon d'utiliser vector pour stocker des répertoires. Pourquoi ne pas utiliser Map? Ce sera plus rapide que d'obtenir des données par recherche vectorielle linéaire. C'est une mauvaise idée. Lors de l'utilisation de la carte, l'ordre des entrées du répertoire ne sera pas suivi, mais nous avons besoin de ces informations pour obtenir les données correctes.

    Et une autre idée fausse: la carte en C ++ est implémentée comme des arbres rouge-noir avec un temps de recherche O (log N), et les itérations sur la carte donnent toujours un ordre croissant de clés. Si vous avez besoin d'une structure de données qui donne le temps moyen O (1) et le pire moment O (N), alors vous devez utiliser une carte non ordonnée.
  • Le chargement de tous les fichiers WAD en mémoire n'est pas une méthode d'implémentation optimale. Il serait plus logique de simplement lire les répertoires dans l'en-tête de la mémoire, puis de revenir au fichier WAD et de charger les ressources à partir du disque. J'espère qu'un jour nous en apprendrons plus sur la mise en cache.

    DOOMReboot : complètement en désaccord. 15 Mo de RAM de nos jours est une bagatelle complète, et la lecture depuis la mémoire sera beaucoup plus rapide que le volumineux fseek, qui devra être utilisé après avoir téléchargé tout ce qui est nécessaire pour le niveau. Cela augmentera le temps de téléchargement d'au moins une à deux secondes (cela me prend moins de 20 ms pour télécharger tout le temps). fseek utilise le système d'exploitation. Quel fichier est le plus susceptible dans le cache RAM, mais il ne le peut pas. Mais même s'il est là, c'est un gros gaspillage de ressources et ces opérations vont confondre de nombreuses lectures WAD en termes de cache CPU. La meilleure chose est que vous pouvez créer des méthodes de démarrage hybrides et stocker des données WAD pour un niveau qui tient dans le cache L3 des processeurs modernes, où les économies seront incroyables.

Code source


Code source

Données de base de la carte


Après avoir appris à lire le fichier WAD, essayons d'utiliser les données lues. Ce sera formidable d'apprendre à lire les données de mission (monde / niveau) et à les appliquer. Les «morceaux» de ces missions (Mission Lumps) devraient être quelque chose de complexe et délicat. Par conséquent, nous devrons évoluer et développer les connaissances progressivement. Dans un premier temps, créons quelque chose comme une fonction Automap: un plan bidimensionnel d'une carte avec une vue de dessus. Voyons d'abord ce qu'il y a à l'intérieur du bloc de mission.

Anatomie de la carte


Reprenons: la description des niveaux DOOM est très similaire au dessin 2D, sur lequel les murs sont marqués de lignes. Cependant, pour obtenir des coordonnées 3D, chaque mur prend la hauteur du sol et du plafond (XY est le plan le long duquel nous nous déplaçons horizontalement, et Z est la hauteur qui nous permet de monter et descendre, par exemple, en montant dans un ascenseur ou en sautant d'une plate-forme. Ces trois les composants de coordonnées sont utilisés pour rendre la mission comme un monde 3D, cependant, pour assurer de bonnes performances, le moteur a certaines limites: il n'y a pas de pièces situées les unes au-dessus des autres à des niveaux et le joueur ne peut pas regarder vers le haut et vers le bas. Les roches, par exemple, les roquettes, montent verticalement pour toucher une cible située sur une plate-forme plus élevée.

Ces caractéristiques curieuses ont provoqué des holivars sans fin pour savoir si le DOOM est un moteur 2D ou 3D. Progressivement, un compromis diplomatique a été trouvé, qui a sauvé de nombreuses vies: les parties se sont mises d'accord sur la désignation «2.5D» acceptable par les deux.

Pour simplifier la tâche et revenir au sujet, essayons simplement de lire ces données 2D et de voir si elles peuvent être utilisées d'une manière ou d'une autre. Plus tard, nous essaierons de les rendre en 3D, mais pour l'instant, nous devons comprendre comment les différentes parties du moteur fonctionnent ensemble.

Après avoir mené des recherches, j'ai découvert que chaque mission est composée d'un ensemble de "pièces". Ces «morceaux» sont toujours représentés dans le fichier WAD d'un jeu DOOM dans le même ordre.

  1. Vertexes: les extrémités des murs en 2D. Deux VERTEX connectés forment un LINEDEF. Trois VERTEX connectés forment deux murs / LINEDEF, et ainsi de suite. Ils peuvent être simplement perçus comme les points de connexion de deux ou plusieurs murs. (Oui, la plupart des gens préfèrent le pluriel «Vertices», mais John Carmack ne l'aimait pas. Selon merriam-webster , les deux options s'appliquent.
  2. LINEDEFS: lignes formant des joints entre des sommets et formant des murs. Toutes les lignes (murs) ne se comportent pas de la même manière, il existe des indicateurs qui spécifient le comportement de ces lignes.
  3. SIDEDDEFS: dans la vraie vie, les murs ont deux côtés - nous en regardons un, le second est de l'autre côté. Les deux côtés peuvent avoir des textures différentes, et SIDEDEFS est le bloc contenant les informations de texture pour le mur (LINEDEF).
  4. SECTEURS: les secteurs sont des «chambres» obtenues par la jointure LINEDEF. Chaque secteur contient des informations telles que les hauteurs de plancher et de plafond, les textures, les valeurs d'éclairage, les actions spéciales, telles que les planchers / plates-formes / ascenseurs mobiles. Certains de ces paramètres affectent également le rendu des murs, par exemple, le niveau d'éclairage et le calcul des coordonnées de mappage de texture.
  5. SSECTORS: (sous-secteurs) forment des zones convexes dans un secteur qui sont utilisées dans le rendu en conjonction avec un contournement BSP, et aident également à déterminer où un joueur est à un niveau particulier. Ils sont très utiles et sont souvent utilisés pour déterminer la position verticale d'un joueur. Chaque SSECTOR est constitué de parties connectées d'un secteur, par exemple des murs formant un angle. Ces parties des murs, ou «segments», sont stockées dans leur propre bloc appelé ...
  6. SEGS: parties murales / LINEDEF; en d'autres termes, ce sont les «segments» du mur / LINEDEF. Le monde est rendu en contournant l'arbre BSP pour déterminer les murs à dessiner en premier (les tous premiers sont les plus proches). Bien que le système fonctionne très bien, il entraîne souvent des linedefs en deux SEG ou plus. Ces SEG sont ensuite utilisés pour rendre les murs au lieu de LINEDEF. La géométrie de chaque SSECTOR est déterminée par les segments qu'il contient.
  7. NŒUDS: Un nœud BSP est un nœud d'une structure arborescente binaire qui stocke des données de sous-secteur. Il est utilisé pour déterminer rapidement quels SSECTOR (et SEG) sont devant le joueur. L'élimination des SEG situés derrière le lecteur, et donc invisibles, permet au moteur de se concentrer sur les SEG potentiellement visibles, ce qui réduit considérablement le temps de rendu.
  8. THINGS: Lump appelé THINGS est une liste d'acteurs de décors et de missions (ennemis, armes, etc.). Chaque élément de ce bloc contient des informations sur une instance de l'acteur / ensemble, par exemple, le type d'objet, le point de création, la direction, etc.
  9. REJETER: ce bloc contient des données sur les secteurs visibles des autres secteurs. Il est utilisé pour déterminer quand un monstre apprend la présence d'un joueur. Il est également utilisé pour déterminer la plage de distribution des sons créés par le joueur, par exemple, les tirs. Lorsqu'un tel son peut être transmis au secteur du monstre, il peut en apprendre davantage sur le joueur. La table REJECT peut également être utilisée pour accélérer la reconnaissance des collisions d'obus d'armes.
  10. BLOCKMAP: informations sur la reconnaissance des collisions des joueurs et le mouvement THING. Se compose d'une grille couvrant la géométrie de l'ensemble de la mission. Chaque cellule de la grille contient une liste de LINEDEF qui se trouvent à l'intérieur ou se croisent. Il est utilisé pour accélérer considérablement la reconnaissance des collisions: les vérifications de collision ne sont nécessaires que pour quelques LINEDEF par joueur / CHOSE, ce qui économise considérablement la puissance de calcul.

Lors de la génération de notre carte 2D, nous nous concentrerons sur les VERTEXES et LINEDEFS. Si nous pouvons dessiner les sommets et les relier aux lignes données par linedef, alors nous devons générer un modèle 2D de la carte.

Carte de démonstration

La carte de démonstration illustrée ci-dessus présente les caractéristiques suivantes:

  • 4 pics
    • sommet 1 po (10.10)
    • top 2 à (10,100)
    • top 3 à (100, 10)
    • pic 4 po (100,100)
  • 4 lignes
    • ligne du haut 1 au 2
    • ligne du haut 1 au 3
    • ligne du haut 2 au 4
    • ligne du haut 3 au 4

Format de sommet


Comme vous pouvez vous y attendre, les données de sommet sont très simples - juste x et y (point) de certaines coordonnées.

Taille du champType de donnéesLe contenu
0x00-0x01Signé courtPosition X
0x02-0x03Signé courtPosition Y

Format Linedef


Linedef contient plus d'informations; il décrit la ligne reliant les deux sommets et les propriétés de cette ligne (qui deviendra plus tard un mur).

Taille du champType de donnéesLe contenu
0x00-0x01Court non signéPic de départ
0x02-0x03Court non signéPic ultime
0x04-0x05Court non signéDrapeaux (voir ci-dessous pour plus de détails)
0x06-0x07Court non signéType de ligne / Action
0x08-0x09Court non signéLabel de secteur
0x10-0x11Court non signéFront sidedef (0xFFFF - pas de côté)
0x12-0x13Court non signéSide sidedef (0xFFFF - pas de côté)

Valeurs du drapeau de Linedef


Toutes les lignes (murs) ne sont pas dessinées. Certains d'entre eux ont un comportement spécial.

BitLa description
0Bloque la voie pour les joueurs et les monstres
1Bloquer les monstres
2Double face
3La texture supérieure est désactivée (nous en reparlerons plus tard)
4La texture du bas est désactivée (nous en reparlerons plus tard)
5Secret (montré sur la carte comme un mur unilatéral)
6Obstrue le son
7Jamais montré sur la carte automatique
8Toujours affiché sur la carte automatique

Buts


  1. Créez une classe Map.
  2. Lire les données des sommets.
  3. Lisez les données de linedef.

L'architecture


Tout d'abord, créons une classe et appelons-la map. Nous y stockons toutes les données associées à la carte.

Pour l'instant, je prévois de ne stocker que les sommets et les lignes en tant que vecteur, afin de pouvoir les appliquer plus tard.

Complétons également WADLoader et WADReader afin de pouvoir lire ces deux nouvelles informations.

Codage


Le code sera similaire au code de lecture WAD, nous n'ajouterons que quelques structures supplémentaires, puis les remplirons de données WAD. Commençons par ajouter une nouvelle classe et en passant le nom de la carte.

 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; }; 

Ajoutez maintenant des structures pour lire ces nouveaux champs. Comme nous l'avons déjà fait plusieurs fois, ajoutez-les tous en même temps.

 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; }; 

Ensuite, nous avons besoin d'une fonction pour les lire à partir de WADReader, elle sera proche de ce que nous avons fait plus tôt.

 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); } 

Je pense qu'il n'y a rien de nouveau pour vous ici. Et maintenant, nous devons appeler ces fonctions à partir de la classe WADLoader. Permettez-moi de dire les faits: la séquence de blocs est importante ici, nous trouverons le nom de la carte dans le répertoire des blocs, suivi de tous les blocs associés aux cartes dans l'ordre donné. Pour simplifier notre tâche et ne pas suivre les indices de grumeaux séparément, nous allons ajouter une énumération qui nous permet de nous débarrasser des nombres magiques.

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

J'ajouterai également une fonction pour rechercher une carte par son nom dans la liste des répertoires. Plus tard, nous allons probablement augmenter les performances de cette étape en utilisant la structure des données de la carte, car il y a un nombre important d'enregistrements ici, et nous devrons les parcourir assez souvent, en particulier au début du chargement des ressources telles que les textures, les sprites, les sons, etc.

 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, nous avons presque fini! Maintenant, comptons juste les VERTEXES! Je le répète, nous l'avons déjà fait auparavant, maintenant vous devez comprendre cela.

 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, il semble que nous copions constamment le même code; vous devrez peut-être l'optimiser à l'avenir, mais pour l'instant, vous allez implémenter ReadMapLinedef vous-même (ou regarder le code source à partir du lien).

Touche finale - nous devons appeler cette fonction et lui passer l'objet cartographique.

 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; } 

Modifions maintenant la fonction principale et voyons si tout fonctionne. Je souhaite charger la carte «E1M1», que je transférerai sur l'objet carte.

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

Maintenant, exécutons tout cela. Wow, un tas de chiffres intéressants, mais sont-ils vrais? Voyons ça!

Voyons si Slade peut nous aider.

Nous pouvons trouver la carte dans le menu slade et regarder les détails des morceaux. Comparons les chiffres.

Vertex

Super!

Et Linedef?

Linedef

J'ai également ajouté cette énumération, que nous essaierons d'utiliser lors du rendu de la carte.

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

Autres remarques


Lors de l'écriture du code, j'ai lu par erreur plus d'octets que nécessaire et j'ai reçu des valeurs incorrectes. Pour le débogage, j'ai commencé à regarder le décalage WAD en mémoire pour voir si j'étais au bon décalage. Cela peut être fait en utilisant la fenêtre de mémoire de Visual Studio, qui est un outil très utile pour suivre les octets ou la mémoire (vous pouvez également définir des points d'arrêt dans cette fenêtre).

Si vous ne voyez pas la fenêtre de mémoire, accédez à Débogage> Mémoire> Mémoire.


Maintenant, nous voyons les valeurs en mémoire en hexadécimal. Ces valeurs peuvent être comparées à l'affichage hexadécimal dans slade en cliquant avec le bouton droit sur une bosse et en l'affichant en hexadécimal.

Slade

Comparez-les avec l'adresse du WAD chargé en mémoire.


Et la dernière chose pour aujourd'hui: nous avons vu toutes ces valeurs de vertex, mais existe-t-il un moyen facile de les visualiser sans écrire de code? Je ne veux pas perdre de temps là-dessus, juste pour découvrir que nous allons dans la mauvaise direction.

Certes, quelqu'un a déjà créé un traceur. J'ai googlé «dessiner des points sur un graphique» et le premier résultat a été le site Web Plot Points - Desmos . Sur celui-ci, vous pouvez coller des nombres depuis le presse-papiers et il les dessinera. Ils doivent être au format "(x, y)". Pour l'obtenir, changez simplement la fonction de sortie à l'écran.

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

Ouah! Il ressemble déjà à un E1M1! Nous avons réalisé quelque chose!

Points de tracé E1M1

Si vous êtes paresseux pour ce faire, voici un lien vers un graphique en pointillés: Plot Vertex .

Mais faisons un pas de plus: après un peu de travail, nous pouvons relier ces points en fonction des lignes de repère.

E1M1 Plot Vertex

Voici le lien: E1M1 Plot Vertex

Code source


Code source

Les références


Doom Wiki

ZDoom Wiki

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


All Articles