Introduction à la programmation: un jeu de tir 3D simple à partir du sol pendant le week-end, partie 2

Nous poursuivons la conversation sur le jeu de tir 3D ce week-end. Si quoi que ce soit, je vous rappelle que c'est la seconde moitié:


Comme je l'ai dit, je fais de mon mieux pour soutenir le désir des élèves de faire quelque chose de leurs propres mains. En particulier, lorsque je donne un cours sur l'introduction à la programmation, puis comme exercices pratiques je leur laisse une liberté presque totale. Il n'y a que deux limitations: le langage de programmation (C ++) et le thème du projet, ce devrait être un jeu vidéo. Voici un exemple de l'un des centaines de jeux créés par mes étudiants de première année:


Malheureusement, la plupart des étudiants choisissent des jeux simples tels que des jeux de plateforme 2D. J'écris cet article pour montrer que créer l'illusion d'un monde en trois dimensions n'est pas plus difficile que de cloner mario broz.

Je vous rappelle que nous nous sommes arrêtés à une étape qui vous permet de texturer les murs:





Étape 13: dessinez des monstres sur la carte


Qu'est-ce qu'un monstre dans notre jeu? Ce sont ses coordonnées et son numéro de texture:

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} }; 

Après avoir défini plusieurs monstres, pour commencer nous les dessinons simplement sur la carte:



Les changements que vous pouvez voir ici .
Ouvrir dans gitpod



Étape 14: des carrés noirs au lieu de monstres en 3D


Nous allons maintenant dessiner des sprites dans la fenêtre 3D. Pour ce faire, nous devons déterminer deux choses: la position du sprite sur l'écran et sa taille. Voici la fonction qui dessine un carré noir à la place de chaque 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)); } } } 

Voyons comment cela fonctionne. Voici le schéma:



Dans la première ligne, nous considérons l'angle absolu sprite_dir (l'angle entre la direction du joueur au sprite et l'abscisse). L'angle relatif entre le sprite et la direction du regard est évidemment obtenu en soustrayant simplement deux angles absolus: sprite_dir - player.a. La distance du joueur au sprite est triviale à calculer, et la taille du sprite est une simple division de la taille de l'écran par la distance. Eh bien, juste au cas où, j'en couperais deux mille du haut pour ne pas avoir de carrés géants (au fait, ce code peut facilement être divisé par zéro). h_offset et v_offset donnent les coordonnées du coin supérieur gauche du sprite sur l'écran; puis une simple double boucle remplit notre carré de noir. Vérifiez avec un stylo et un morceau de papier que h_offset et v_offset sont correctement calculés, dans mon commit il y a une erreur (non critique), croyez le code dans l'article :) Eh bien, le code le plus récent dans le référentiel a également été corrigé.



Les changements que vous pouvez voir ici .

Ouvrir dans gitpod



Étape 15: Carte de profondeur


Nos carrés sont miraculeusement bons, mais il n'y a qu'un seul problème: le monstre lointain jette un coup d'œil au coin et le carré est entièrement dessiné. Comment être Très simple. Nous dessinons des sprites après le dessin des murs. Par conséquent, pour chaque colonne de notre écran, nous connaissons la distance au mur le plus proche. Nous enregistrons ces distances dans un tableau de 512 valeurs et passons le tableau à la fonction de rendu de sprite. Les sprites sont également dessinés colonne par colonne, donc pour chaque colonne du sprite, nous comparerons la distance à celle-ci avec la valeur de notre tableau de profondeur.


Les changements que vous pouvez voir ici .

Ouvrir dans gitpod



Étape 16: problème avec les sprites


Ils sont devenus de grands monstres, non? Mais à ce stade je n'ajouterai aucune fonctionnalité, au contraire, je vais tout casser en ajoutant un autre monstre:


Les changements que vous pouvez voir ici .

Ouvrir dans gitpod



Étape 17: tri des sprites


Quel était le problème? Le problème est que je peux avoir un ordre arbitraire de dessin de sprites, et pour chacun d'eux je compare sa distance avec les murs, mais pas avec d'autres sprites, de sorte que la créature éloignée rampait sur la plus proche. Est-il possible d'adapter une solution avec une carte de profondeur pour dessiner des sprites?

Texte masqué
La bonne réponse est «vous pouvez». Mais comment? Écrivez dans les commentaires.

J'irai dans l'autre sens, en résolvant le problème bêtement dans le front. Je vais simplement dessiner tous les sprites du plus éloigné au plus éloigné. Autrement dit, je vais trier les sprites par ordre décroissant de distance et les dessiner dans cet ordre.


Les changements que vous pouvez voir ici .

Ouvrir dans gitpod



Étape 18: Heure SDL


Le temps est venu pour SDL. Il existe de nombreuses bibliothèques de fenêtres multiplateformes différentes, et je ne les comprends pas du tout. Personnellement, j'aime imgui , mais pour une raison quelconque mes étudiants préfèrent SDL, donc je fais le lien avec lui. La tâche de cette étape est très simple: créez une fenêtre et affichez l'image de l'étape précédente:



Les changements que vous pouvez voir ici . Je ne donne plus de lien vers le gitpod, car SDL dans le navigateur n'a pas encore appris à démarrer :(

Mise à jour: APPRIS! Vous pouvez exécuter le code en un clic dans le navigateur!

Ouvrir dans gitpod

Étape 19: Traitement et nettoyage des événements


Ajouter une réaction aux frappes n'est même pas drôle, je ne décrirai pas. Lors de l'ajout de SDL, j'ai supprimé la dépendance sur stb_image.h. C'est beau, mais ça prend trop de temps à compiler.

Pour ceux qui ne comprennent pas, les sources de la dix-neuvième étape sont ici . Eh bien, voici une performance typique:


Conclusion


Mon code ne contient pour le moment que 486 lignes, et en même temps je ne les ai pas économisées du tout:

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

Je n'ai pas léché mon code, jetant intentionnellement du linge sale. Oui, j'écris comme ça (et pas seulement moi). Un samedi matin, je me suis juste assis et j'ai écrit ceci :)

Je n'ai pas fait le jeu fini, ma tâche est seulement de donner une impulsion initiale au vol de votre imagination. Écrivez votre propre code, il sera probablement meilleur que le mien. Partagez votre code, partagez vos idées, envoyez des demandes de tirage.

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


All Articles