NeHe - Lektion 21 - Linien, Timing, Orthogonale Sicht, Sound (Teil 1)

Lektion 21



Willkommen zu meinem 21ten OpenGL Tutorial! Ein Thema für dieses Tutorial zu finden, war extrem schwierig. Ich weiß, dass vielen von Ihnen es leid sind, die Grundlagen zu lernen. Jeder will was über 3D Objekte, Multitexturing und all die anderen interessante Dinge lernen. Für diese Leute tut es mir leid, aber ich möchte die Lernkurve langsam ansteigen lassen. Wenn ich erst einmal einen großen Schritt nach vorne mache, ist es nicht so leicht einen Schritt zurück zu machen, ohne das die Leute das Interesse verlieren. Deshalb bevorzuge ich es, es langsam und stetig angehen zu lassen.

Das nur für den Fall, dass ich ein paar von euch vergraule :) Ich werde jetzt etwas über dieses Tutorial erzählen. Bisher haben alle meine Tutorials Polygone, Quads und Dreiecke verwendet. Deshalb habe ich mich entschieden ein Tutorial über Linien zu schreiben. Ein paar Stunden nachdem ich mit dem Linien-Tutorial angefangen habe, habe ich mich entschieden es quits zu nennen. Das Tutorial verlief ganz gut, aber es war LANGWEILIG! Linien sind großartig, aber es gibt nicht soviel, was man interessantes damit machen kann. Ich habe meine Emails gelesen, das Message-Board durchforstet und einige Ihrer Tutorial-Anfragen aufgeschrieben. Bei diesen Anfragen kamen ein paar Fragen häufiger vor als andere. Deshalb... habe ich mich dazu entschieden ein Multi-Tutorial zu schreiben :)

In diesem Tutorial werden Sie etwas über folgendes lernen: Linien, Anti-Aliasing, orthographische Projektion, Timing, grundlegende Sound Effekte und einfache Spiele-Logik. Ich hoffe das es in diesem Tutorial genügend gibt, um jeden glücklich zu halten :) Ich habe 2 Tage damit verbracht, dieses Tutorial zu programmieren und es hat mich gut 2 Wochen gekostet, diese HTML Datei zu schreiben. Ich hoffe ihr geniesst meine Mühen!

Am Ende dieses Tutorials werden Sie ein einfaches 'Amidar' ähnliches Spiel haben. Ihre Aufgabe ist es, das Gitter zu füllen, ohne von den bösen Jungs erwischt zu werden. Das Spiel hat Level, Stages, Leben, Sound und ein geheimes Item, um Ihnen durch das Level zu helfen, wenn es mal schwierig wird. Obwohl dieses Spiel auf einem Pentium 166 mit einer Voodoo 2 gut läuft, wird ein schnellerer Prozessor empfohlen, wenn Sie weichere Animationen haben wollen.

Ich habe den Code aus Lektion 1 als Startpunkt verwendet, während ich dieses Tutorial geschrieben habe. Wir fangen damit an, die benötigten Header-Dateien zu inkludieren. stdio.h wird für Datei-Operationen verwendet und wir inkludieren stdarg.h, so dass wir Variablen wie die Punktezahl und die aktuelle Stage auf dem Screen anzeigen können.

// Dieser Code wurde von Jeff Molofee erzeugt, 2000
// Wenn Sie diesen Code hilfreich finden, lassen Sie es mich bitte wissen.

#include    <windows.h>                            // Header Datei für Windows
#include    <stdio.h>                            // Standard Input / Output
#include    <stdarg.h>                            // Header Datei für die Variablen Argumente Routinen
#include    <gl\gl.h>                            // Header Datei für die OpenGL32 Library
#include    <gl\glu.h>                            // Header Datei für die GLu32 Library
#include    <gl\glaux.h>                            // Header Datei für die Glaux Library

HDC        hDC=NULL;                            // Privater GDI Device Context
HGLRC        hRC=NULL;                            // Permanenter Rendering Context
HWND        hWnd=NULL;                            // enthält unser Fenster Handle
HINSTANCE    hInstance;                            // enthält die Instanz der Applikation

Nun initialisieren wir die booleschen Variablen. vline verfolgt den Status der 121 vertikalen Linien, aus denen das Spiel-Gitter besteht. 11 Linien längs und 11 hoch und runter. hline verfolgt den Status der 121 horizontalen Linien, aus denen das Spiel-Gitter besteht. Wir benutzen ap um zu verfolgen, ob die Taste 'A' gedrückt wurde oder nicht.

filled ist FALSE wenn das Gitter nicht gefüllt ist und gleich TRUE wenn es gefüllt ist. gameover ist ziemlich selbsterklärend. Wenn gameove gleich TRUE ist, ist das Spiel vorbei, ansonsten sind Sie immer noch mittem im Spiel. anti verfolgt den Status des Antialiasing. Wenn anti gleich TRUE ist, ist Objekt-Antialiasing eingeschaltet. Ansonsten ist es ausgeschaltet. active und fullscreen verfolgen, ob das Programm minimiert wurde oder nicht und ob das Programm im Fullscreen-Modus oder im Fenster-Modus läuft.

bool        keys[256];                            // Array das für die Tastatus Routine verwendet wird
bool        vline[11][10];                            // verfolgt den Status der vertikalen Linien
bool        hline[10][11];                            // verfolgt den Status der horizontalen Linien
bool        ap;                                // Taste 'A' gedrückt?
bool        filled;                                // fertig mit dem Füllen des Gitters?
bool        gameover;                            // Ist das Spiel vorbei?
bool        anti=TRUE;                            // Antialiasing?
bool        active=TRUE;                            // Fenster ist aktiv Flag ist standardmäßig gleich TRUE
bool        fullscreen=TRUE;                        // Fullscreen Flag ist standardmäßig auf Fullscreen Modus gesetzt

Nun initialisieren wir unsere Integer-Variablen. loop1 und loop2 werden dazu verwendet, um Punkte in unserem Gitter zu überprüfen, ob ein Feind uns getroffen hat und um Objekte zufällige neue Positionen auf dem Gitter zuzuweisen. Sie werden loop1 / loop2 später im Programm in Aktion erleben. delay ist eine Zähler-Variable, die ich dazu verwende, um die bösen Jungs etwas zu verlangsamen. Wenn delay größer als ein bestimmter Wert ist, werden die Feinde bewegt und delay zurück auf 0 gesetzt.

Die Variable adjust ist eine sehr spezielle Variable! Obwohl dieses Programm einen Timer hat, überprüft der Timer nur, ob ihr Computer zu schnell ist. Wenn er es ist, wird eine Verzögerung erzeugt, um den Computer zu verlangsamen. Auf meiner GeForce-Karte lief das Programm unglaublich weich und sehr sehr schnell. Nachdem ich das Programm auf meinem PIII/450 mit einer Voodoo 3500TV getestet habe, habe ich festgestellt, dass es extrem langsam lief. Das Problem ist, dass mein Timing-Code lediglich das Gameplay verlangsamt. Aber nichts verschnellert. Deshalb habe ich eine neue Variable namens adjust eingefügt. adjust kann einen Wert zwischen 0 und 5 annehmen. Die Objekte in dem Spiel bewegen sich mit unterschiedlicher Geschwindigkeit, abhängig von dem Wert von adjust. Je kleiner der Wert ist, umso weicher bewegen sie sich, je höher der Wert, umso schneller bewegen sie sich (holprig, ab einem Wert größer als 3). Das war der einzige wirklich einfache Weg, das Spiel auch auf langsamen Systemen spielbar zu machen. Noch eine Anmerkung, egal wie schnell sich die Objekte bewegen, die Spielgeschwindigkeit wird niemals schneller laufen, wie ich es wünsche. Deshalb ist man auf der sicheren Seite, wenn man adjust einen Wert von 3 zuweist, sowohl für schnelle als auch langsame Systeme.

Die Variable lives wird auf 5 gesetzt, so dass Sie bei Beginn des Spieles 5 Leben haben. level ist eine interne Variable. Das Spiel verwendet die Variable, um das Level der Schwierigkeit zu verfolgen. Dies ist nicht das Level, dass Sie auf dem Screen sehen können. Die Variable level2 fängt mit dem selben Wert wie level an, kann aber unendlich weit inkrementiert werden, abhängig von Ihren Fähigkeiten. Wenn Sie es schaffen sollten, über Level 3 hinaus zu kommen, die Variable level wird bei 3 aufhören zu inkrementieren. Die level Variable ist eine interne Variable, die für die Spielschwierigkeit verwendet wird. Die stage Variable verfolgt die aktuelle Spiel-Ebene.

int        loop1;                                // Generische Loop1
int        loop2;                                // Generische Loop2
int        delay;                                // Feind-Verzögerung
int        adjust=3;                            // Geschwindigkeits Anpassung für wirklich langsame Grafikkarten
int        lives=5;                            // Spieler-Leben
int        level=1;                            // internes Level
int        level2=level;                            // angezeigtes Level
int        stage=1;                            // Spiel-Ebene

Nun erzeugen wir eine Struktur die den Status der Objekte in unserem Spiel verfolgt. Wir haben eine feine X-Position (fx) und eine feine Y-Position (fy). Diese Variablen lassen den Spieler und die Feinde auf dem Gitter um ein paar Pixel jeweils bewegen. Das erzeugt eine weiche Bewegung der Objekte.

Dann haben wir x und y. Diese Variablen verfolgen, in welchem Abschnitt unser Spieler ist. Es gibt 11 Punkte von links nach rechts und 11 Punkte hoch und runter. Deshalb können x und y jegliche Werte zwischen 0 und 10 sein. Deshalb benötigen wir auch die feinen Werte. Wenn wir uns nur auf einen von 11 Punkten von links nach rechts und einen von 11 Punkten hoch und runter bewegen könnten, würde unser Spiel auf dem Screen in einer schnellen (nicht weichen) Bewegung hin und her springen.

Die letzte Variable spin wird dazu verwendet, die Objekte auf ihrer Z-Achse zu rotieren.

struct        object                                // erzeuge eine Struktur für unseren Spieler
{
    int    fx, fy;                                // feine Bewegungs-Position
    int    x, y;                                // aktuelle Spieler Position
    float    spin;                                // Rotations-Richtung
};

Nun da wir eine Struktur erzeugt haben, die sowohl für den Spieler, als auch Feinde und sogar einem Spezial-Item verwendet werden kann, können wir Strukturen erzeugen, die die Charakteristika der Struktur annehmen, die wir gerade gemacht haben.

Die erste folgende Zeile erzeugt eine Struktur für unseren Spieler. Wir geben unserer Spieler-Struktur Werte für fx, fy, x, y und spin. Indem wir diese Zeile einfügen, können wir auf die X-Position des Spielers mittels player.x zugreifen. Wir können die Spieler-Rotation verändern, indem wir eine Zahl zu player.spin addieren.

Die zweite Zeile ist etwas anders. Da wir bis zu 9 Feinde auf dem Screen auf einmal haben können, müssen wir für jeden Feind eine Variable haben. Wir machen das, indem wir ein Array von 9 Feinden anlegen. Die X-Position des ersten Feindes wird bei enemy[0].x sein. Der zweite Feind bei enemy[1].x, etc.

Die letzte Zeile erzeugt eine Struktur für unser Spezial-Item. Das Spezial-Item ist eine Sanduhr, die von Zeit zur Zeit auf dem Screen erscheint. Wir müssen die X und Y-Werte der Sanduhr verfolgen, da sich die Sanduhr aber nicht bewegen wird, brauchen wir die feinen Positionen nicht. Statt dessen werden wir die feinen Variablen (fx und fy) für andere Dinge später im Programm verwenden.

struct    object    player;                                // Spieler Informationen
struct    object    enemy[9];                            // Feind Informationen
struct    object    hourglass;                            // Sanduhr Informationen

Nun erzeugen wir eine Timer-Struktur. Wir erzeugen eine Struktur, damit es leichter die Timer-Variablen anzusprechen und damit sie sich leichter als Timer-Variablen zuordnen lassen.

Als erstes erzeugen wir ein 64 Bit Integer namens frequency. Diese Variable wird die Frequenz des Timers enthalten. Als ich dieses Programm zum ersten Mal schrieb, habe ich vergessen, diese Variable einzufügen. Ich habe nicht bemerkt, dass die Frequenz auf einer Maschine nicht unbedingt mit der Frequenz auf einer anderen Maschine übereinstimme muss. Großer Fehler meinerseits! Der Code lief wunderbar auf 3 Systemen in meinem Haus, aber als ich ihn auf der Maschine eines Freundes getestet habe, lief das Spiel VIEL zu schnell. Frequency ist grundsätzlich der Wert, wie häufig die (interne) Uhr aktualisiert wird. Etwas, auf das man achten sollte :)

Die resolution Variable enthält die Anzahl der Schritte, die gemacht werden müssen, bevor wir 1 Millisekunde an Zeit erhalten.

mm_timer_start und mm_timer_elapsed enthalten den Wert, wann der Timer gestartet wurde. Diese beiden Variablen werden nur verwendet, wenn der Computer keinen Performance Counter hat. In diesem Fall würden wie den weniger genauen Multimedia Timer verwenden, welcher immer noch gut genug für ein nicht-zeitkritisches Spiel wie dieses ist.

Die Variable performance_timer kann entweder TRUE oder FALSE sein. Wenn das Programm einen Performance Counter entdeckt, wird die Variable performance_timer auf TRUE gesetzt und das gesamte Timing wird über den Performance Counter abgewickelt (wesentlich genauer als der Multimedia Timer). Wenn kein Performance Counter gefunden wird, wird performance_timer auf FALSE gesetzt und der Multimedia Timer wird für das Timing verwendet.

Die letzten 2 Variablen sind 64 Bit Integer Variablen, die die Startzeit der Performance Counters enthalten und die Menge der Zeit die verstrichen ist, seitdem der Performance Counter gestartet wurde.

Der Name der Struktur ist "timer" wie Sie am Ende der Struktur sehen können. Wenn wir die Timer-Frequenz wissen wollen, können wir einfach timer.frequency überprüfen. Nett!

struct                                         // erzeuge eine Struktur für die Timer Informationen
{
  __int64       frequency;                            // Timer Frequenz
  float         resolution;                            // Timer Auflösung
  unsigned long mm_timer_start;                            // Multimedia Timer Start Wert
  unsigned long mm_timer_elapsed;                        // Multimedia Timer verstrichene Zeit
  bool        performance_timer;                        // wird der Performance Timer benutzt?
  __int64       performance_timer_start;                    // Performance Timer Start Wert
  __int64       performance_timer_elapsed;                    // Performance Timer verstrichene Zeit
} timer;                                    // Struktur wird timer genannt

Die nächste Codezeile ist unsere Geschwindigkeitstabelle. Die Objekte in dem Spiel werden sich mit verschiedenen Geschwindigkeiten, abhängig vom adjust-Wert, bewegen. Wenn adjust gleich 0 ist, werden sich die Objekte einen Pixel zur Zeit bewegen. Wenn der Wert von adjust 5 ist, werden sich die Objekte 20 Pixel zur Zeit bewegen. Indem der Wert von adjust also inkrementiert wird, wird auch die Geschwindigkeit der Objekte inkrementiert, was das Spiel auf langsamen Systemen schneller laufen lässt. Je höher adjust allerdings ist, umso holpriger lässt sich das Spiel spielen.

steps[ ] ist eigentlich nur eine Look-Up Tabelle. Wenn adjust 3 wäre, würden wir uns die Zahl die an Position 3 in steps[ ] gespeichert ist, anschauen. Position 0 enthält den Wert 1, Position 1 enthält den Wert 2, Position 2 enthält den Wert 4 und Position 3 enthält den Wert 5. Wenn adjust 3 wäre, würden sich unsere Objekte um 5 Pixel zur Zeit bewegen. Verstanden?

int        steps[6]={ 1, 2, 4, 5, 10, 20 };                // Schritt-Werte für langsame Grafik Adjustierung

Als nächstes schaffen wir Platz für zwei Texturen. Wir laden eine Hintergrund-Szene und eine Bitmap-Font-Textur. Dann initialisieren wir eine Basis-Variable, so dass wir unsere Font-Display-Liste genauso wie in den anderen Tutorials managen können. Zu guter Letzt deklarieren wir WndProc().

GLuint        texture[2];                            // Font Textur Speicherplatz
GLuint        base;                                // Basis Display Liste für den Font

LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);                // Deklaration von WndProc

Nun zum spassigen Teil :) Der nächste Codeabschnitt initialisiert unseren Timer. Er überprüft, ob der Computer einen Performance Counter hat (einen sehr genauen Counter). Wenn wir keinen Performance Counter haben, wird der Computer den Multimedia Timer verwenden. Dieser Code sollte portabel sein, soweit man mir sagen konnte.

Wir fangen damit an, die Timer-Variablen auf null zu löschen. Damit setzen wir alle Variablen unserer Timer-Struktur auf null. Danach überprüfen wir, ob es KEINEN Performance Counter gibt. Das ! bedeutet NICHT. Wenn es einen gibt, wird die Frequenz in timer.frequency gespeichert.

Wenn es keinen Performance Counter gibt, wird der Code zwischen den { }'s durchlaufen. Die erste Zeile setzt die Variable timer.performance_timer auf FALSE. Damit teilen wir unserem Programm mit, dass es keinen Performance Counter gibt. Die zweite Zeile ermittelt unseren Start Wert des Multimedia Timers mittels timeGetTime(). Wir setzen timer.resolution auf 0.001f und timer.frequency auf 1000. Da bisher noch keine Zeit verstrichen ist, setzen wir die verstrichene Zeit gleich der Start-Zeit.

void TimerInit(void)                                // Initialisiere unseren Timer (bereite ihn vor)
{
    memset(&timer, 0, sizeof(timer));                    // lösche unsere Timer Struktur

    // überprüfe, ob ein Performance Counter verfügbar ist
    // wenn einer verfügbar ist, wird die Timer Frequenz aktualisiert
    if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))
    {
        // Kein Performace Counter verfügbar
        timer.performance_timer    = FALSE;                // Setze Performance Timer auf FALSE
        timer.mm_timer_start    = timeGetTime();            // benutze timeGetTime() um die aktuelle Zeit zu ermitteln
        timer.resolution    = 1.0f/1000.0f;                // Setze unsere Timer Auflösung auf .001f
        timer.frequency        = 1000;                    // Setze unsere Timer Frequenz auf 1000
        timer.mm_timer_elapsed    = timer.mm_timer_start;            // Setze die verstrichene Zeit gleich der aktuellen Zeit
    }

Wenn es einen Performance Counter gibt, wird statt dessen der folgende Code durchlaufen. Die erste Zeile ermittelt den aktuellen Startwert des Performance Counters und speichert ihn in timer.performace_timer_start. Dann setzen wir timer.performance_timer auf TRUE, so dass unser Programm weiss, dass es einen Performance Counter gibt. Danach berechnen wir die Timer Auflösung mittels der Frequenz, die wir erhalten haben, als wir im obigen Code überprüft haben, ob es einen Performance Counter gibt. Wir dividieren 1 durch die Frequenz, um die Auflösung zu erhalten. Als letztes setzen wir die vertsrichene Zeit gleich der Anfangszeit.

Beachten Sie, dass ich mich dafür entschieden habe, seperate Variable für die Start- und verstrichene Zeit des Performance und Multimedia Timers zu verwenden, anstatt die selben Variablen zu verwenden. Beides würde allerdings funktionieren.

    else
    {
        // Performance Counter ist verfügbar, benutze diesen statt des Multimedia Timers
        // ermittle die aktuelle Zeit und speichere sie in performance_timer_start
        QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);
        timer.performance_timer        = TRUE;                // Setze Performance Timer auf TRUE
        // berechne die Timer Auflösung mittels der Timer Frequenz
        timer.resolution        = (float) (((double)1.0f)/((double)timer.frequency));
        // Setze die vergangene Zeit gleich der aktuellen Zeit
        timer.performance_timer_elapsed    = timer.performance_timer_start;
    }
}

Der obige Codeabschnitt intialisiert den Timer. Der folgende Code liest den Timer aus und liefert die verstrichene Zeit in Millisekunden zurück.

Als erstes müssen wir eine 64 Bit Variable namens time erzeugen. Wir werden diese variable dazu verwenden, um den aktuellen counter-Wert zu ermitteln. Die nächste Zeile überprüft, ob wir einen Performance Counter haben. Wenn dem so ist, wird timer.performance_timer gleich TRUE sein und der folgende Code wird durchlaufen.

Die erste Codezeile innerhalb der { } ermittelt den Counter Wert und speichert ihn in der Variable time. Die zweite Zeile nimmt die Zeit, die wir gerade erst ermittelt haben (time) und subtrahiert die Anfangszeit, die wir ermittelt haben, als wir den Timer initialisiert haben. Auf diese Weise sollte unsere Timer ziemlich nahe bei 0 anfangen. Wir multiplizieren dann das Ergebnis mit der Auflösung, um herauszufinden, wieviele Sekunden vergangen sind. Als letztes multiplizieren wir das Ergebniss mit 1000 um herauszufinden, wieviele Millisekunden verstrichen sind. Nachdem die Berechnung beendet wurde, wird unser Ergebniss an den Codeabschnitt zurückgesendet, der diese Prozedur aufgerufen hat. Das Ergebniss wird im Floating-Point Format sein, damit die Genauigkeit höher ist.

Wenn wir nicht den Performance Counter verwenden, wird der Code nach dem Else-Statement ausgeführt. Er macht ziemlich genau das Selbe. Wir ermitteln die aktuelle Zeit mit timeGetTime() und subtrahieren unseren Counter Start-Wert. Wir multiplizieren mit der Auflösung und mutliplizieren das Ergebniss nochmal mit 1000 um Sekunden in Millisekunden zu konvertieren.

float TimerGetTime()                                // ermittle die Zeit in Millisekunden
{
    __int64 time;                                // time wird ein 64 Bit Integer enthalten

    if (timer.performance_timer)                        // verwenden wir den Performance Timer?
    {
        QueryPerformanceCounter((LARGE_INTEGER *) &time);        // ermittle die aktuelle Performance Zeit
        // gebe die aktuelle Zeit minus die Startzeit multipliziert mit der Auflösung und mal 1000 (wegen der ms)
        return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;
    }
    else
    {
        // gebe die aktuelle Zeit minus die Startzeit multipliziert mit der Auflösung und mal 1000 (wegen der ms)
        return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;
    }
}

Der folgende Codeabschnitt resettet den Spieler in die obere linke Ecke des Screens und gibt den Feinden einen zufälligen Startpunkt.

Die oberen linke Ecke des Screens ist 0 auf der X-Achse und 0 auf der Y-Achse. Indem der player.x Wert auf 0 gesetzt wird, bewegen wir den Spieler ganz nach links auf dem Screen. Indem der player.y Wert auf 0 gesetzt wird, bewegen wir den Spieler ganz nach oben auf dem Screen.

Die feinen Positionen müssen gleich der aktuellen Spieler Position sein, ansonsten würde unser Spieler sich von der feinen Position (wo immer das auch sein mag), in die obere linke Ecke bewegen. Wir wollen aber nicht, dass sich der Spieler dahin bewegt, wir wollen dass er dort erscheint, weshalb wir die feine Position ebenfalls auf 0 setzen.

void ResetObjects(void)                                // Resette Spieler und Feinde
{
    player.x=0;                                // Resette Spieler X Position auf ganz links des Screens
    player.y=0;                                // Resette Spieler Y Position auf ganz oben des Screens
    player.fx=0;                                // Setze feine X Position 
    player.fy=0;                                // Setze feine Y Position

Als nächstes geben wir den Feinden eine zufällige Startposition. Die Anzahl der angezeigten Feinde auf dem Screen, ist gleich dem aktuellen (internen) Level Wert multipliziert mit der aktuellen Ebene (stage). Denken Sie daran, der maximale Wert, den level annehmen kann, ist 3 und die maximal Anzahl der Ebenen pro Level ist 3. Deshalb können wir bis zu 9 Feinde gleichzeitig haben.

Um sicher zu gehen, dass wir allen sichtbaren Feinden eine neue Position geben, iterieren wir durch alle sichtbaren Feinde (Ebene mal Level). Wir setzen jede Feind X-Position auf 5 plus einem zufälligen Wert zwischen 0 und 5. (der maximale Wert von rand ist immer der Wert den Sie spezifizieren minus 1). Deshalb kann der Feind irgendwo auf dem Gitter zwischen 5 und 10 erscheinen. Wir geben dem Feind dann einen zufälligen Wert zwischen 0 und 10 für die Y-Achse.

Wir wollen nicht, dass der Feind sich von der alten Position zur neuen zufälligen Position hin bewegt, weshalb wir sicherstellen, dass das feine x (fx) und y (fy) gleich dem eigentlichen x und y Wert multipliziert mit der Breite und Höhe von jedem Tile des Screens ist. Jeder Tile hat eine Breite von 60 und eine Höhe von 40.

    for (loop1=0; loop1// iteriere durch alle Feinde
    {
        enemy[loop1].x=5+rand()%6;                    // wähle eine zufällige X Position
        enemy[loop1].y=rand()%11;                    // wähle eine zufällige Y Position
        enemy[loop1].fx=enemy[loop1].x*60;                // Setze das feine X
        enemy[loop1].fy=enemy[loop1].y*40;                // Setze das feine Y
    }
}

Der AUX_RGBImageRec Code hat sich nicht geändert, weshalb ich ihn überspringe. In LoadGLTextures() werden wir unsere zwei Texturen laden. Als erstes das Font Bitmap (Font.bmp) und dann das Hintergrundbild (Image.bmp). Wir konvertieren beiden Bilder in Texturen, die wir in unserem Spiel verwenden können. Nachdem wir die Texturen erzeugt haben, räumen wir auf, indem wir die Bitmap Informationen löschen. Nichts wirklich Neues. Wenn Sie die anderen Tutorials gelesen haben, sollten Sie keine Probleme haben, den Code zu verstehen.

int LoadGLTextures()                                // Lade Bitmaps und konvertiere sie in Texturen
{
    int Status=FALSE;                            // Status Indikator
    AUX_RGBImageRec *TextureImage[2];                    // erzeuge Speicherplatz für die Texturen
    memset(TextureImage,0,sizeof(void *)*2);                // Setze den Zeiger aufNULL

    if     ((TextureImage[0]=LoadBMP("Data/Font.bmp")) &&            // Lade den Font
         (TextureImage[1]=LoadBMP("Data/Image.bmp")))            // Lade Hintergrundbild
    {
        Status=TRUE;                            // Setze den Status auf TRUE

        glGenTextures(2, &texture[0]);                    // erzeuge die Textur

        for (loop1=0; loop1// iteriere durch  2 Texturen
        {
            glBindTexture(GL_TEXTURE_2D, texture[loop1]);
            glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX, TextureImage[loop1]->sizeY,
                0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);
            glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
        }

        for (loop1=0; loop1// iteriere durch 2 Texturen
        {
            if (TextureImage[loop1])                // wenn Textur existiert
            {
                if (TextureImage[loop1]->data)            // wenn Textur Bild existiert
                {
                    free(TextureImage[loop1]->data);    // gebe den Textur Bild Speicher frei
                }
                free(TextureImage[loop1]);            // gebe die Bildstruktur frei
            }
        }
    }
    return Status;                                // gebe Status zurück
}

Der folgende Code erzeugt unsere Font-Display-Liste. Ich habe bereits ein Tutorial über Bitmap Textur Fonts gemacht. Alles was der Code macht, ist, er dividiert das Bild in Font.bmp in 16 x 16 Kästchen (256 Zeichen). Jedes 16x16 Kästchen wird ein Zeichen sein. Da ich die Y-Achse nach oben gesetzt habe, so dass das Positive nach unten geht, anstatt nach oben, ist es nötig, unsere Y-Achsen-Wert von 1.0f zu subtrahieren. Ansonsten wären alle Buchstaben auf dem Kopf :) Wenn Sie nicht verstehen, was hier passiert, gehen Sie zurück und lesen Sie das 'Texturierte Fonts' Tutorial.

GLvoid BuildFont(GLvoid)                            // erzeuge unsere Font Display Liste
{
    base=glGenLists(256);                            // erzeuge 256 Display Listen
    glBindTexture(GL_TEXTURE_2D, texture[0]);                // wähle unsere Font Textur aus
    for (loop1=0; loop1// iteriere durch alle 256 Listen
    {
        float cx=float(loop1%16)/16.0f;                    // X Position des aktuellen Zeichens
        float cy=float(loop1/16)/16.0f;                    // Y Position des aktuellen Zeichens

        glNewList(base+loop1,GL_COMPILE);                // fange an eine Liste zu erzeugen
            glBegin(GL_QUADS);                    // benutzen einen Quad für jedes Zeichen
                glTexCoord2f(cx,1.0f-cy-0.0625f);        // Textur Coord (unten links)
                glVertex2d(0,16);                // Vertex Coord (unten links)
                glTexCoord2f(cx+0.0625f,1.0f-cy-0.0625f);    // Textur Coord (unten rechts)
                glVertex2i(16,16);                // Vertex Coord (unten rechts)
                glTexCoord2f(cx+0.0625f,1.0f-cy);        // Textur Coord (oben rechts)
                glVertex2i(16,0);                // Vertex Coord (oben rechts)
                glTexCoord2f(cx,1.0f-cy);            // Textur Coord (oben links)
                glVertex2i(0,0);                // Vertex Coord (oben links)
            glEnd();                        // fertig mit der Erzeugung unseres Quads (Zeichens)
            glTranslated(15,0,0);                    // gehe nach rechts vom Zeichen
        glEndList();                            // fertig mit dem Erzeugen der Display Liste
    }                                    // iteriere bis alle 256 erzeugt wurden
}

Es ist eine gute Idee, die Font Display Liste wieder zu löschen, wenn Sie sie nicht mehr benötigen, deshalb habe ich folgenden Codeabschnitt eingefügt. Erneut, nichts neues.

GLvoid KillFont(GLvoid)                                // Lösche den Font aus dem Speicher
{
    glDeleteLists(base,256);                        // Lösche alle 256 Display Listen
}

Der glPrint() Code hat sich nicht sonderlich viel geändert. Der einzige Unterschied zu dem 'Texturierte Fonts' Tutorial ist, dass ich die Möglichkeit eingefügt habe, den Wert von Variablen auszugeben. Der einzige Grund, warum ich diesen Codeabschnitt geschrieben habe, ist, dass Sie die Änderungen sehen können. Das Print Statement positioniert den Text an der spezifizierten X und Y Koordinate. Sie können zwischen 2 Zeichensätzen wählen und der Wert der Variablen wird auf dem Screen ausgegeben. Damit ist es uns erlaubt, das aktuelle Level und Ebene auf dem Screen auszugeben.

Beachten Sie, dass ich Texturmapping aktiviert habe, die View resette und dann zur korrekte X / Y Position translatiere. Beachten Sie auch, dass, wenn der Zeichensatz 0 ausgewählt ist, der Font 1,5 Mal in der Breite vergrößert wird und in seiner Höhe verdoppelt wird. Ich habe das gemacht, damit ich den Titel des Spiels in großen Lettern ausgeben kann. Nachdem der Text gezeichnet wurde, deaktiviere ich Texturmapping.

GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, ...)            // wo die Ausgabe geschieht
{
    char        text[256];                        // enthält unseren String
    va_list        ap;                            // Zeiger auf die Liste der Argumente

    if (fmt == NULL)                            // Wenn es keinen Text gibt
        return;                                // mache nichts

    va_start(ap, fmt);                            // Parse den String nach Variablen
        vsprintf(text, fmt, ap);                        // und konvertiere Symbole in die eigentlichen Zahlen
    va_end(ap);                                // Ergebnisse werden imText gespeichert

    if (set>1)                                // hat der Benutzer einen ungültigen Zeichensatz gewählt?
    {
        set=1;                                // wenn ja, wähle Satz 1 (kursiv)
    }
    glEnable(GL_TEXTURE_2D);                        // aktiviere Textur Mapping
    glLoadIdentity();                            // Resette die Modelview Matrix
    glTranslated(x,y,0);                            // Positioniere den Text (0,0 - unten links)
    glListBase(base-32+(128*set));                        // wähle den Zeichensatz (0 oder 1)

    if (set==0)                                // wenn Satz 0 verwendet wird, vergrößere den Font
    {
        glScalef(1.5f,2.0f,1.0f);                    // vergrößere Font Breite und Höhe
    }

    glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);            // gebe den Text auf dem Screen aus
    glDisable(GL_TEXTURE_2D);                        // deaktiviere Textur Mapping
}

Der Resize Code ist NEU :) Anstatt eine perspektivische Sicht zu verwenden, verwende ich eine orthogonale Sicht für dieses Tutorial. Das bedeutet, dass Objekte nicht kleiner werden, wenn sie sich vom Betrachter entfernen. Die Z-Achse ist ziemlich nutzlos in diesem Tutorial.

Wir fangen damit an, den Viewport zu setzen. Wir machen das genauso wie wenn wir unsere perspektivische Sicht initialisieren würden. Wir setzen den Viewport gleich der Breite unseres Fensters.

Dann wählen wir die Projektionsmatrix aus (denken Sie an einen Filmprojektor, er enthält die Informationen wie unser Bild angezeigt wird) und resetten sie.

Direkt nachdem wir die Projektionsmatrix resettet haben, setzen wir unsere orthogonale Sicht. Ich erkläre den Befehl etwas mehr im Detail:

Der erste Parameter (0.0f) ist der Wert für die äußerste linke Seite des Screens. Sie wollten wissen, wie man Pixel-Werte verwenden kann, anstatt eine negative Zahl für die linke Seite zu verwenden. Ich habe den Wert auf 0 gesetzt. Der zweite Parameter ist der Wert für die rechte Seite des Screens. Wenn unser Fenster 640x480 ist, ist der Wert, der in width gespeichert ist, gleich 640. Deshalb wird die rechte Seite des Screens gleich 640 sein. Deshalb verläuft unser Screen auch von 0 bis 639 auf der X-Achse (640 Pixel).

Der dritte Parameter (height) wäre normalerweise unser negativer Y-Achsen Wert (untere Seite des Screens). Da wir aber exakte Pixel haben wollen, werden wir keinen negativen Wert haben. Statt dessen setzen wir die untere Kante gleich der Höhe unseres Fensters. Wenn unser Fenster 640x480 ist, ist die Höhe (height) gleich 480. So dass die untere Kante des Screens gleich 479 wäre. Der vierte Parameter wäre normalerweise ein positiver Wert für die obere Kante unseres Screens. Wir wollen, dass die obere Kante gleich 0 ist (die guten old-fashioned Screen Koordinaten), so dass wir den vierten Parameter auf 0 setzen. Damit haben wir die Werte 0 bis 479 auf der Y-Achse (480 Pixel).

Die letzten beiden Parameter sind für die Z-Achse. Da uns die Z-Achse nicht wirklich interessiert, geben wir die Werte -1.0f und 1.0f an. Das langt, um die Dinge zu sehen, die bei 0.0f auf der Z-Achse gezeichnet werden.

Nachdem wir die orthogonale Sicht gesetzt haben, wählen wir die Modelview Matrix aus (Objekt Informationen... Position, etc.) und resetten diese.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)                // Resize und initialsiere das GL Fenster
{
    if (height==0)                                // verhindere eine Division durch 0
    {
        height=1;                            // indem die Höhe auf 1 gesetzt wird
    }

    glViewport(0,0,width,height);                        // Resette den aktuellenViewport

    glMatrixMode(GL_PROJECTION);                        // wähle die Projektions Matrix aus
    glLoadIdentity();                            // Resette die Projektions Matrix

    glOrtho(0.0f,width,height,0.0f,-1.0f,1.0f);                // erzeuge Ortho 640x480 Sicht (0,0 oben links)

    glMatrixMode(GL_MODELVIEW);                        // wähle die Modelview Matrix aus
    glLoadIdentity();                            // Resette die Modelview Matrix
}

Der Init Code enthält ein paar neue Befehle. Wir fangen mit dem Laden unserer Texturen an. Wenn diese nicht korrekt geladen werden, beendet sich das Programm mit einer Fehlermeldung. Danach erzeugen wir die Texturen, wir erzeugen unserer Font-Satz. Ich habe mich nicht um die Fehlerbehandlung gekümmert, aber Sie können das machen, wenn Sie wollen.

Nachdem der Font erzeugt wurde, intialisieren wir noch ein paar Dinge. Wir aktivieren Smooth Shading, setzen unsere Löschfarbe auf Schwarz und setzen Depth Clearing auf 1.0f. Danach kommt eine neue Codezeile.

glHint() teilt OpenGL mit, wie etwas gezeichnet werden soll. In diesem Fall teilen wir OpenGL mit, dass wir weiche Linien haben wollen, so dass OpenGL das beste (netteste) Ergebniss liefern soll, das möglich ist. Dieser Befehl aktiviert Anti-Aliasing.

Als letztes aktivieren wir Blending und wählen den Blend-Modus aus, welcher Anti-Aliased Linien möglich macht. Blending wird benötigt, wenn Sie wollen, dass die Linien gut mit dem Hintergrundbild 'geblendet' wird. Deaktivieren Sie Blending um mal zu sehen, wie grausam es ohne aussieht. Es ist wichtig anzumerken, dass es so aussehen kann, als ob Anti-Aliasing nicht korrekt funktionert. Die Objekte in diesem Spiel sind relativ klein, weshalb Sie das Antialiasing vielleicht nicht sofort bemerken. Schauen Sie genau hin. Beachten Sie, wie die 'Treppchen' bei den Feinden ausgeglättet werden, wenn Antialiasing an ist. Der Spieler und die Sanduhr sollten ebenfalls besser aussehen.

int InitGL(GLvoid)                                // das gesamte Setup für OpenGL kommt hier hin
{
    if (!LoadGLTextures())                            // springe zur Textur Lade Routine
    {
        return FALSE;                            // wenn Texturen nicht geladen wurden, gebe FALSE zurück
    }

    BuildFont();                                // erzeuge den Font

    glShadeModel(GL_SMOOTH);                        // aktiviere Smooth Shading
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);                    // schwarzer Hintergrund
    glClearDepth(1.0f);                            // Depth Buffer Setup
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);                    // Setze Linien Antialiasing
    glEnable(GL_BLEND);                            // aktiviere Blending
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);            // Art des zu verwendenden Blending
    return TRUE;                                // Initialisierung verlief OK
}

Nun zum Zeichnen-Code. Hier passiert der gesamte Zauber :)

Wir löschen den Screen (auf schwarz) zusammen mit dem Depth Buffer. Dann wählen wir die Font Textur (texture[0]) aus. Wir wollen die Wörter "GRID CRAZY" in purpurner Farbe haben, weshalb wir Rot und Blau auf volle Intensität setzen und das Grün halb aufdrehen. Nachdem wir die Farbe gewählt haben, rufen wir glPrint() auf. Wir positionieren die Wörter "GRID CRAZY" bei 207 auf der X-Achse (dem Zentrum unseres Screens) und 24 auf der Y-Achse (hoch und runter). Wir benutzen unseren großen Font, indem wir den Font-Satz 0 verwenden.

Nachdem wir "GRID CRAZY" auf den Screen gezeichnet haben, ändern wir die Farbe auf gelb (volles rot, volles grün). Wir schreiben "Level:" und die Variable level2 auf den Screen. Erinnern Sie sich daran, dass level2 größer als 3 sein kann. level2 enthält den Level-Wert, den der Spieler auf dem Screen sieht. %2i bedeutet, dass wir nicht mehr als 2 Ziffern auf dem Screen haben wollen, die das Level repräsentieren. Das i bedeutet, dass die Zahl eine Integer-Zahl ist.

Nachdem wir die Level-Informationen auf den Screen gebracht haben, geben wir die Stage-Informationen direkt dadrunter in der selben Farbe aus.

int DrawGLScene(GLvoid)                                // Hier kommt der gesamte Zeichnen-Kram hin
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);            // lösche Screen und Depth Buffer
    glBindTexture(GL_TEXTURE_2D, texture[0]);                // wähle unsere Font Textur aus
    glColor3f(1.0f,0.5f,1.0f);                        // Setze Farbe auf purpur
    glPrint(207,24,0,"GRID CRAZY");                        // schreibe GRID CRAZY auf den Screen
    glColor3f(1.0f,1.0f,0.0f);                        // Setze Farbe auf gelb
    glPrint(20,20,1,"Level:%2i",level2);                    // gebe aktuelle Level Statistik aus
    glPrint(20,40,1,"Stage:%2i",stage);                    // gebe Ebenen-Statistik aus

Nun überprüfen wir, ob das Spiel vorbei ist. Wenn das Spiel vorbei ist, ist die Variable gameover gleich TRUE. Wenn das Spiel vorbei ist, verwenden wir glColor3ub(r,g,b), um eine zufällige Farbe zu wählen. Beachten Sie, dass wir 3ub, statt 3f verwenden. Indem wir 3ub verwenden, können wir Integer-Wert zwischen 0 und 255 verwenden, um unsere Farbe zu setzen. Außerdem ist es einfacher zufällige Zahlen zwischen 0 und 255 zu ermitteln, als zufällige Zahlen zwischen 0.0f und 1.0f.

Wenn eine zufällige Zahl gewählt wurde, geben wir die Wörter "GAME OVER" aus, rechts neben dem Spiel-Titel. Direkt unter "GAME OVER" geben wir "PRESS SPACE" aus. Damit übermitteln wir dem Spieler die visuelle Nachricht, dass er gestorben ist und die Leertaste drücken soll, wenn er ein neues Spiel starten möchte.

    if (gameover)                                // Ist das Spiel vorbei?
    {
        glColor3ub(rand()%255,rand()%255,rand()%255);            // wähle eine zufällige Farbe
        glPrint(472,20,1,"GAME OVER");                    // gebe GAME OVER auf dem Screen aus
        glPrint(456,40,1,"PRESS SPACE");                // gebe PRESS SPACE auf dem Screen aus
    }

Wenn der Spieler noch Leben übrig hat, zeichnen wir animierte Bilder des Charakters des Spielers rechts neben den Spiele-Titel. Um das zu machen, erzeugen wir eine Schleife, die von 0 bis zur aktuellen verbleibenden Anzahl der Leben des Spielers minus eins, durchläuft. Ich subtrahiere eins, da das aktuelle Leben das Bild ist, dass Sie steuern.

Innerhalb der Schleife resette ich die View. Nachdem die View resettet wurden, translatieren wir zum Pixel 490 nach rechts plus dem Wert von loop1 mal 40.0f. Damit zeichnen wir die animierten Spieler-Leben 40 Pixel voneinander entfernt. Das erste animierte Bild wird bei 490+(0*40) gezeichnet werden (=490), das zweite animierte Bild wird bei 490+(1*40) gezeichnet werden (=530), etc.

Nachdem wir uns zu dem Punkt bewegt haben, wo wir die animierten Bilder zeichnen wollen, rotieren wir gegen den Uhrzeigersinn, abhängig von dem Wert, der in player.spin gespeichert ist. Damit rotiert das animierte Lebens-Bild in die entgegengesetzte Richtung wie der aktive Spieler.

Wir wählen dann grün als Farbe aus und fangen an, das Bild zu zeichnen. Das Zeichnen von Linien ist ziemlich ähnlich dem Zeichnen von Quads oder Polygonen. Sie fangen mit glBegin(GL_LINES) an, was OpenGL mitteilt, dass wir einen Linien zeichnen wollen. Linien haben 2 Vertices. Wir verwenden glVertex2d um unseren ersten Punkt zu setzen. glVertex2d benötigt keinen Z-Wert, was ganz nett ist, da wir uns nicht um den Z-Wert kümmmern wollen. Der erste Punkt wird 5 Pixel links von der aktuellen X-Position gezeichnet und 5 Pixel oberhalb der aktuellen Y-Position. Damit erhalten wir den oberen linken Punkt. Der zweite Punkt unserer ersten Linie wir 5 Pixel rechts von der aktuellen X-Position unt 5 Pixel runter gezeichnet, was uns den unteren rechten Punkt gibt. Damit wird eine Linie von oben Links nach unten rechts gezeichnet. Unsere zweite Linie wird von oben rechts nach unten links gezeichnet. Damit zeichnen wir ein grünes X auf den Screen.

Nachdem wir unser grünes X gezeichnet haben, rotieren wir gegen den Uhrzeigersinn (auf der Z-Achse) etwas weiter, aber diesmal nur mit halber Geschwindigkeit. Wir wählen dann einen etwas dunkleren Grünton (0.75f) und zeichnen ein weiteres X, aber diesmal benutzen wir 7 statt 5. Damit zeichnen wir ein größeres / dunkleres X über das erste grüne X.

    for (loop1=0; loop1<lives-1; loop1++)                    // iteriere durch die Leben minus ein aktuelles Leben
    {
        glLoadIdentity();                        // Resette die View
        glTranslatef(490+(loop1*40.0f),40.0f,0.0f);            // gehe rechts neben unseren Titel-Text
        glRotatef(-player.spin,0.0f,0.0f,1.0f);                // rotiere gegen den Uhrzeigersinn
        glColor3f(0.0f,1.0f,0.0f);                    // Setze Spieler-Farbe auf hell-grün
        glBegin(GL_LINES);                        // fange an unseren Spieler mit Linien zu zeichnen
            glVertex2d(-5,-5);                    // oben links des Spielers
            glVertex2d( 5, 5);                    // unten rechts des Spielers
            glVertex2d( 5,-5);                    // oben rechts des Spielers
            glVertex2d(-5, 5);                    // unten links des Spielers
        glEnd();                            // fertig mit Zeichnen des Spielers
        glRotatef(-player.spin*0.5f,0.0f,0.0f,1.0f);            // rotiere mit dem Uhrzeigersinn
        glColor3f(0.0f,0.75f,0.0f);                    // Setze Spielerfarbe auf dunkel grün
        glBegin(GL_LINES);                        // fange an unseren Spieler mit Linien zu zeichnen
            glVertex2d(-7, 0);                    // linke mitte des Spielers
            glVertex2d( 7, 0);                    // rechte mitte des Spielers
            glVertex2d( 0,-7);                    // obere mitte des Spielers
            glVertex2d( 0, 7);                    // untere mitte des Spielers
        glEnd();                            // fertig mit dem Zeichnen des Spielers
    }

Nun werden wir das Gitter zeichnen. Wir setzen die Variable filled auf TRUE. Damit teilen wir unserem Programm mit, dass das Gitter komplett gefüllt wurde (sie werden gleich sehen, warum wir das machen).

Direkt danach setzen wir die Linien-Breite auf 2.0f. Damit werden die Linien dicker gemacht, damit das Gitter klarer aussieht.

Dann deaktiveren wir Anti-Aliasing. Der Grund warum wir Anti-Aliasing deaktivieren ist der, obwohl es ein großartiges Feature ist, benötigt es doch recht viel CPU Power. Solange Sie keine Killer-Grafikkarte haben, werden Sie eine deutliche Verlangsamung feststellen, wenn Sie Anti-Aliasing anlassen. Probieren Sie es ruhig aus, wenn Sie wollen :)

Die View wurde resettet und wir starten zwei Schleifen. loop1 wird von links nach rechts wandern und loop2 von oben nach unten.

Wir setzen die Linien-Farbe auf blau und überprüfen dann, ob die horizontale Linie, die wir zeichnen wollen, bereits einmal passiert wurde. Wenn Sie einmal durchlaufen wurde, setzen wir die Farbe auf weiß. Der Wert von hline[loop1][loop2] wird gleich TRUE sein, wenn die Linie durchlaufen wurde und gleich FALSE wenn nicht.

Nachdem wir die Farbe auf blau oder weiß gesetzt haben, zeichnen wir die Linie. Als erstes müssen wir sicherstellen, dass wir nicht zu weit nach rechts kommen. Wir wollen keine Linien zeichnen oder überprüfen, ob die Linie gefüllt wurde, wenn loop1 größer als 9 ist.

Wenn wir erst einmal sicher sein können, dass loop1 innerhalb einer gültigen Spanne ist, überprüfen wir, ob die horizontale Linie nocht nicht gefüllt wurde. Wenn Sie das noch nicht wurde, setzen wir filled auf FALSE, was unser OpenGL Programm wissen lässt, dass es mindestens eine Linie gibt, die noch nicht gefüllt wurde.

Die Linie wird dann gezeichnet. Wir zeichnen unsere erste horizontale Linie (von links nach rechts) bei 20+(0*60) (=20). Diese Linie wird komplett bis 80+(0*60) (=80) gezeichnet. Beachten Sie, dass die Linie nach rechts gezeichnet wird. Darum wollen wir keine 11 (0-10) Linien zeichnen, da die letzte Line ganz rechts auf dem Screen beginnen würde und 80 Pixel außerhalb des Screens enden würde.

    filled=TRUE;                                // Setze Filled auf True vor dem Testen
    glLineWidth(2.0f);                            // Setze Linien-Breite für die Zellen auf 2.0f
    glDisable(GL_LINE_SMOOTH);                        // deaktiviere Antialiasing
    glLoadIdentity();                            // Resette die aktuelleModelview Matrix
    for (loop1=0; loop1// iteriere von links nach rechts
    {
        for (loop2=0; loop2// iteriere von oben nach unten
        {
            glColor3f(0.0f,0.5f,1.0f);                // Setze Linien-Farbe auf blau
            if (hline[loop1][loop2])                // wurde die horizontale Linie bereits durchlaufen
            {
                glColor3f(1.0f,1.0f,1.0f);            // wenn ja, setze Linien-Farbe auf weiß
            }
            if (loop1// zeichne nicht zu weit nach rechts
            {
                if (!hline[loop1][loop2])            // wenn eine horizontale Linie nicht gefüllt ist
                {
                    filled=FALSE;                // setze filled auf False
                }
                glBegin(GL_LINES);                // fange an horizontalen Zellenrand zu zeichnen
                    glVertex2d(20+(loop1*60),70+(loop2*40));    // linke seite der horizontalen Linie
                    glVertex2d(80+(loop1*60),70+(loop2*40));    // rechte Seite der horizontalen Linie
                glEnd();                    // fertig mit dem Zeichnen der horizontalen Zellränder
            }

Der folgende Code macht das Selbe, überprüft aber, dass wir nicht zu weit nach unten auf dem Screen zeichnen, anstatt zu weit nach rechts. Dieser Code ist dafür verantwortlich vertikale Linien zu zeichnen.

            glColor3f(0.0f,0.5f,1.0f);                // Setze Linien-Farbe auf blau
            if (vline[loop1][loop2])                // wurde die vertikale Linie vereits durchlaufen
            {
                glColor3f(1.0f,1.0f,1.0f);            // wenn ja, setze Linien-Farbe auf weiß
            }
            if (loop2// zeichne nicht zu weit nach unten
            {
                if (!vline[loop1][loop2])            // wenn eine vertikale Linie noch nicht gefüllt wurde
                {
                    filled=FALSE;                // setze filled auf False
                }
                glBegin(GL_LINES);                // fange an vertikale Zellränder zu zeichnen
                    glVertex2d(20+(loop1*60),70+(loop2*40));    // linke Seite der vertikalen Linie
                    glVertex2d(20+(loop1*60),110+(loop2*40));    // rechte Seite der vertikalen Linie
                glEnd();                    // fertig mit dem Zeichnen der Zellränder
            }

Nun überprüfen wir, ob 4 Seiten einer Box durchlaufen wurden. Jede Box auf dem Screen ist 1/10tel eines gesamten Bildes. Da jede Box ein Teil einer größeren textur ist, müssen wir als erstes Textur Mapping aktivieren. Da wir die Textur nicht rot, grün oder blau gefärbt haben wollen, setzen wir die Farbe auf ein helles weiß. Nachdem die Farbe auf weiß gesetzt wurde, wählen wir unsere Gitter-Textur (texture[1]) aus.

Als nächstes überprüfen wir, ob wir eine Box überprüfen, die auf dem Screen existiert. Erinnern Sie sich daran, dass unsere Schleife die 11 Linien von rechts nach links und 11 Linien von oben nach unten zeichnet. Wir haben aber keine 11 Boxen. Wir haben 10 Boxen. Deshalb müssen wir sicherstellen, dass wir nicht die 11te Position überprüfen. Wir machen das, indem wir sicherstellen, dass loop1 und loop2 kleiner als 10 ist. Das sind dann 10 Boxen, von 0-9.

Nachdem wir sichergestellt haben, dass wir uns innerhalb der Grenzen befinden, können wir anfangen, die Ränder zu überprüfen. hline[loop1][loop2] ist die obere Kante der Box. hline[loop1][loop2+1] ist die untere Kante der Box. vline[loop1][loop2] ist die linke Seite einer Box und vline[loop1+1][loop2] ist die rechte Seite einer Box. Hoffentlich kann ich die Dinge mit einem Diagramm etwas klarer ausdrücken:


Es wird angenommen, dass alle horizontalen Linien von loop1 bis loop1+1 verlaufen. Wie Sie sehen können verläuft die erste horizontale Linie entlängs loop2. Die zweite horizontale Linie verläuft entlängs loop2+1. Es wird angenommen, dass vertikale Linien von loop2 bis loop2+1 verlaufen. Die erste vertikale Linie läuft entlängs loop1 und die zweite vertikale Linie läuft entlängs loop1+1.

Wenn loop1 inkrementiert wird, wird die rechte Seite unserer alten Box die linke Seite unserer neuen Box. Wenn loop2 inkrementiert wird, wird die untere Kante der alten Box die obere Kante der neuen Box.

Wenn alle 4 Kanten TRUE sind (was bedeutet, dass wir alle einmal durchlaufen sind), können wir die Box texturieren. Wir machen das genauso wie wir die Font-Textur in mehrere Zeichen aufgeteilt haben. Wir dividieren sowohl loop1 als auch loop2 durch 10, da wir die Textur über 10 Boxen texturieren wollen, von links nach recht und 10 Boxen hoch und runter. Texturkoordinaten verlaufen von 0.0f bis 1.0f und 1/10tel von 1.0f ist 0.1f.

Um nun die obere rechte Seite unserer Box zu erhalten, dividieren wir den Schleifen-Wert durch 10 und addieren 0.1f zur X-Textur-Koordinate. Um die obere linke Seite zu erhalten, dividieren wir unsere Schleifen-Werte durch 10. Um die untere linke Seite zu erhalten, dividieren wir unsere Schleifen-Werte durch 10 und addieren 0.1f zur Y-Textur-Koordinate. Zu guter letzt erhalten wir die untere rechte Texturkoordinate, indem wir die Schleifen-Wert durch 10 dividieren und 0.1f zu beiden, den x und y-Textur-Koordinaten, hinzu addieren.

Kurze Beispiele:

loop1=0 und loop2=0

  • Rechte X Textur Koordinate = loop1/10+0.1f = 0/10+0.1f = 0+0.1f = 0.1f
  • Linke X Textur Koordinate = loop1/10 = 0/10 = 0.0f
  • Obere Y Textur Koordinate = loop2/10 = 0/10 = 0.0f;
  • Untere Y Textur Koordinate = loop2/10+0.1f = 0/10+0.1f = 0+0.1f = 0.1f;


  • loop1=1 und loop2=1

  • Rechte X Textur Koordinate = loop1/10+0.1f = 1/10+0.1f = 0.1f+0.1f = 0.2f
  • Linke X Textur Koordinate = loop1/10 = 1/10 = 0.1f
  • Obere Y Textur Koordinate = loop2/10 = 1/10 = 0.1f;
  • Untere Y Textur Koordinate = loop2/10+0.1f = 1/10+0.1f = 0.1f+0.1f = 0.2f;


  • Hoffentlich ergibt das alles einen Sinn. Wenn loop1 und loop2 gleich 9 wären, würden wir die Werte 0.9f und 1.0f erhalten. Wie Sie also sehen können verlaufen unsere Texturkoordinaten werden über die 10 Boxen gemapped vom niedrigsten 0.0f bis zum höchsten 1.0f. Dadurch wird die gesamte Textur auf den Screen gemappt. Nachdem wir einen Teil der Textur auf den Screen gemapped haben, dekativieren wir Texturmapping. Wenn wir alle Linien gezeichnet haben und alle Boxen gefüllt haben, setzen wir die Linien-Breite auf 1.0f.

                glEnable(GL_TEXTURE_2D);                // aktiviere Textur Mapping
                glColor3f(1.0f,1.0f,1.0f);                // helle weiße Farbe
                glBindTexture(GL_TEXTURE_2D, texture[1]);        // wähle das Tile Bild
                if ((loop1// wenn innerhalb der Grenzen, fülle umlaufende Boxen
                {
                    // wurden alle Seiten der Box abgelaufen?
                    if (hline[loop1][loop2] && hline[loop1][loop2+1] && vline[loop1][loop2] && vline[loop1+1][loop2])
                    {
                        glBegin(GL_QUADS);            // zeichne einen texturierten Quad
                            glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)));
                            glVertex2d(20+(loop1*60)+59,(70+loop2*40+1));    // oben rechts
                            glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)));
                            glVertex2d(20+(loop1*60)+1,(70+loop2*40+1));    // oben links
                            glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)+0.1f));
                            glVertex2d(20+(loop1*60)+1,(70+loop2*40)+39);    // unten links
                            glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)+0.1f));
                            glVertex2d(20+(loop1*60)+59,(70+loop2*40)+39);    // unten rechts
                        glEnd();                // fertig mit dem Texturieren der Box
                    }
                }
                glDisable(GL_TEXTURE_2D);                // deaktiviere Textur Mapping
            }
        }
        glLineWidth(1.0f);                            // Setze die Linien-Breite auf 1.0f
    

    Der folgende Code überprüft, ob anti gleich TRUE ist. Wenn dem so ist, aktivieren wir die Linien-Glättung (anti-aliasing).

        if (anti)                                // Ist Anti gleich TRUE?
        {
            glEnable(GL_LINE_SMOOTH);                    // wenn ja, aktiviereAntialiasing
        }
    

    Um das Spiel etwas einfacher zu machen, habe ich ein Spezial-Item eingefügt. Das Itemist eine Sanduhr. Wenn Sie die Sanduhr berühren, werden die Feinde für eine gewisse Zeit 'eingefroren'. Der folgende Codeabschnitt ist für das Zeichnen der Sanduhr verantwortlich.

    Für die Sanduhr verwenden wir x und y um sie zu positionieren, aber anders als beim Spieler und den Feinden, benutzen wir nicht fy und fy für die Fein-Positionierung. Statt dessen verwenden wir fx, um zu verfolgen, ob die Sanduhr angezeigt wird oder nicht. fx wird gleich 0 sein, wenn die Sanduhr nicht sichtbar ist. 1 wenn sie sichtbar ist und 2 wenn der Spieler die Sanduhr berührt hat. fy wird als Zähler verwendet, um zu verfolgen, wie lange die Sanduhr sichtbar oder unsichtbar sein soll.

    Deshalb fangen wir mit der Überprüfung an, ob die Sanduhr sichtbar ist. Wenn nicht, überspringen wir den Code, ohne sie zu zeichnen. Wenn sie sichtbar ist, resetten wir die Modelview Matrix und positionieren die Sanduhr. Da unser erster Gitterpunkt von links nach rechts bei 20 startet, werden wir hourglass.x mal 60 zu 20 addieren. Wir multiplizieren hourglass.x mit 60 weil die Punkten auf unserem Gitter von links nach rechts jeweils 60 Pixel auseinander sind. Wir positionieren dann die Sanduhr auf der Y-Achse. Wir addiere hourglass.y mal 40 zu 70.0f weil wir 70 Pixel unterhalb der oberen Kante des Screens anfangen wollen zu zeichnen. Jeder Punkt auf unserem Gitter von oben nach unten, hat einen Abstand von 40 Pixel.

    Nachdem wir die Sanduhr positioniert haben, könne wir sie auf der Z-Achse rotieren. hourglass.spin wird dazu verwendet, die Rotation zu verfolgen, geanuso wie player.spin die Rotation des Spielers verfolgt. Bevor wir anfangen die Sanduhr zu zeichnen, wählen wir eine zufällige Farbe.

        if (hourglass.fx==1)                            // wenn fx=1 zeichne die Sanduhr
        {
            glLoadIdentity();                        // Resette die Modelview Matrix
            glTranslatef(20.0f+(hourglass.x*60),70.0f+(hourglass.y*40),0.0f);    // bewege die Sanduhr an die feine Position
            glRotatef(hourglass.spin,0.0f,0.0f,1.0f);            // rotiere mit dem Uhrzeigersinn
            glColor3ub(rand()%255,rand()%255,rand()%255);            // Setze Sanduhr-Farbe auf eine zufällige Farbe
    

    glBegin(GL_LINES) teilt OpenGL mit, dass wir Linien zeichnen wollen. Wir fangen damit an, uns von unserere aktuellen Position nach links und 5 Pixel nach oben zu bewegen. Damit erhalten wir den oberen linken Punkt der Sanduhr. OpenGL wird die Linie von dieser Position aus anfangen zu zeichnen. Das Ende der Linie wird 5 Pixel nacht rechts und runter von unserer Ursprungs-Position sein. Damit erhalten wir eine Linie von oben links nach unten rechts. Direkt danach zeichnen wir eine zweite Linie, von oben rechts nach unten links. Damit erhalten wir ein 'X'. Wir beenden das Ganze damit, indem wir die unteren beiden Punkte verbinden und dann die oberen beiden Punkte, um ein Sanduhr-ähnliches Objekt zu erzeugen :)

            glBegin(GL_LINES);                        // fange an unsere Sanduhr mit Linien zu zeichnen
                glVertex2d(-5,-5);                    // oben links unserer Sanduhr
                glVertex2d( 5, 5);                    // unten rechts unserer Sanduhr
                glVertex2d( 5,-5);                    // oben rechts unserer Sanduhr
                glVertex2d(-5, 5);                    // unten links unserer Sanduhr
                glVertex2d(-5, 5);                    // unten links unserer Sanduhr
                glVertex2d( 5, 5);                    // unten rechts unserer Sanduhr
                glVertex2d(-5,-5);                    // oben links unserer Sanduhr
                glVertex2d( 5,-5);                    // oben rechts unserer Sanduhr
            glEnd();                            // fertig mit dem Zeichnen der Sanduhr
        }
    

    Nun zeichnen wir unseren Spieler. Wir resetten die Modelview Matrix und positionieren den Spieler auf dem Screen. Beachten Sie, dass wir den Spieler mittels fx und fy positionieren. Wir wollen, dass sich der Spieler 'weich' bewegt, weshalb wir die feine Positionierung verwenden. Nachdem der Spieler positioniert wurde, rotieren wir den Spieler auf der Z-Achse um player.spin. Wir setzen die Farbe auf ein helles Grün und fangen an zu zeichnen. Genauso wie beim Code für die Sanduhr, zeichnen wir ein 'X'. Angefangen von oben links nach unten rechts und dann von oben rechts nach unten links.

        glLoadIdentity();                            // Resette die Modelview Matrix
        glTranslatef(player.fx+20.0f,player.fy+70.0f,0.0f);            // bewege zur feinen Spieler Position
        glRotatef(player.spin,0.0f,0.0f,1.0f);                    // rotiere im Uhrzeigersinn
        glColor3f(0.0f,1.0f,0.0f);                        // Setze Spieler-Farbe auf ein helles Grün
        glBegin(GL_LINES);                            // fange an unseren Spieler mit Linien zu zeichnen
            glVertex2d(-5,-5);                        // oben links unseres Spielers
            glVertex2d( 5, 5);                        // unten rechts unseres Spielers
            glVertex2d( 5,-5);                        // oben rechts unseres Spielers
            glVertex2d(-5, 5);                        // unten links unseres Spielers
        glEnd();                                // fertig mit dem Zeichnen des Spielers
    

    Objekte mit wenig Details mittels Linien zu zeichnen kann sehr frustrierend sein. Ich wollte nicht, dass der Spieler allzu langweilig aussieht, weshalb ich den folgenden Codeabschnitt eingefügt habe, um eine größere und schneller rotierende Klinge über den Spieler, den wir weiter oben gezeichnet haben, zu setzen. Wir rotieren auf der Z-Achse um player.spin mal 0.5f. Da wir wieder rotieren, scheint es, als ob dieses Stück des Spielers ein klein wenig schneller rotiert als das erste Stück des Spielers.

    Weiter zu Teil 2