1. Introdução
O objetivo deste projeto é criar um clone do mecanismo DOOM usando os recursos liberados com o Ultimate DOOM (
versão do Steam ).
Ele será apresentado na forma de um tutorial - não quero obter o máximo desempenho no código, mas apenas criar uma versão funcional e, posteriormente, começarei a aprimorá-lo e a otimizá-lo.
Não tenho experiência na criação de jogos ou mecanismos de jogos e pouca experiência na redação de artigos, para que você possa sugerir suas próprias alterações ou até reescrever completamente o código.
Aqui está uma lista de recursos e links.
Livro Jogo Motor Black Book: DOOM Fabien Sanglar . Um dos melhores livros sobre DOOM internos.
Wiki DoomCódigo fonte DOOMCódigo-fonte Chocolate DoomExigências
- Visual Studio: qualquer IDE serve; Vou trabalhar no Visual Studio 2017.
- SDL2: bibliotecas.
- DOOM: Uma cópia da versão Steam do Ultimate DOOM, precisamos apenas de um arquivo WAD.
Opcional
- Slade3: uma boa ferramenta para testar nosso trabalho.
Pensamentos
Não sei, posso concluir este projeto, mas farei o meu melhor para isso.
O Windows será minha plataforma de destino, mas como eu uso o SDL, ele fará o mecanismo funcionar em qualquer outra plataforma.
Enquanto isso, instale o Visual Studio!
O projeto foi renomeado de Handmade DOOM para Do It Yourself Doom com SLD (DIY Doom), para que não fosse confundido com outros projetos chamados “Handmade”. Existem algumas capturas de tela no tutorial, onde ainda é chamado Handmade DOOM.
Arquivos WAD
Antes de iniciar a codificação, vamos definir metas e pensar no que queremos alcançar.
Primeiro, vamos verificar se conseguimos ler os arquivos de recursos DOOM. Todos os recursos do DOOM estão no arquivo WAD.
O que é um arquivo WAD?
"Onde estão todos os meus dados"? ("Onde estão todos os meus dados?") Eles estão no WAD! O WAD é um arquivo de todos os recursos do DOOM (e jogos baseados no DOOM) localizados em um único arquivo.
Os desenvolvedores do Doom criaram esse formato para simplificar a criação de modificações no jogo.
Anatomia do arquivo WAD
O arquivo WAD consiste em três partes principais: o cabeçalho (cabeçalho), as "partes" (pedaços) e os diretórios (diretórios).
- Cabeçalho - contém informações básicas sobre o arquivo WAD e o deslocamento do diretório.
- Nódulos - aqui estão armazenados recursos de jogos, dados de mapas, sprites, músicas etc.
- Diretórios - A estrutura organizacional para encontrar dados na seção 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 cabeçalho
Formato de Diretório
Objetivos
- Crie um projeto.
- Abra o arquivo WAD.
- Leia o título.
- Leia todos os diretórios e exiba-os.
Arquitetura
Não vamos complicar nada ainda. Crie uma classe que apenas abra e carregue WAD e chame-a de WADLoader. Em seguida, escrevemos uma classe que é responsável pela leitura dos dados, dependendo do formato, e denominamos WADReader. Também precisamos de uma função
main
simples que chame essas classes.
Nota: essa arquitetura pode não ser ótima e, se necessário, a alteraremos.
Obtendo o código
Vamos começar criando um projeto C ++ vazio. No Visual Studio, clique em Arquivo-> Novo -> Projeto. Vamos chamá-lo de DIYDoom.
Vamos adicionar duas novas classes: WADLoader e WADReader. Vamos começar com a implementação do WADLoader.
class WADLoader { public: WADLoader(std::string sWADFilePath);
A implementação do construtor será simples: inicialize o ponteiro de dados e armazene uma cópia do caminho transferido no arquivo WAD.
WADLoader::WADLoader(string sWADFilePath) : m_WADData(NULL), m_sWADFilePath(sWADFilePath) { }
Agora vamos à implementação da função auxiliar de carregamento do
OpenAndLoad
: apenas tentamos abrir o arquivo como binário e, em caso de falha, exibir um erro.
m_WADFile.open(m_sWADFilePath, ifstream::binary); if (!m_WADFile.is_open()) { cout << "Error: Failed to open WAD file" << m_sWADFilePath << endl; return false; }
Se tudo correr bem, e podemos encontrar e abrir o arquivo, precisamos saber o tamanho do arquivo para alocar memória para copiar o arquivo para ele.
m_WADFile.seekg(0, m_WADFile.end); size_t length = m_WADFile.tellg();
Agora sabemos quanto espaço um WAD completo ocupa e alocamos a quantidade necessária de memória.
m_WADData = new uint8_t[length];
Copie o conteúdo do arquivo para esta memória.
Você deve ter notado que eu usei o tipo
m_WADData
como o tipo de dados para
unint8_t
. Isso significa que eu preciso de uma matriz exata de 1 byte (1 byte * comprimento). O uso de unint8_t garante que o tamanho seja igual a um byte (8 bits, que pode ser entendido pelo nome do tipo). Se quiséssemos alocar 2 bytes (16 bits), usaríamos unint16_t, sobre o qual falaremos mais adiante. Ao usar esses tipos de código, o código se torna independente da plataforma. Vou explicar: se usarmos "int", o tamanho exato de int na memória dependerá do sistema. Se compilarmos “int” em uma configuração de 32 bits, obteremos um tamanho de memória de 4 bytes (32 bits) e, ao compilar o mesmo código em uma configuração de 64 bits, obteremos um tamanho de memória de 8 bytes (64 bits)! Pior ainda, compilar o código em uma plataforma de 16 bits (você pode ser um fã do DOS) nos fornecerá 2 bytes (16 bits)!
Vamos verificar brevemente o código e garantir que tudo funcione. Mas primeiro precisamos implementar o LoadWAD. Enquanto o LoadWAD chamará "OpenAndLoad"
bool WADLoader::LoadWAD() { if (!OpenAndLoad()) { return false; } return true; }
E vamos adicionar ao código de função principal que cria uma instância da classe e tenta carregar o WAD
int main() { WADLoader wadloader("D:\\SDKs\\Assets\\Doom\\DOOM.WAD"); wadloader.LoadWAD(); return 0; }
Você precisará inserir o caminho correto para o seu arquivo WAD. Vamos lá!
Ai! Temos uma janela do console que se abre por alguns segundos! Nada de particularmente útil ... o programa funciona? A ideia! Vamos dar uma olhada na memória e ver o que há nela! Talvez lá encontremos algo especial! Primeiro, coloque um ponto de interrupção clicando duas vezes à esquerda do número da linha. Você deve ver algo assim:
Coloquei um ponto de interrupção imediatamente após ler todos os dados do arquivo para examinar a matriz de memória e ver o que foi carregado nela. Agora execute o código novamente! Na janela automática, vejo os primeiros bytes. Os primeiros 4 bytes dizem "IWAD"! Ótimo, funciona! Eu nunca pensei que esse dia chegaria! Então, tudo bem, você precisa se acalmar, ainda há muito trabalho pela frente!
Ler cabeçalho
O tamanho total do cabeçalho é de 12 bytes (de 0x00 a 0x0b), esses 12 bytes são divididos em 3 grupos. Os primeiros 4 bytes são um tipo de WAD, geralmente "IWAD" ou "PWAD". O IWAD deve ser o WAD oficial lançado pela ID Software, "PWAD" deve ser usado para mods. Em outras palavras, essa é apenas uma maneira de determinar se o arquivo WAD é um lançamento oficial ou lançado por modders. Observe que a sequência não é terminada em NULL, portanto, tenha cuidado! Os próximos 4 bytes são int sem sinal, que contém o número total de diretórios no final do arquivo. Os próximos 4 bytes indicam o deslocamento do primeiro diretório.
Vamos adicionar uma estrutura que irá armazenar informações. Vou adicionar um novo arquivo de cabeçalho e denominar “DataTypes.h”. Nele descreveremos todas as estruturas que precisamos.
struct Header { char WADType[5];
Agora precisamos implementar a classe WADReader, que lerá os dados da matriz de bytes carregada do WAD. Ai! Há um truque aqui - os arquivos WAD estão no formato big-endian, ou seja, precisaremos mudar os bytes para torná-los little-endian (hoje, a maioria dos sistemas usa little endian). Para isso, adicionaremos duas funções, uma para processar 2 bytes (16 bits) e outra para processar 4 bytes (32 bits); se precisarmos ler apenas 1 byte, nada precisará ser feito.
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]; }
Agora estamos prontos para ler o cabeçalho: conte os quatro primeiros bytes como char e adicione NULL a eles para simplificar nosso trabalho. No caso do número de diretórios e seu deslocamento, você pode simplesmente usar funções auxiliares para convertê-las no formato correto.
void WADReader::ReadHeaderData(const uint8_t *pWADData, int offset, Header &header) {
Vamos juntar tudo, chamar essas funções e imprimir os 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; }
Execute o programa e veja se tudo funciona!
Ótimo! A linha IWAD é claramente visível, mas os outros dois números estão corretos? Vamos tentar ler diretórios usando essas compensações e ver se funciona!
Precisamos adicionar uma nova estrutura para lidar com o diretório correspondente às opções acima.
struct Directory { uint32_t LumpOffset; uint32_t LumpSize; char LumpName[9]; };
Agora vamos adicionar a função ReadDirectories: conte o deslocamento e produza-os!
Em cada iteração, multiplicamos i * 16 para ir para o incremento de deslocamento do próximo diretório.
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; }
Execute o código e veja o que acontece. Uau! Uma grande lista de diretórios.
A julgar pelo nome fixo, podemos assumir que conseguimos ler os dados corretamente, mas talvez haja uma maneira melhor de verificar isso. Vamos dar uma olhada nas entradas do diretório WAD usando o Slade3.
Parece que o nome e o tamanho do nódulo correspondem aos dados obtidos usando nosso código. Hoje fizemos um ótimo trabalho!
Outras notas
- Em algum momento, pensei que seria bom usar o vetor para armazenar diretórios. Por que não usar o mapa? Isso será mais rápido do que obter dados pela pesquisa vetorial linear. Esta é uma má ideia. Ao usar o mapa, a ordem das entradas do diretório não será rastreada, mas precisamos dessas informações para obter os dados corretos.
E outro equívoco: o mapa em C ++ é implementado como árvores vermelho-pretas com tempo de pesquisa O (log N), e as iterações no mapa sempre fornecem uma ordem crescente de chaves. Se você precisar de uma estrutura de dados que dê o tempo médio O (1) e o pior tempo O (N), será necessário usar um mapa não ordenado. Carregar todos os arquivos WAD na memória não é um método de implementação ideal. Seria mais lógico simplesmente ler os diretórios no cabeçalho da memória e retornar ao arquivo WAD e carregar recursos do disco. Espero que um dia possamos aprender mais sobre o cache.
DOOMReboot : discordo completamente. Hoje em dia, 15 MB de RAM são uma ninharia completa, e a leitura da memória será muito mais rápida que o volumoso fseek, que terá que ser usado após o download de tudo o que é necessário para o nível. Isso aumentará o tempo de download em não menos de um a dois segundos (levo menos de 20 ms para baixar o tempo todo). fseek usa o sistema operacional. Qual arquivo é mais provável no cache da RAM, mas talvez não. Mas mesmo se ele estiver lá, é um grande desperdício de recursos e essas operações confundirão muitas leituras do WAD em termos de cache da CPU. O melhor é que você pode criar métodos de inicialização híbridos e armazenar dados WAD para um nível que se encaixe no cache L3 dos processadores modernos, onde a economia será incrível.
Código fonte
Código fonteDados Básicos do Cartão
Tendo aprendido a ler o arquivo WAD, vamos tentar usar os dados lidos. Será ótimo aprender a ler os dados da missão (nível / mundo) e aplicá-los. Os "pedaços" dessas missões (Mission Lumps) devem ser algo complexo e complicado. Portanto, precisaremos mover e desenvolver o conhecimento gradualmente. Como primeiro pequeno passo, vamos criar algo como um recurso Automap: um plano bidimensional de um mapa com uma vista superior. Primeiro, vamos ver o que há dentro do Mission Lump.
Anatomia do cartão
Vamos começar de novo: a descrição dos níveis de DOOM é muito semelhante ao desenho 2D, no qual as paredes são marcadas com linhas. No entanto, para obter coordenadas 3D, cada parede mede a altura do piso e do teto (XY é o plano ao longo do qual nos movemos horizontalmente e Z é a altura que nos permite subir e descer, por exemplo, levantando em um elevador ou pulando de uma plataforma. os componentes de coordenadas são usados para tornar a missão como um mundo 3D, no entanto, para garantir um bom desempenho, o mecanismo tem certas limitações: não há salas localizadas uma acima da outra nos níveis e o jogador não pode olhar para cima e para baixo. O rock, por exemplo, foguetes, ascende verticalmente para atingir um alvo localizado em uma plataforma mais alta.
Esses recursos curiosos causaram incontáveis holivares sobre se o DOOM é um mecanismo 2D ou 3D. Gradualmente, foi alcançado um compromisso diplomático que salvou muitas vidas: as partes concordaram com a designação “2.5D” aceitável para ambos.
Para simplificar a tarefa e retornar ao tópico, vamos apenas tentar ler esses dados 2D e ver se eles podem ser usados de alguma forma. Mais tarde, tentaremos renderizá-los em 3D, mas, por enquanto, precisamos entender como as partes individuais do mecanismo funcionam juntas.
Após realizar pesquisas, descobri que cada missão é composta por um conjunto de "peças". Esses "nódulos" são sempre representados no arquivo WAD de um jogo DOOM na mesma ordem.
- Vértices: os pontos finais das paredes em 2D. Dois VERTEXs conectados formam um LINEDEF. Três VERTEX conectados formam duas paredes / LINEDEF, e assim por diante. Eles podem ser simplesmente percebidos como os pontos de conexão de duas ou mais paredes. (Sim, a maioria das pessoas prefere o plural "Vertices", mas John Carmack não gostou. Segundo merriam-webster , as duas opções se aplicam.
- LINEDEFS: linhas formando juntas entre vértices e formando paredes. Nem todas as linhas (paredes) se comportam da mesma forma; existem sinalizadores que especificam o comportamento dessas linhas.
- SIDEDDEFS: na vida real, as paredes têm dois lados - olhamos para um, o segundo está do outro lado. Os dois lados podem ter texturas diferentes, e SIDEDEFS é o nódulo que contém as informações de textura da parede (LINEDEF).
- SETORES: setores são “salas” obtidas pela junção do LINEDEF. Cada setor contém informações como altura do piso e do teto, texturas, valores de iluminação, ações especiais, como mover pisos / plataformas / elevadores. Alguns desses parâmetros também afetam a maneira como as paredes são renderizadas, por exemplo, o nível de iluminação e o cálculo das coordenadas do mapeamento de textura.
- SSECTORS: (subsetores) formam áreas convexas dentro de um setor que são usadas na renderização em conjunto com o desvio do BSP e também ajudam a determinar onde um jogador está em um nível específico. Eles são bastante úteis e costumam ser usados para determinar a posição vertical de um jogador. Cada SSECTOR consiste em partes conectadas de um setor, por exemplo, de paredes formando um ângulo. Essas partes das paredes, ou "segmentos", são armazenadas em seu próprio caroço chamado ...
- SEGS: peças de parede / LINEDEF; em outras palavras, esses são os “segmentos” da parede / LINEDEF. O mundo é renderizado ignorando a árvore BSP para determinar quais paredes desenhar primeiro (as primeiras são as mais próximas). Embora o sistema funcione muito bem, faz com que os alinhados sejam divididos em dois ou mais SEGs. Esses SEGs são então usados para renderizar paredes em vez de LINEDEF. A geometria de cada SSECTOR é determinada pelos segs contidos nela.
- NODES: Um nó BSP é um nó de uma estrutura de árvore binária que armazena dados do subsetor. É usado para determinar rapidamente qual SSECTOR (e SEG) está na frente do player. A eliminação de SEGs localizados atrás do player e, portanto, invisíveis, permite que o mecanismo se concentre em SEGs potencialmente visíveis, o que reduz significativamente o tempo de renderização.
- COISAS: Nódulo chamado COISAS é uma lista de atores de cenários e missões (inimigos, armas, etc.). Cada elemento desse agrupamento contém informações sobre uma instância do ator / conjunto, por exemplo, o tipo de objeto, o ponto de criação, a direção e assim por diante.
- REJEITAR: esse número contém dados sobre quais setores são visíveis de outros setores. É usado para determinar quando um monstro aprende sobre a presença de um jogador. Também é usado para determinar a faixa de distribuição de sons criados pelo player, por exemplo, tiros. Quando esse som pode ser transmitido para o setor do monstro, ele pode descobrir sobre o jogador. A tabela REJECT também pode ser usada para acelerar o reconhecimento de colisões de cartuchos de armas.
- BLOCKMAP: informações de reconhecimento de colisão de jogador e movimento de COISA. Consiste em uma grade que cobre a geometria de toda a missão. Cada célula da grade contém uma lista de LINEDEFs que estão dentro ou se cruzam com ela. É usado para acelerar significativamente o reconhecimento de colisões: as verificações de colisão são necessárias apenas para alguns LINEDEF por jogador / COISA, o que economiza significativamente o poder de computação.
Ao gerar nosso mapa 2D, focaremos em VERTEXES e LINEDEFS. Se conseguirmos desenhar os vértices e conectá-los às linhas dadas porlinedef, precisamos gerar um modelo 2D do mapa.
O cartão de demonstração mostrado acima tem as seguintes características:
- 4 picos
- vértice 1 em (10.10)
- 2 principais em (10.100)
- 3 principais em (100, 10)
- pico 4 in (100.100)
- 4 linhas
- linha do topo 1 ao 2
- linha do top 1 ao 3
- linha do top 2 ao 4
- linha do top 3 ao 4
Formato de vértice
Como você pode esperar, os dados do vértice são muito simples - apenas x e y (ponto) de algumas coordenadas.
Formato Linedef
O Linedef contém mais informações; descreve a linha que liga os dois vértices e as propriedades dessa linha (que mais tarde se tornará uma parede).
Valores do sinalizador Linedef
Nem todas as linhas (paredes) são desenhadas. Alguns deles têm um comportamento especial.
Objetivos
- Crie uma classe de mapa.
- Leia dados de vértice.
- Leia os dados alinhados.
Arquitetura
Primeiro, vamos criar uma classe e chamar de mapa. Nele, armazenaremos todos os dados associados ao cartão.
Por enquanto, pretendo armazenar apenas vértices e alinhamentos como um vetor, para poder aplicá-los mais tarde.
Além disso, vamos complementar o WADLoader e o WADReader para que possamos ler essas duas novas informações.
Codificação
O código será semelhante ao código de leitura do WAD, adicionaremos apenas mais algumas estruturas e as preencheremos com dados do WAD. Vamos começar adicionando uma nova classe e passando o nome do mapa.
class Map { public: Map(std::string sName); ~Map(); std::string GetName();
Agora adicione estruturas para ler esses novos campos. Como já fizemos isso várias vezes, basta adicioná-los todos de uma 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; };
Em seguida, precisamos de uma função para lê-los no WADReader, pois ela estará próxima do que fizemos anteriormente. 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); }
Eu acho que não há nada novo para você aqui. E agora precisamos chamar essas funções da classe WADLoader. Deixe-me declarar os fatos: a sequência de nódulos é importante aqui, encontraremos o nome do mapa no diretório lump, seguido por todos os nódulos associados aos mapas na ordem especificada. Para simplificar nossa tarefa e não rastrear os índices de massa separadamente, adicionaremos uma enumeração que nos permite eliminar os números mágicos. enum EMAPLUMPSINDEX { eTHINGS = 1, eLINEDEFS, eSIDEDDEFS, eVERTEXES, eSEAGS, eSSECTORS, eNODES, eSECTORS, eREJECT, eBLOCKMAP, eCOUNT };
Também adicionarei uma função para procurar um mapa por seu nome na lista de diretórios. Posteriormente, provavelmente aumentaremos o desempenho dessa etapa usando a estrutura de dados do mapa, porque há um número significativo de registros aqui, e teremos que analisá-los com bastante frequência, especialmente no início do carregamento de recursos como texturas, sprites, 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; }
Uau, estamos quase terminando! Agora, vamos apenas contar VERTEXES! Repito, já fizemos isso antes, agora você deve entender isso. 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 constantemente copiando o mesmo código; pode ser necessário otimizá-lo no futuro, mas por enquanto você implementará o ReadMapLinedef (ou examinará o código-fonte no link).Toques finais - precisamos chamar essa função e passar o objeto do mapa para ela. 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; }
Agora vamos mudar a função principal e ver se tudo funciona. Quero carregar o mapa "E1M1", que transferirei para o objeto de mapa. Map map("E1M1"); wadloader.LoadMapData(map);
Agora vamos executar tudo. Uau, um monte de números interessantes, mas eles são verdadeiros? Vamos conferir!Vamos ver se o slade pode nos ajudar com isso.Podemos encontrar o mapa no menu slade e ver os detalhes dos pedaços. Vamos comparar os números.Ótimo!
E o Linedef?Também adicionei essa enumeração, que tentaremos usar ao renderizar o mapa. enum ELINEDEFFLAGS { eBLOCKING = 0, eBLOCKMONSTERS = 1, eTWOSIDED = 2, eDONTPEGTOP = 4, eDONTPEGBOTTOM = 8, eSECRET = 16, eSOUNDBLOCK = 32, eDONTDRAW = 64, eDRAW = 128 };
Outras notas
No processo de escrever o código, li por engano mais bytes do que o necessário e recebi valores incorretos. Para depuração, comecei a olhar para o deslocamento WAD na memória para ver se estava no deslocamento certo. Isso pode ser feito usando a janela de memória do Visual Studio, que é uma ferramenta muito útil para rastrear bytes ou memória (você também pode definir pontos de interrupção nesta janela).Se você não vir a janela de memória, vá para Debug> Memory> Memory.Agora vemos os valores na memória em hexadecimal. Esses valores podem ser comparados com a exibição hexadecimal no slade clicando com o botão direito do mouse em qualquer massa e exibindo-a como hexadecimal.Compare-os com o endereço do WAD carregado na memória.E a última coisa de hoje: vimos todos esses valores de vértices, mas existe uma maneira fácil de visualizá-los sem escrever código? Não quero perder tempo com isso, apenas para descobrir que estamos nos movendo na direção errada.Certamente alguém já criou um plotter. Pesquisei no Google “desenhar pontos em um gráfico” e o primeiro resultado foi o site Plot Points - Desmos . Nele, você pode colar números da área de transferência e ele os desenhará. Eles devem estar no formato "(x, y)". Para obtê-lo, basta alterar a função de saída para a tela. cout << "(" << vertex.XPosition << "," << vertex.YPosition << ")" << endl;
Uau! Já parece um E1M1! Conseguimos algo!Se você tiver preguiça de fazer isso, aqui está um link para um gráfico pontilhado: Plot Vertex .Mas vamos dar mais um passo: depois de um pouco de trabalho, podemos conectar esses pontos com base em linhas alinhadas.Aqui está o link: E1M1 Plot VertexCódigo fonte
Código fonteReferências
Doom WikiZDoom Wiki