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í .

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) {
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í .

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í .

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í .

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 ocultoLa 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í .

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!
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.