Introdução à programação: um simples atirador 3D desde o início no fim de semana, parte 2

Continuamos a conversa sobre o jogo de tiro em 3D no fim de semana. Se alguma coisa, então eu lembro que esta é a segunda metade:


Como eu disse, faço o possível para apoiar o desejo dos alunos de fazer algo com suas próprias mãos. Em particular, quando dou um curso de palestras sobre a introdução à programação, então, como exercícios práticos, deixo a eles quase total liberdade. Existem apenas duas limitações: a linguagem de programação (C ++) e o tema do projeto, este deve ser um videogame. Aqui está um exemplo de um dos centenas de jogos que meus alunos do primeiro ano fizeram:


Infelizmente, a maioria dos estudantes escolhe jogos simples, como plataformas 2D. Estou escrevendo este artigo para mostrar que criar a ilusão de um mundo tridimensional não é mais difícil do que clonar mario broz.

Lembro que paramos em um estágio que permite texturizar as paredes:





Etapa 13: desenhe monstros no mapa


O que é um monstro no nosso jogo? Estas são suas coordenadas e número de textura:

struct Sprite { float x, y; size_t tex_id; }; [..] std::vector<Sprite> sprites{ {1.834, 8.765, 0}, {5.323, 5.365, 1}, {4.123, 10.265, 1} }; 

Tendo definido vários monstros, para começar, simplesmente os desenhamos no mapa:



As mudanças que você pode ver aqui .
Abrir no gitpod



Etapa 14: quadrados pretos em vez de monstros em 3D


Agora vamos desenhar sprites na janela 3D. Para fazer isso, precisamos determinar duas coisas: a posição do sprite na tela e seu tamanho. Aqui está a função que desenha um quadrado preto no lugar de cada sprite:

 void draw_sprite(Sprite &sprite, FrameBuffer &fb, Player &player, Texture &tex_sprites) { // absolute direction from the player to the sprite (in radians) float sprite_dir = atan2(sprite.y - player.y, sprite.x - player.x); // remove unnecessary periods from the relative direction while (sprite_dir - player.a > M_PI) sprite_dir -= 2*M_PI; while (sprite_dir - player.a < -M_PI) sprite_dir += 2*M_PI; // distance from the player to the sprite float sprite_dist = std::sqrt(pow(player.x - sprite.x, 2) + pow(player.y - sprite.y, 2)); size_t sprite_screen_size = std::min(2000, static_cast<int>(fb.h/sprite_dist)); // do not forget the 3D view takes only a half of the framebuffer, thus fb.w/2 for the screen width int h_offset = (sprite_dir - player.a)*(fb.w/2)/(player.fov) + (fb.w/2)/2 - sprite_screen_size/2; int v_offset = fb.h/2 - sprite_screen_size/2; for (size_t i=0; i<sprite_screen_size; i++) { if (h_offset+int(i)<0 || h_offset+i>=fb.w/2) continue; for (size_t j=0; j<sprite_screen_size; j++) { if (v_offset+int(j)<0 || v_offset+j>=fb.h) continue; fb.set_pixel(fb.w/2 + h_offset+i, v_offset+j, pack_color(0,0,0)); } } } 

Vamos descobrir como isso funciona. Aqui está o diagrama:



Na primeira linha, consideramos o ângulo absoluto sprite_dir (o ângulo entre a direção do jogador para o sprite e a abcissa). O ângulo relativo entre o sprite e a direção do olhar é obviamente obtido subtraindo simplesmente dois ângulos absolutos: sprite_dir - player.a. A distância do jogador ao sprite é trivial para calcular, e o tamanho do sprite é uma simples divisão do tamanho da tela pela distância. Bem, por precaução, cortei dois mil do topo para não obter quadrados gigantes (a propósito, esse código pode ser facilmente dividido por zero). h_offset e v_offset fornecem as coordenadas do canto superior esquerdo do sprite na tela; então um simples loop duplo preenche nosso quadrado com preto. Verifique com uma caneta e um pedaço de papel se h_offset e v_offset estão corretamente calculados, no meu commit há um erro (não crítico), acredite no código do artigo :) Bem, o código mais recente no repositório também foi corrigido.



As mudanças que você pode ver aqui .

Abrir no gitpod



Etapa 15: Mapa de Profundidade


Nossos quadrados são milagrosamente bons, mas há apenas um problema: o monstro distante espreita na esquina, e o quadrado é desenhado inteiramente. Como ser Muito simples Nós desenhamos sprites depois que as paredes foram desenhadas. Portanto, para cada coluna da tela, sabemos a distância da parede mais próxima. Salvamos essas distâncias em uma matriz de 512 valores e passamos a matriz para a função de renderização de sprite. Os sprites também são desenhados coluna por coluna; portanto, para cada coluna do sprite, compararemos a distância a ele com o valor da nossa matriz de profundidade.


As mudanças que você pode ver aqui .

Abrir no gitpod



Etapa 16: problema com sprites


Eles se tornaram grandes monstros, não é? Mas, neste estágio, não adicionarei nenhuma funcionalidade, pelo contrário, quebrarei tudo adicionando outro monstro:


As mudanças que você pode ver aqui .

Abrir no gitpod



Etapa 17: classificação de sprites


Qual foi o problema? O problema é que eu posso ter uma ordem arbitrária de desenhar sprites e, para cada um deles, comparo sua distância com as paredes, mas não com outros sprites, de modo que a criatura distante se arrasta sobre o mais próximo. É possível adaptar uma solução com um mapa de profundidade para desenhar sprites?

Texto oculto
A resposta correta é "você pode". Mas como Escreva nos comentários.

Eu vou para o outro lado, resolvendo o problema estupidamente na testa. Vou desenhar todos os sprites do mais distante ao mais distante. Ou seja, vou ordenar os sprites em ordem decrescente de distância e desenhá-los nessa ordem.


As mudanças que você pode ver aqui .

Abrir no gitpod



Etapa 18: Horário do SDL


Chegou a hora do SDL. Existem várias bibliotecas de janelas multiplataforma e eu não as entendo. Pessoalmente, eu gosto do imgui , mas por algum motivo meus alunos preferem o SDL, então eu faço o link com ele. A tarefa para esta etapa é muito simples: crie uma janela e exiba a imagem da etapa anterior:



As mudanças que você pode ver aqui . Não dou mais um link para o gitpod, porque SDL no navegador ainda não aprendeu a iniciar :(

Atualização: APRENDIDO! Você pode executar o código em um clique no navegador!

Abrir no gitpod

Etapa 19: Processamento e limpeza de eventos


Adicionar uma reação ao pressionamento de teclas nem sequer é engraçado, não vou descrever. Ao adicionar o SDL, removi a dependência em stb_image.h. É bonito, mas leva muito tempo para compilar.

Para quem não entende, as fontes do décimo nono estágio estão aqui . Bem, aqui está uma performance típica:


Conclusão


No momento, meu código contém apenas 486 linhas e, ao mesmo tempo, não os salvei:

 haqreu@daffodil:~/tinyraycaster$ cat *.cpp *.h | wc -l 486 

Não lambi meu código, intencionalmente jogando roupa suja. Sim, eu escrevo assim (e não apenas eu). Um sábado de manhã, sentei-me e escrevi isso :)

Eu não fiz o jogo terminado, minha tarefa é apenas dar um impulso inicial para a fuga da sua imaginação. Escreva seu próprio código, provavelmente será melhor que o meu. Compartilhe seu código, compartilhe suas idéias, envie solicitações pull.

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


All Articles