Introduccion
El objetivo de este proyecto es crear un clon del motor DOOM utilizando recursos lanzados con Ultimate DOOM (
versión de Steam ).
Se presentará en forma de tutorial: no quiero lograr el máximo rendimiento en el código, solo creo una versión funcional y más tarde comenzaré a mejorarlo y optimizarlo.
No tengo experiencia en la creación de juegos o motores de juegos, y poca experiencia en la redacción de artículos, por lo que puede sugerir sus propios cambios o incluso reescribir completamente el código.
Aquí hay una lista de recursos y enlaces.
Libro Game Engine Black Book: DOOM Fabien Sanglar . Uno de los mejores libros sobre DOOM internos.
Doom wikiCódigo fuente DOOMCódigo fuente Chocolate DoomRequisitos
- Visual Studio: cualquier IDE servirá; Trabajaré en Visual Studio 2017.
- SDL2: bibliotecas.
- DOOM: una copia de la versión Steam de Ultimate DOOM, solo necesitamos un archivo WAD de él.
Opcional
- Slade3: una buena herramienta para probar nuestro trabajo.
Pensamientos
No sé, puedo completar este proyecto, pero haré lo mejor que pueda para esto.
Windows será mi plataforma de destino, pero como uso SDL, solo hará que el motor funcione bajo cualquier otra plataforma.
Mientras tanto, instale Visual Studio!
El proyecto fue renombrado de Handmade DOOM para Do It Yourself Doom with SLD (DIY Doom) para que no se confunda con otros proyectos llamados "Handmade". Hay algunas capturas de pantalla en el tutorial donde todavía se llama Handmade DOOM.
Archivos WAD
Antes de embarcarse en la codificación, establezcamos objetivos y pensemos en lo que queremos lograr.
Primero, verifiquemos si podemos leer los archivos de recursos DOOM. Todos los recursos de DOOM están en el archivo WAD.
¿Qué es un archivo WAD?
"¿Dónde están todos mis datos"? ("¿Dónde están todos mis datos?") ¡Están en WAD! WAD es un archivo de todos los recursos de DOOM (y juegos basados en DOOM) ubicados en un solo archivo.
Los desarrolladores de Doom crearon este formato para simplificar la creación de modificaciones del juego.
WAD File Anatomy
El archivo WAD consta de tres partes principales: el encabezado (encabezado), "piezas" (grumos) y directorios (directorios).
- Encabezado: contiene información básica sobre el archivo WAD y el desplazamiento del directorio.
- Bultos: aquí se almacenan recursos del juego, datos de mapas, sprites, música, etc.
- Directorios: la estructura organizativa para buscar datos en la sección global.
<---- 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 | | |------------------| | | . | | | . | | | . | ---> \------------------/
Formato de encabezado
Formato de directorio
Objetivos
- Crea un proyecto.
- Abre el archivo WAD.
- Lee el titular.
- Lea todos los directorios y muéstrelos.
Arquitectura
No complicamos nada todavía. Cree una clase que simplemente se abra y cargue WAD, y llámelo WADLoader. Luego escribimos una clase que es responsable de leer los datos dependiendo de su formato, y lo llamamos WADReader. También necesitamos una función
main
simple que llame a estas clases.
Nota: esta arquitectura puede no ser óptima, y si es necesario la cambiaremos.
Llegando al código
Comencemos creando un proyecto C ++ vacío. En Visual Studio, haga clic en Archivo-> Nuevo -> Proyecto. Llamémoslo DIYDoom.
Agreguemos dos nuevas clases: WADLoader y WADReader. Comencemos con la implementación de WADLoader.
class WADLoader { public: WADLoader(std::string sWADFilePath);
Implementar el constructor será simple: inicialice el puntero de datos y almacene una copia de la ruta transferida al archivo WAD.
WADLoader::WADLoader(string sWADFilePath) : m_WADData(NULL), m_sWADFilePath(sWADFilePath) { }
Ahora pasemos a la implementación de la función auxiliar de cargar
OpenAndLoad
: solo intentamos abrir el archivo como binario y, en caso de falla, mostrar un error.
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 todo va bien, y podemos encontrar y abrir el archivo, entonces necesitamos saber el tamaño del archivo para asignar memoria para copiar el archivo.
m_WADFile.seekg(0, m_WADFile.end); size_t length = m_WADFile.tellg();
Ahora sabemos cuánto espacio ocupa un WAD completo, y asignaremos la cantidad de memoria necesaria.
m_WADData = new uint8_t[length];
Copie el contenido del archivo a esta memoria.
Es posible que haya notado que usé el tipo
m_WADData
como tipo de datos para
unint8_t
. Esto significa que necesito una matriz exacta de 1 byte (1 byte * longitud). El uso de unint8_t garantiza que el tamaño sea igual a un byte (8 bits, que se pueden entender a partir del nombre del tipo). Si quisiéramos asignar 2 bytes (16 bits), usaríamos unint16_t, del cual hablaremos más adelante. Al usar estos tipos de código, el código se vuelve independiente de la plataforma. Explicaré: si usamos "int", entonces el tamaño exacto de int en la memoria dependerá del sistema. Si compilamos "int" en una configuración de 32 bits, obtenemos un tamaño de memoria de 4 bytes (32 bits), y cuando compilamos el mismo código en una configuración de 64 bits, obtenemos un tamaño de memoria de 8 bytes (64 bits). ¡Aún peor, compilar el código en una plataforma de 16 bits (puede que seas un fanático de DOS) nos dará 2 bytes (16 bits)!
Revisemos el código brevemente y asegurémonos de que todo funcione. Pero primero necesitamos implementar LoadWAD. Mientras que LoadWAD llamará "OpenAndLoad"
bool WADLoader::LoadWAD() { if (!OpenAndLoad()) { return false; } return true; }
Y agreguemos al código de función principal que crea una instancia de la clase e intenta cargar WAD
int main() { WADLoader wadloader("D:\\SDKs\\Assets\\Doom\\DOOM.WAD"); wadloader.LoadWAD(); return 0; }
Deberá ingresar la ruta correcta a su archivo WAD. ¡Vamos a correrlo!
¡Ay! ¡Tenemos una ventana de consola que solo se abre por unos segundos! Nada particularmente útil ... ¿funciona el programa? La idea! ¡Echemos un vistazo a la memoria y veamos qué contiene! ¡Quizás allí encontraremos algo especial! Primero, coloque un punto de interrupción haciendo doble clic a la izquierda del número de línea. Deberías ver algo como esto:
Puse un punto de interrupción inmediatamente después de leer todos los datos del archivo para mirar la matriz de memoria y ver lo que estaba cargado. ¡Ahora ejecute el código nuevamente! En la ventana automática, veo los primeros bytes. ¡Los primeros 4 bytes dicen "IWAD"! Genial, funciona! ¡Nunca pensé que llegaría este día! Entonces, está bien, debes calmarte, ¡todavía hay mucho trabajo por delante!
Leer encabezado
El tamaño total del encabezado es de 12 bytes (de 0x00 a 0x0b), estos 12 bytes se dividen en 3 grupos. Los primeros 4 bytes son un tipo de WAD, generalmente "IWAD" o "PWAD". IWAD debería ser el WAD oficial lanzado por ID Software, "PWAD" debería usarse para modificaciones. En otras palabras, esta es solo una forma de determinar si el archivo WAD es un lanzamiento oficial o si lo publican los modders. Tenga en cuenta que la cadena no tiene terminación NULL, ¡así que tenga cuidado! Los siguientes 4 bytes son int sin signo, que contiene el número total de directorios al final del archivo. Los siguientes 4 bytes indican el desplazamiento del primer directorio.
Agreguemos una estructura que almacenará información. Agregaré un nuevo archivo de encabezado y lo nombraré "DataTypes.h". En él describiremos todas las estructuras que necesitamos.
struct Header { char WADType[5];
Ahora necesitamos implementar la clase WADReader, que leerá datos de la matriz de bytes WAD cargada. ¡Ay! Aquí hay un truco: los archivos WAD están en formato big-endian, es decir, tendremos que cambiar los bytes para hacerlos little endian (hoy, la mayoría de los sistemas usan little endian). Para hacer esto, agregaremos dos funciones, una para procesar 2 bytes (16 bits), la otra para procesar 4 bytes (32 bits); Si necesitamos leer solo 1 byte, entonces no es necesario hacer nada.
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]; }
Ahora estamos listos para leer el encabezado: cuente los primeros cuatro bytes como char y luego agregue NULL para simplificar nuestro trabajo. En el caso de la cantidad de directorios y su desplazamiento, simplemente puede usar funciones auxiliares para convertirlos al formato correcto.
void WADReader::ReadHeaderData(const uint8_t *pWADData, int offset, Header &header) {
Vamos a poner todo junto, llamar a estas funciones e imprimir los resultados.
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; }
¡Ejecute el programa y vea si todo funciona!
Genial La línea IWAD es claramente visible, pero ¿son correctos los otros dos números? ¡Intentemos leer directorios usando estos desplazamientos y veamos si funciona!
Necesitamos agregar una nueva estructura para manejar el directorio correspondiente a las opciones anteriores.
struct Directory { uint32_t LumpOffset; uint32_t LumpSize; char LumpName[9]; };
Ahora agreguemos la función ReadDirectories: ¡cuente el desplazamiento y déles salida!
En cada iteración, multiplicamos i * 16 para ir al incremento de desplazamiento del siguiente directorio.
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; }
Ejecute el código y vea qué sucede. Wow! Una gran lista de directorios.
A juzgar por el nombre de la masa, podemos suponer que logramos leer los datos correctamente, pero tal vez hay una mejor manera de verificar esto. Echaremos un vistazo a las entradas del Directorio WAD usando Slade3.
Parece que el nombre y el tamaño del bulto corresponden a los datos obtenidos con nuestro código. ¡Hoy hicimos un gran trabajo!
Otras notas
- En algún momento, pensé que sería bueno usar vector para almacenar directorios. ¿Por qué no usar Map? Esto será más rápido que obtener datos mediante la búsqueda lineal de vectores. Esta es una mala idea Cuando se usa el mapa, no se rastreará el orden de las entradas del directorio, pero necesitamos esta información para obtener los datos correctos.
Y otro concepto erróneo: Map en C ++ se implementa como árboles rojo-negros con tiempo de búsqueda O (log N), y las iteraciones sobre el mapa siempre dan un orden creciente de claves. Si necesita una estructura de datos que proporcione el tiempo promedio O (1) y el peor tiempo O (N), entonces debe usar un mapa desordenado. Cargar todos los archivos WAD en la memoria no es un método de implementación óptimo. Sería más lógico simplemente leer los directorios en el encabezado de la memoria y luego regresar al archivo WAD y cargar recursos desde el disco. Esperemos que algún día aprendamos más sobre el almacenamiento en caché.
DOOMReboot : completamente en desacuerdo. 15 MB de RAM en estos días es una bagatela completa, y leer de memoria será mucho más rápido que el voluminoso fseek, que deberá usarse después de descargar todo lo necesario para el nivel. Esto aumentará el tiempo de descarga en no menos de uno o dos segundos (me lleva menos de 20 ms descargar todo el tiempo). fseek usa el sistema operativo. Qué archivo es más probable en la memoria caché de RAM, pero puede que no. Pero incluso si él está allí, es una gran pérdida de recursos y estas operaciones confundirán muchas lecturas de WAD en términos de caché de CPU. Lo mejor es que puede crear métodos de arranque híbridos y almacenar datos WAD para un nivel que se ajuste al caché L3 de los procesadores modernos, donde los ahorros serán increíbles.
Código fuente
Código fuenteDatos básicos de la tarjeta
Después de haber aprendido a leer el archivo WAD, intentemos usar los datos leídos. Será genial aprender a leer los datos de la misión (mundo / nivel) y aplicarlos. Los "fragmentos" de estas misiones (Mission Lumps) deberían ser algo complejo y complicado. Por lo tanto, necesitaremos movernos y desarrollar el conocimiento gradualmente. Como primer pequeño paso, creemos algo así como una característica de Automap: un plano bidimensional de un mapa con una vista superior. Primero, veamos qué hay dentro del bulto de la misión.
Anatomía de la tarjeta
Comencemos de nuevo: la descripción de los niveles de DOOM es muy similar al dibujo 2D, en el que las paredes están marcadas con líneas. Sin embargo, para obtener coordenadas 3D, cada pared toma la altura del piso y el techo (XY es el plano a lo largo del cual nos movemos horizontalmente, y Z es la altura que nos permite movernos hacia arriba y hacia abajo, por ejemplo, al levantarnos en un elevador o saltar desde una plataforma. Los componentes de coordenadas se utilizan para representar la misión como un mundo 3D, sin embargo, para garantizar un buen rendimiento, el motor tiene ciertas limitaciones: no hay habitaciones ubicadas una encima de la otra en los niveles y el jugador no puede mirar hacia arriba y hacia abajo. Otra característica interesante: proyectiles y Las rocas, por ejemplo, los cohetes, ascienden verticalmente para alcanzar un objetivo ubicado en una plataforma más alta.
Estas características curiosas han provocado un sinfín de preguntas acerca de si el DOOM es un motor 2D o 3D. Poco a poco, se alcanzó un compromiso diplomático, que salvó muchas vidas: las partes acordaron la designación "2.5D" aceptable para ambos.
Para simplificar la tarea y volver al tema, intentemos leer estos datos 2D y ver si pueden usarse de alguna manera. Más adelante intentaremos renderizarlos en 3D, pero por ahora necesitamos entender cómo funcionan juntas las partes individuales del motor.
Después de realizar una investigación, descubrí que cada misión se compone de un conjunto de "piezas". Estos "Bultos" siempre se representan en el archivo WAD de un juego DOOM en el mismo orden.
- Vértices: los puntos finales de las paredes en 2D. Dos VERTEX conectados forman un LINEDEF. Tres VERTEX conectados forman dos paredes / LINEDEF, y así sucesivamente. Se pueden percibir simplemente como los puntos de conexión de dos o más paredes. (Sí, la mayoría de la gente prefiere el plural "Vértices", pero a John Carmack no le gustó. Según merriam-webster , se aplican ambas opciones.
- LINEDEFS: líneas que forman juntas entre vértices y paredes que forman. No todas las líneas (paredes) se comportan igual, hay banderas que especifican el comportamiento de dichas líneas.
- DEDOS LADOS: en la vida real, las paredes tienen dos lados: miramos uno, el segundo está del otro lado. Los dos lados pueden tener diferentes texturas, y SIDEDEFS es el bulto que contiene la información de textura para la pared (LINEDEF).
- SECTORES: los sectores son "salas" obtenidas por la unión LINEDEF. Cada sector contiene información como alturas de piso y techo, texturas, valores de iluminación, acciones especiales, como mover pisos / plataformas / elevadores. Algunos de estos parámetros también afectan la forma en que se representan las paredes, por ejemplo, el nivel de iluminación y el cálculo de las coordenadas del mapeo de texturas.
- SECTORES: (subsectores) forman áreas convexas dentro de un sector que se utilizan para renderizar junto con el bypass BSP, y también ayudan a determinar dónde se encuentra un jugador en un nivel particular. Son bastante útiles y a menudo se usan para determinar la posición vertical de un jugador. Cada SSECTOR consta de partes conectadas de un sector, por ejemplo, de paredes que forman un ángulo. Tales partes de las paredes, o "segmentos", se almacenan en su propio bulto llamado ...
- SEGS: piezas de pared / LINEDEF; en otras palabras, estos son los "segmentos" del muro / LINEDEF. El mundo se representa sin pasar por el árbol BSP para determinar qué paredes dibujar primero (las primeras son las más cercanas). Aunque el sistema funciona muy bien, a menudo hace que los linedefs se dividan en dos o más SEG. Estos SEG se utilizan para representar muros en lugar de LINEDEF. La geometría de cada SSECTOR está determinada por los segmentos contenidos en él.
- NODOS: Un nodo BSP es un nodo de una estructura de árbol binario que almacena datos del subsector. Se utiliza para determinar rápidamente qué SSECTOR (y SEG) están delante del jugador. La eliminación de los SEG ubicados detrás del jugador y, por lo tanto, invisibles, permite que el motor se concentre en los SEG potencialmente visibles, lo que reduce significativamente el tiempo de renderizado.
- COSAS: El bulto llamado COSAS es una lista de actores de escenarios y misiones (enemigos, armas, etc.). Cada elemento de este bulto contiene información sobre una instancia del actor / conjunto, por ejemplo, el tipo de objeto, el punto de creación, la dirección, etc.
- RECHAZAR: este bulto contiene datos sobre qué sectores son visibles desde otros sectores. Se utiliza para determinar cuándo un monstruo se entera de la presencia de un jugador. También se utiliza para determinar el rango de distribución de sonidos creados por el jugador, por ejemplo, disparos. Cuando ese sonido puede transmitirse al sector del monstruo, puede averiguar sobre el jugador. La tabla RECHAZAR también se puede utilizar para acelerar el reconocimiento de colisiones de proyectiles de armas.
- BLOCKMAP: información de reconocimiento de colisión del jugador y movimiento de THING. Consiste en una cuadrícula que cubre la geometría de toda la misión. Cada celda de la cuadrícula contiene una lista de LINEDEF que están dentro o que se cruzan con ella. Se utiliza para acelerar significativamente el reconocimiento de colisiones: se requieren comprobaciones de colisión para solo unos pocos LINEDEF por jugador / COSA, lo que ahorra significativamente la potencia informática.
Al generar nuestro mapa 2D, nos centraremos en VERTEXOS y LINEDEFS. Si podemos dibujar los vértices y conectarlos con las líneas dadas por linedef, entonces necesitamos generar un modelo 2D del mapa.
La tarjeta de demostración que se muestra arriba tiene las siguientes características:
- 4 picos
- vértice 1 en (10.10)
- top 2 en (10,100)
- top 3 en (100, 10)
- pico 4 in (100,100)
- 4 lineas
- línea de arriba 1 a 2
- línea de arriba 1 a 3
- línea de arriba 2 a 4
- línea de arriba 3 a 4
Formato de vértice
Como es de esperar, los datos de vértice son muy simples: solo x e y (punto) de algunas coordenadas.
Formato Linedef
Linedef contiene más información; describe la línea que conecta los dos vértices y las propiedades de esta línea (que luego se convertirá en un muro).
Linedef Flag Values
No todas las líneas (paredes) están dibujadas. Algunos de ellos tienen un comportamiento especial.
Objetivos
- Crea una clase de Mapa.
- Leer datos de vértices.
- Leer datos linedef.
Arquitectura
Primero, creemos una clase y llamémosla mapa. En él almacenaremos todos los datos asociados con la tarjeta.
Por ahora, planeo almacenar solo vértices y linedefs como un vector, para poder aplicarlos más tarde.
Además, complementemos WADLoader y WADReader para que podamos leer estas dos nuevas piezas de información.
Codificación
El código será similar al código de lectura de WAD, solo agregaremos unas pocas estructuras más y luego los llenaremos con datos de WAD. Comencemos agregando una nueva clase y pasando el nombre del mapa.
class Map { public: Map(std::string sName); ~Map(); std::string GetName();
Ahora agregue estructuras para leer estos nuevos campos. Como ya lo hemos hecho varias veces, solo agréguelas todas a la vez.
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; };
A continuación, necesitamos una función para leerlos desde WADReader, estará cerca de lo que hicimos antes. 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); }
Creo que no hay nada nuevo para ti aquí. Y ahora necesitamos llamar a estas funciones desde la clase WADLoader. Permítanme exponer los hechos: la secuencia de grumos es importante aquí, encontraremos el nombre del mapa en el directorio de grumos, seguido de todos los grumos asociados con los mapas en el orden dado. Para simplificar nuestra tarea y no rastrear los índices de bultos por separado, agregaremos una enumeración que nos permite deshacernos de los números mágicos. enum EMAPLUMPSINDEX { eTHINGS = 1, eLINEDEFS, eSIDEDDEFS, eVERTEXES, eSEAGS, eSSECTORS, eNODES, eSECTORS, eREJECT, eBLOCKMAP, eCOUNT };
También agregaré una función para buscar un mapa por su nombre en la lista de directorios. Más adelante, es probable que aumentemos el rendimiento de este paso mediante el uso de la estructura de datos del mapa, porque aquí hay un número significativo de registros, y tendremos que revisarlos con bastante frecuencia, especialmente al comienzo de cargar recursos como texturas, sprites, sonidos, 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; }
¡Guau, ya casi hemos terminado! Ahora, ¡solo contemos los VERTEXOS! Repito, ya hemos hecho esto antes, ahora debes entender esto. 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, parece que estamos copiando constantemente el mismo código; Puede que tenga que optimizarlo en el futuro, pero por ahora implementará ReadMapLinedef usted mismo (o mire el código fuente desde el enlace).Toques finales: necesitamos llamar a esta función y pasarle el objeto del mapa. 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; }
Ahora cambiemos la función principal y veamos si todo funciona. Quiero cargar el mapa "E1M1", que transferiré al objeto del mapa. Map map("E1M1"); wadloader.LoadMapData(map);
Ahora vamos a ejecutarlo todo. Wow, un montón de números interesantes, pero ¿son ciertos? ¡Vamos a echarle un vistazo!Veamos si slade puede ayudarnos con esto.Podemos encontrar el mapa en el menú de slade y ver los detalles de los bultos. Comparemos los números.Genial
¿Qué hay de Linedef?También agregué esta enumeración, que intentaremos usar al representar el mapa. enum ELINEDEFFLAGS { eBLOCKING = 0, eBLOCKMONSTERS = 1, eTWOSIDED = 2, eDONTPEGTOP = 4, eDONTPEGBOTTOM = 8, eSECRET = 16, eSOUNDBLOCK = 32, eDONTDRAW = 64, eDRAW = 128 };
Otras notas
En el proceso de escribir el código, leí por error más bytes de los necesarios y recibí valores incorrectos. Para la depuración, comencé a mirar el desplazamiento WAD en la memoria para ver si estaba en el desplazamiento correcto. Esto se puede hacer usando la ventana de memoria de Visual Studio, que es una herramienta muy útil para rastrear bytes o memoria (también puede establecer puntos de interrupción en esta ventana).Si no ve la ventana de memoria, vaya a Depurar> Memoria> Memoria.Ahora vemos los valores en memoria en hexadecimal. Estos valores se pueden comparar con la visualización hexadecimal en slade haciendo clic derecho en cualquier bulto y mostrándolo como hexadecimal.Compárelos con la dirección del WAD cargado en la memoria.Y lo último para hoy: vimos todos estos valores de vértice, pero ¿hay una manera fácil de visualizarlos sin escribir código? No quiero perder tiempo en esto, solo para descubrir que nos estamos moviendo en la dirección equivocada.Seguramente alguien ya creó un trazador. Busqué en Google "dibujar puntos en un gráfico" y el primer resultado fue el sitio web de Plot Points: Desmos . En él, puede pegar números del portapapeles y él los dibujará. Deben estar en el formato "(x, y)". Para obtenerlo, simplemente cambie la función de salida a la pantalla. cout << "(" << vertex.XPosition << "," << vertex.YPosition << ")" << endl;
Wow! ¡Ya parece un E1M1! ¡Hemos logrado algo!Si eres perezoso para hacer esto, aquí hay un enlace a un gráfico punteado: Plot Vertex .Pero demos un paso más: después de un poco de trabajo, podemos conectar estos puntos en función de linedefs.Aquí está el enlace: E1M1 Plot VertexCódigo fuente
Código fuenteReferencias
Doom WikiZDoom Wiki