Einführung in die Programmierung: Ein einfacher 3D-Shooter, der am Wochenende von Grund auf neu erstellt wurde, Teil 2

Published on February 10, 2019

Einführung in die Programmierung: Ein einfacher 3D-Shooter, der am Wochenende von Grund auf neu erstellt wurde, Teil 2

  • Tutorial
Wir reden über das Wochenende weiter über den 3D-Shooter. Wenn überhaupt, erinnere ich Sie daran, dass dies die zweite Hälfte ist:


Wie gesagt, ich unterstütze nachdrücklich den Wunsch der Schüler, etwas mit ihren eigenen Händen zu tun. Insbesondere wenn ich Vorlesungen zur Einführung in das Programmieren lese, lasse ich sie im praktischen Unterricht fast völlig frei. Es gibt nur zwei Einschränkungen: eine Programmiersprache (C ++) und ein Projektthema, dies sollte ein Videospiel sein. Hier ist ein Beispiel für eines der Hunderte von Spielen, die meine Schüler im ersten Jahr 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 einer Stelle angehalten haben, an der Sie die Wände texturieren können:





Stufe 13: Zeichne Monster auf die Karte


Was ist ein Monster in unserem Spiel? Dies sind seine Koordinaten und die 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 Sie mehrere Monster identifiziert haben, zeichnen Sie sie einfach auf die Karte: Hier können Sie die vorgenommenen



Änderungen sehen .
In GitPod öffnen



Stufe 14: Schwarze Quadrate statt Monster in 3D


Jetzt werden wir die 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 verstehen, wie es funktioniert. Hier ist das Diagramm:



In der ersten Zeile betrachten wir den absoluten Winkel sprite_dir (den Winkel zwischen der Richtung vom Player zum Sprite und der x-Achse). Der relative Winkel zwischen dem Sprite und der Blickrichtung wird offensichtlich durch Subtrahieren von zwei absoluten Winkeln erhalten: sprite_dir - player.a. Die Entfernung vom Player zum Sprite ist trivial zu zählen, und die Größe des Sprites ist eine einfache Aufteilung der Bildschirmgröße nach Entfernung. Nun, nur für den Fall, dass ich zweitausend von oben abschneide, 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 die Berechnungen von h_offset und v_offset in meinem (unkritischen) Commit-Fehler korrekt sind, um dem Code im Artikel zu glauben :) Nun, der neuere Code im Repository ist ebenfalls bereits behoben.



Änderungen können Sie hier sehen .

In GitPod öffnen



Stufe 15: Tiefenkarte


Unsere Quadrate sind ein Wunder, aber nur ein Problem: Das ferne Monster schaut um die Ecke und das Quadrat ist vollständig gezeichnet. Wie zu sein Sehr einfach. Wir zeichnen Sprites, nachdem die Wände gestrichen wurden. Daher kennen wir für jede Spalte unseres Bildschirms den Abstand zur nächsten Wand. Speichern Sie diese Abstände in einem Array mit 512 Werten und übergeben Sie ein Array an die Sprite-Zeichenfunktion. Sprites werden auch spaltenweise gezeichnet, sodass wir für jede Spalte des Sprites den Abstand dazu mit dem Wert aus unserem Tiefenarray vergleichen.


Änderungen können Sie hier sehen .

In GitPod öffnen



Schritt 16: Sprites Problem


Es stellte sich heraus, dass große Monster sind, nicht wahr? Aber zu diesem Zeitpunkt werde ich keine Funktionalität hinzufügen, im Gegenteil, ich werde alles durch Hinzufügen eines weiteren Monsters brechen: Sie können die hier


vorgenommenen Änderungen sehen .

In GitPod öffnen



Schritt 17: Sprites sortieren


Was war das problem Das Problem ist, dass ich eine willkürliche Reihenfolge beim Rendern von Sprites haben kann und für jeden von ihnen die Entfernung mit den Wänden vergleiche, nicht jedoch mit anderen Sprites, sodass die entfernte Kreatur über der nächsten herauskam. Ist es möglich, die Lösung mit der Tiefenkarte anzupassen, um die Sprites zu rendern?

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

Ich werde einen anderen Weg gehen und das Problem dumm in der Stirn lösen. Ich werde einfach alle Sprites vom weitesten zum nächsten ziehen. Das heißt, ich sortiere die Sprites in absteigender Reihenfolge der Entfernung und zeichne sie in dieser Reihenfolge.


Änderungen können Sie hier sehen .

In GitPod öffnen



Stufe 18: SDL-Zeit


Es ist Zeit für SDL. Plattformübergreifende Fensterbibliotheken sind sehr unterschiedlich 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 vorgenommenen Änderungen können hier angezeigt werden . Ich gebe keinen Link mehr zum Gitarren-Pod, weil SDL im Browser hat das Ausführen noch nicht gelernt :(

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

In GitPod öffnen

Schritt 19: Ereignisverarbeitung und Bereinigung


Hinzufügen einer Antwort auf Tastatureingaben ist nicht einmal lustig, werde ich nicht beschreiben. Beim Hinzufügen von SDL habe ich die Abhängigkeit von stb_image.h entfernt. Es ist schön, aber es dauert lange, bis es kompiliert ist.

Für diejenigen, die nicht verstehen, liegt hier der Quellcode der neunzehnten Stufe . So sieht eine typische Aufführung aus:


Fazit


Mein Code enthält derzeit 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 abgeleckt und absichtlich schmutzige Wäsche entsorgt. Ja, ich schreibe so (und ich bin nicht der einzige). An einem Samstagmorgen habe ich mich einfach hingesetzt und das geschrieben :)

Ich habe nicht begonnen, ein komplettes Spiel zu machen, meine Aufgabe ist es nur, einen ersten Impuls für den Flug deiner Fantasie zu geben. Schreiben Sie Ihren eigenen Code, er wird sicherlich besser sein als meiner. Teilen Sie Ihren Code, teilen Sie Ihre Ideen, senden Sie Pull-Anfragen.