Introducción a la programación: un simple tirador 3D desde cero durante el fin de semana, parte 2

Continuamos la conversación sobre el tirador 3D durante el fin de semana. En todo caso, les recuerdo que esta es la segunda mitad:


Como dije, hago todo lo posible para apoyar el deseo de los estudiantes de hacer algo con sus propias manos. En particular, cuando doy un curso de conferencias sobre la introducción a la programación, luego, como ejercicios prácticos, les dejo una libertad casi completa. Solo hay dos limitaciones: el lenguaje de programación (C ++) y el tema del proyecto, este debería ser un videojuego. Aquí hay un ejemplo de uno de los cientos de juegos que hicieron mis estudiantes de primer año:


Desafortunadamente, la mayoría de los estudiantes eligen juegos simples como plataformas 2D. Escribo este artículo para mostrar que crear la ilusión de un mundo tridimensional no es más difícil que clonar Mario Broz.

Te recuerdo que nos detuvimos en una etapa que te permite texturizar las paredes:





Etapa 13: dibuja monstruos en el mapa


¿Qué es un monstruo en nuestro juego? Estas son sus coordenadas y 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} }; 

Habiendo definido varios monstruos, para empezar, simplemente los dibujamos en el mapa:



Los cambios que puedes ver aquí .
Abierto en gitpod



Etapa 14: cuadrados negros en lugar de monstruos en 3D


Ahora dibujaremos sprites en la ventana 3D. Para hacer esto, necesitamos determinar dos cosas: la posición del sprite en la pantalla y su tamaño. Aquí está la función que dibuja un cuadrado negro en 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)); } } } 

Veamos cómo funciona. Aquí está el diagrama:



En la primera línea, consideramos el ángulo absoluto sprite_dir (el ángulo entre la dirección del jugador al sprite y la abscisa). Obviamente, el ángulo relativo entre el sprite y la dirección de la mirada se obtiene simplemente restando dos ángulos absolutos: sprite_dir - player.a. La distancia del jugador al sprite es trivial de calcular, y el tamaño del sprite es una simple división del tamaño de la pantalla por la distancia. Bueno, por si acaso, corté dos mil de la parte superior para no obtener cuadrados gigantes (por cierto, este código se puede dividir fácilmente por cero). h_offset y v_offset dan las coordenadas de la esquina superior izquierda del sprite en la pantalla; entonces un simple doble circuito llena nuestro cuadrado de negro. Compruebe con un bolígrafo y una hoja de papel que h_offset y v_offset están calculados correctamente, en mi confirmación hay un error (no crítico), crea el código en el artículo :) Bueno, el código más reciente en el repositorio también se ha solucionado.



Los cambios que puedes ver aquí .

Abierto en gitpod



Paso 15: Mapa de profundidad


Nuestros cuadrados son milagrosamente buenos, pero solo hay un problema: el monstruo distante se asoma a la vuelta de la esquina, y el cuadrado se dibuja por completo. Como ser Muy simple Dibujamos sprites después de dibujar las paredes. Por lo tanto, para cada columna de nuestra pantalla, sabemos la distancia a la pared más cercana. Guardamos estas distancias en una matriz de 512 valores y pasamos la matriz a la función de representación de sprites. Los sprites también se dibujan columna por columna, por lo que para cada columna del sprite compararemos la distancia con el valor de nuestra matriz de profundidad.


Los cambios que puedes ver aquí .

Abierto en gitpod



Etapa 16: problema con los sprites


Se convirtieron en grandes monstruos, ¿no? Pero en esta etapa no agregaré ninguna funcionalidad, por el contrario, romperé todo agregando otro monstruo:


Los cambios que puedes ver aquí .

Abierto en gitpod



Etapa 17: clasificación de sprites


Cual era el problema El problema es que puedo tener un orden arbitrario de dibujar sprites, y para cada uno de ellos comparo su distancia con las paredes, pero no con otros sprites, por lo que la criatura distante se arrastró sobre el más cercano. ¿Es posible adaptar una solución con un mapa de profundidad para dibujar sprites?

Texto oculto
La respuesta correcta es "puedes". Pero como? Escribe en los comentarios.

Iré por el otro lado, resolviendo el problema estúpidamente en la frente. Dibujaré todos los sprites del más lejano al más lejano. Es decir, ordenaré los sprites en orden descendente de distancia y los dibujaré en ese orden.


Los cambios que puedes ver aquí .

Abierto en gitpod



Paso 18: Tiempo SDL


Ha llegado el momento de SDL. Hay muchas bibliotecas de ventanas multiplataforma diferentes, y no las entiendo en absoluto. Personalmente, me gusta imgui , pero por alguna razón mis alumnos prefieren SDL, así que me conecto con él. La tarea para esta etapa es muy simple: crear una ventana y mostrar la imagen de la etapa anterior:



Los cambios que puedes ver aquí . Ya no doy un enlace al gitpod, porque SDL en el navegador aún no ha aprendido a comenzar :(

Actualización: APRENDIDO! ¡Puede ejecutar el código con un clic en el navegador!

Abierto en gitpod

Paso 19: procesamiento y limpieza de eventos


Agregar una reacción a las pulsaciones de teclas ni siquiera es divertido, no lo describiré. Al agregar SDL, eliminé la dependencia de stb_image.h. Es hermoso, pero lleva mucho tiempo compilarlo.

Para aquellos que no entienden, las fuentes de la decimonovena etapa están aquí . Bueno, aquí hay una actuación típica:


Conclusión


Mi código en este momento contiene solo 486 líneas, y al mismo tiempo no las guardé en absoluto:

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

No lamí mi código, arrojé ropa sucia intencionalmente. Sí, escribo así (y no solo yo). Un sábado por la mañana me senté y escribí esto :)

No hice el juego terminado, mi tarea es solo dar un impulso inicial para el vuelo de tu imaginación. Escriba su propio código, probablemente será mejor que el mío. Comparta su código, comparta sus ideas, envíe solicitudes de extracción.

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


All Articles