Einführung in die Programmierung: Ein einfacher 3D-Shooter von Grund auf über das Wochenende, Teil 2

Wir setzen das Gespräch über 3D-Shooter über das Wochenende fort. Wenn überhaupt, dann erinnere ich Sie daran, dass dies die zweite Hälfte ist:


Wie gesagt, ich gebe mein Bestes, um den Wunsch der Schüler zu unterstützen, etwas mit ihren eigenen Händen zu tun. Insbesondere wenn ich Vorlesungen über die Einführung in die Programmierung halte, lasse ich ihnen als praktische Übungen fast völlige Freiheit. Es gibt nur zwei Einschränkungen: Die Programmiersprache (C ++) und das Thema des Projekts, dies sollte ein Videospiel sein. Hier ist ein Beispiel für eines der Hunderte von Spielen, die meine Erstsemester gemacht haben:


Leider wählen die meisten Schüler einfache Spiele wie 2D-Plattformer. Ich schreibe diesen Artikel, um zu zeigen, dass es nicht schwieriger ist, die Illusion einer dreidimensionalen Welt zu erschaffen, als Mario Broz zu klonen.

Ich erinnere Sie daran, dass wir an einem Punkt angehalten haben, an dem Sie die Wände strukturieren können:





Stufe 13: Zeichne Monster auf die Karte


Was ist ein Monster in unserem Spiel? Dies sind seine Koordinaten und Texturnummer:

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

Nachdem wir mehrere Monster definiert haben, zeichnen wir sie zunächst einfach auf die Karte:



Die Änderungen können Sie hier sehen .
Im Gitpod öffnen



Stufe 14: Schwarze Quadrate anstelle von Monstern in 3D


Jetzt werden wir Sprites im 3D-Fenster zeichnen. Dazu müssen wir zwei Dinge bestimmen: die Position des Sprites auf dem Bildschirm und seine Größe. Hier ist die Funktion, die anstelle jedes Sprites ein schwarzes Quadrat zeichnet:

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

Lassen Sie uns herausfinden, wie es funktioniert. Hier ist das Diagramm:



In der ersten Zeile betrachten wir den absoluten Winkel sprite_dir (den Winkel zwischen der Richtung vom Spieler zum Sprite und der Abszisse). Der relative Winkel zwischen dem Sprite und der Blickrichtung wird offensichtlich durch einfaches Subtrahieren von zwei absoluten Winkeln erhalten: sprite_dir - player.a. Die Entfernung vom Spieler zum Sprite ist trivial zu berechnen, und die Größe des Sprites ist eine einfache Division der Bildschirmgröße durch die Entfernung. Nun, nur für den Fall, ich schneide zweitausend von oben ab, um keine riesigen Quadrate zu erhalten (dieser Code kann übrigens leicht durch Null geteilt werden). h_offset und v_offset geben die Koordinaten der oberen linken Ecke des Sprites auf dem Bildschirm an. dann füllt eine einfache Doppelschleife unser Quadrat mit Schwarz. Überprüfen Sie mit einem Stift und einem Blatt Papier, ob h_offset und v_offset korrekt berechnet wurden. Bei meinem Commit liegt ein (unkritischer) Fehler vor. Glauben Sie dem Code im Artikel :) Nun, der neuere Code im Repository wurde ebenfalls behoben.



Die Änderungen können Sie hier sehen .

Im Gitpod öffnen



Schritt 15: Tiefenkarte


Unsere Quadrate sind auf wundersame Weise gut, aber es gibt nur ein Problem: Das entfernte Monster späht um die Ecke und das Quadrat ist vollständig gezeichnet. Wie man ist Sehr einfach. Wir zeichnen Sprites, nachdem die Wände gezeichnet wurden. Daher kennen wir für jede Spalte unseres Bildschirms den Abstand zur nächsten Wand. Wir speichern diese Abstände in einem Array von 512 Werten und übergeben das Array an die Sprite-Rendering-Funktion. Die Sprites werden auch spaltenweise gezeichnet, sodass wir für jede Spalte des Sprites den Abstand dazu mit dem Wert aus unserem Tiefenarray vergleichen.


Die Änderungen können Sie hier sehen .

Im Gitpod öffnen



Stufe 16: Problem mit Sprites


Sie sind großartige Monster geworden, nicht wahr? Aber zu diesem Zeitpunkt werde ich keine Funktionalität hinzufügen, im Gegenteil, ich werde alles durch Hinzufügen eines weiteren Monsters zerstören:


Die Änderungen können Sie hier sehen .

Im Gitpod öffnen



Stufe 17: Sprites sortieren


Was war das Problem? Das Problem ist, dass ich eine beliebige Reihenfolge für das Zeichnen von Sprites haben kann und für jeden von ihnen die Entfernung mit den Wänden vergleiche, aber nicht mit anderen Sprites, sodass die entfernte Kreatur über die nächste kroch. Ist es möglich, eine Lösung mit einer Tiefenkarte zum Zeichnen von Sprites anzupassen?

Versteckter Text
Die richtige Antwort lautet "Sie können". Aber wie? Schreiben Sie in die Kommentare.

Ich werde den anderen Weg gehen und das Problem dumm in der Stirn lösen. Ich werde einfach alle Sprites vom entferntesten zum am weitesten entfernten zeichnen. Das heißt, ich werde die Sprites in absteigender Reihenfolge der Entfernung sortieren und sie in dieser Reihenfolge zeichnen.


Die Änderungen können Sie hier sehen .

Im Gitpod öffnen



Schritt 18: SDL-Zeit


Es ist Zeit für SDL. Es gibt viele verschiedene plattformübergreifende Fensterbibliotheken, und ich verstehe sie überhaupt nicht. Persönlich mag ich imgui , aber aus irgendeinem Grund bevorzugen meine Schüler SDL, also verbinde ich mich damit. Die Aufgabe für diese Phase ist sehr einfach: Erstellen Sie ein Fenster und zeigen Sie das Bild aus der vorherigen Phase an:



Die Änderungen können Sie hier sehen . Ich gebe keinen Link mehr zum Gitpod, weil SDL im Browser hat noch nicht gelernt zu starten :(

Update: ERLERNT! Sie können den Code mit einem Klick im Browser ausführen!

Im Gitpod öffnen

Schritt 19: Ereignisverarbeitung und -bereinigung


Eine Reaktion auf Tastenanschläge hinzuzufügen ist nicht einmal lustig, ich werde es nicht beschreiben. Beim Hinzufügen von SDL habe ich die Abhängigkeit von stb_image.h entfernt. Es ist schön, aber das Kompilieren dauert zu lange.

Für diejenigen, die nicht verstehen, sind die Quellen der neunzehnten Stufe hier . Nun, hier ist eine typische Leistung:


Fazit


Mein Code enthält im Moment nur 486 Zeilen, und gleichzeitig habe ich sie überhaupt nicht gespeichert:

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

Ich habe meinen Code nicht geleckt und absichtlich schmutzige Wäsche entsorgt. Ja, ich schreibe so (und nicht nur ich). An einem Samstagmorgen habe ich mich einfach hingesetzt und das geschrieben :)

Ich habe das fertige Spiel nicht geschafft, meine Aufgabe ist es nur, einen ersten Anstoß für den Flug Ihrer Fantasie zu geben. Schreiben Sie Ihren eigenen Code, er wird wahrscheinlich besser sein als meiner. Teilen Sie Ihren Code, teilen Sie Ihre Ideen, senden Sie Pull-Anfragen.

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


All Articles