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

Zurück zu Teil 1

Nachdem wir die neue Rotation gemacht haben, setzen wir die Farbe auf einen dunkleren Grünton. So dass es aussieht, als ob der Spieler aus verschiedenen Farben / Teilen zusammengesetzt wird. Wir zeichnen dann ein großes '+' über den ersten Teil des Spielers. Es ist größer weil wir -7 und +7 statt -5 und +5 verwenden. Beachten Sie auch, dass wir statt von einer Ecke zur anderen, ich diesen Teil des Spielers von links nach rechts und von oben nach unten zeichne.
 
    glRotatef(player.spin*0.5f,0.0f,0.0f,1.0f);                // Rotiere im Uhrzeigersinn
    glColor3f(0.0f,0.75f,0.0f);                        // Setze Spielerfarbe auf Dunkelgrün
    glBegin(GL_LINES);                            // fange an unseren Spieler mit Linien zu zeichnen
        glVertex2d(-7, 0);                        // Links mitte unseres Spielers
        glVertex2d( 7, 0);                        // Rechts mitte unseres Spielers
        glVertex2d( 0,-7);                        // Oben mitte unseres Spielers
        glVertex2d( 0, 7);                        // Unten mitte unseres Spielers
    glEnd();                                // fertig mit dem Zeichnen des Spielers

Alles was wir nur noch machen müssen, ist das Zeichnen der Feinde und wir sind fertig mit dem Zeichnen :) Wir fangen damit an, eine Schleife zu erzeugen, die durch alle sichtbaren Feinde des aktuellen Levels iteriert. Wir berechnen dann, wieviele Feinde gezeichnet werden, indem wir unsere aktuelle Spiel-Ebene mit dem internen Spiel-Level multiplizieren. Denken Sie daran, dass jedes Level 3 Ebenen (Stages) hat und der maximale Wert des internen Levels 3 ist. So, dass wir maximal 9 Feinde haben können.

Innerhalb der Schleife resetten wir die Modelview Matrix und positionieren den aktuellen Feind (enemy[loop1]). Wir positionieren den Feind mittels seiner feinen x und y Wert (fx und fy). Nachdem der aktuelle Feind positioniert wurde, setzen wir die Farbe auf Pink und fangen an zu zeichnen.

Die erste Linie wir von 0, -7 (7 Pixel oberhalb der Startposition) bis -7, 0 (7 Pixel links von der Startposition) gehen. Die zweite Linie wird von -7,0 bis 0,7 (7 Pixel unterhalb der Startposition) gehen. Die dritte Linie wird von 0,8 bis 7,0 (7 Pixel rechts von der Startposition) gehen und die letzte Linie wird von 7,0 zurück zum Anfang der ersten Linie (7 Pixel oberhalb der Startposition) gehen. Das erzeugt einen nicht rotierenden pinken Diamanten auf dem Screen.
 
    for (loop1=0; loop1// Schleife um die Feinde zu zeichnen
    {
        glLoadIdentity();                        // Resette die Modelview Matrix
        glTranslatef(enemy[loop1].fx+20.0f,enemy[loop1].fy+70.0f,0.0f);
        glColor3f(1.0f,0.5f,0.5f);                    // mache den Feind-KörperPink
        glBegin(GL_LINES);                        // fange an Feind zu zeichnen
            glVertex2d( 0,-7);                    // oberer Punkt des Feindes
            glVertex2d(-7, 0);                    // linker Punkt des Körpers
            glVertex2d(-7, 0);                    // linker Punkt des Körpers
            glVertex2d( 0, 7);                    // unterer Punkt des Körpers
            glVertex2d( 0, 7);                    // unterer Punkt des Körpers
            glVertex2d( 7, 0);                    // rechter Punkt des Körpers
            glVertex2d( 7, 0);                    // rechter Punkt des Körpers
            glVertex2d( 0,-7);                    // oberer Punkt des Körpers
        glEnd();                            // fertig mit dem Zeichnen des Feind-Körpers

Wir wollen ja ebenfalls nicht, dass der Feind langweilig aussieht, weshalb wir eine dunkel rote rotierende Klinge ('X') über den Diamanten zeichnen. Wir rotieren auf der Z-Achse um enemy[loop1].spin und zeichnen dann das 'X'. Wir fangen oben links and und zeichnen dann eine Linie nach unten rechts. Dann zeichnen wir eine zweite Linie von oben rechts nach unten links. Die beiden Linien schneiden sich, was ein 'X' ergibt (oder eine Klinge... ).
 
        glRotatef(enemy[loop1].spin,0.0f,0.0f,1.0f);            // Rotiere die Feindes-Klinge
        glColor3f(1.0f,0.0f,0.0f);                    // mache die Klinge rot
        glBegin(GL_LINES);                        // fange an die Feindes-Klinge zu zeichnen
            glVertex2d(-7,-7);                    // oben links des Feindes
            glVertex2d( 7, 7);                    // oben rechts des Feindes
            glVertex2d(-7, 7);                    // unten links des Feindes
            glVertex2d( 7,-7);                    // oben rechts des Feindes
        glEnd();                            // fertig mit dem Zeichnen der Feindes-Klinge
    }
    return TRUE;                                // alles verliefOK
}

Ich habe den KillFont() Befehl am Ende von KillGLWindow() eingefügt. Damit geht man sicher, dass die Font Display-Liste zerstört wird, wenn das Fenster zerstört wird.
 
GLvoid KillGLWindow(GLvoid)                            // Entferne das Fenster korrekt
{
    if (fullscreen)                                // Sind wir im Fullscreen Modus?
    {
        ChangeDisplaySettings(NULL,0);                    // Wenn ja, wechsle zurück zum Desktop
        ShowCursor(TRUE);                        // Zeige den Maus-Zeiger
    }

    if (hRC)                                // Haben wir einen Rendering Context?
    {
        if (!wglMakeCurrent(NULL,NULL))                    // Können wir den DC und RC Kontext freigeben?
        {
            MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        }

        if (!wglDeleteContext(hRC))                    // Können wir den RC löschen?
        {
            MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        }
        hRC=NULL;                            // Setze RC auf NULL
    }

    if (hDC && !ReleaseDC(hWnd,hDC))                    // Können wir DC freigeben?
    {
        MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hDC=NULL;                            // Setze DC auf NULL
    }

    if (hWnd && !DestroyWindow(hWnd))                    // Können wir das Fenster zerstören?
    {
        MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hWnd=NULL;                            // Setze hWnd auf NULL
    }

    if (!UnregisterClass("OpenGL",hInstance))                // Können wir die Klasse de-registrieren?
    {
        MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;                            // Setze hInstance auf NULL
    }

    KillFont();                                // entferne den Font, den wir erzeugt haben
}

Der CreateGLWindow() und WndProc() Code hat sich nicht geändert, deshalb gehen Sie direkt zu folgendem Codeabschnitt.
 
int WINAPI WinMain(    HINSTANCE    hInstance,                // Instanz
            HINSTANCE    hPrevInstance,                // vorherige Instanz
            LPSTR        lpCmdLine,                // Kommandozeilen Parameter
            int        nCmdShow)                // Fenster Anzeige Status
{
    MSG    msg;                                // Windows Nachrichten Struktur
    BOOL    done=FALSE;                            // Bool Variable um die Schleife zu beenden

    // Frage den Benutzer, in welchen Modus er starten will
    if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
    {
        fullscreen=FALSE;                        // Fenster-Modus
    }

Dieser Codeabschnitt hat sich nicht allzu sehr geändert. Ich habe den Fenstertitel in "NeHe's Line Tutorial" geändert und ich habe den ResetObjects() Befehl hinzugefügt. Damit wird der Spieler in die obere linke Ecke des Gitters gesetzt und den Feinden einen zufälligen Startpunkt zugewiesen. Die Feinde werden immer mindestens 5 Tiles von Ihnen entfernt starten. TimerInit() initialisiert den Timer, damit er korrekt genutzt werden kann.
 
    if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))    // erzeuge unser OpenGL Fenster
    {
        return 0;                            // Beende, wenn Fenster nicht erzeugt wurde
    }

    ResetObjects();                                // Setze Spieler / Feind Startposition
    TimerInit();                                // Initialisiere den Timer

    while(!done)                                // Schleife die so lange läuft, bis done=TRUE
    {
        if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))            // Wartet eine Nachricht?

        {
            if (msg.message==WM_QUIT)                // Haben wir eine Nachricht zum Beenden erhalten?
            {
                done=TRUE;                    // wenn ja,  done=TRUE
            }
            else                            // Wenn nicht, bearbeite die Fenster-Nachrichten
            {
                TranslateMessage(&msg);                // Übersetze die Nachricht
                DispatchMessage(&msg);                // bearbeite die Nachricht
            }
        }
        else                                // Wenn da keine Nachrichten sind
        {

Nun bringen wir den Timing Code zum laufen. Beachten Sie, dass wir, bevor wir die Szene zeichnen, die Zeit ermitteln und sie in einer Floating-Point-Variable namens start speichern. Wir zeichnen dann die Szene und swappen die Buffer.

Direkt nachdem die Buffer geswapped wurden erzeugen wir eine Verzögerung. Wir machen das, indem wir überprüfen, ob der aktuelle Wert des Timers (TimerGetTime( )) kleiner als der Startwert plus der Spiel Schrittgeschwindigkeit mal 2 ist. Wenn der aktuelle Timer Wert kleiner als der Wert ist, den wir haben wollen, durchlaufen wir solange eine Schleife, bis der aktuelle Timer Wert gleich oder größer dem Wert ist, den wir haben wollen. Das verlangsamt WIRKLICH schnelle Systeme.

Da wir die Schrittgeschwindigkeit verwenden (gesetzt von dem adjust-Wert), läuft das Programm mit der selben Geschwindigkeit. Wenn unsere Schrittgeschwindigkeit zum Beispiel 1 wäre, würden wir so lange warten, bis der Timer größer oder gleich 2 (1*2) wäre. Wenn wir aber die Schrittgeschwindigkeit auf 2 erhöhen würden (was den Spieler doppelt so viele Pixel auf einmal zurücklegen lassen würde), wäre die Verzögerung gleich 4 (2*2). Obwohl wir also uns doppelt so schnell bewegen, ist die Verzögerung doppelt so lang, weshalb das Spiel immer noch mit der selben Geschwindigkeit läuft :)

Etwas was viele Leute gerne machen, ist, dass sie die aktuelle Zeit nehmen, und die alte Zeit subtrahieren, um herauszufinden, wieviel Zeit vergangen ist. Dann bewegen sie die Objekte um eine bestimmte Distanz basierend auf der verstrichenen Zeit. Leider kann ich das in diesem Programm hier nicht machen, da die Fein-Bewegung exakt sein muss, so dass der Spieler direkt auf den Gitterlinien ist. Wenn die aktuelle feine x-Position gleich 59 wäre und der Computer entscheided, dass der Spieler um 2 Pixel sich bewegen muss, würde der Spieler niemals auf der vertikalen Linie an Position 60 des Gitters landen.
 
            float start=TimerGetTime();                // ermittle Timer Wert bevor wir zeichnen

            // Zeichne die Szene. Überprüfe auf die ESC-Taste und Quit-Nachrichten von DrawGLScene()
            if ((active && !DrawGLScene()) || keys[VK_ESCAPE])    // Aktiv? Wurde eine Nachricht zum Beenden erhalten?
            {
                done=TRUE;                    // ESC oder DrawGLScene signalisierten zum Beenden
            }
            else                            // noch nicht Zeit zum aktualisieren des Screens
            {
                SwapBuffers(hDC);                // Swappe Buffer (Double Buffering)
            }

            while(TimerGetTime() <start+float(steps[adjust]*2.0f)) {}// verschwende Rechenzeit auf schnellen Systemen

Der folgende Code hat sich nicht wirklich geändert. Ich habe den Fenstertitel lediglich in "NeHe's Line Tutorial" geändert.
 
            if (keys[VK_F1])                    // wurde F1 gedrückt?
            {
                keys[VK_F1]=FALSE;                // wenn ja, setze Key auf FALSE
                KillGLWindow();                    // Kill unser aktuelles Fenster
                fullscreen=!fullscreen;                // wechsel Fullscreen / Fenstermodus
                // erzeuge unser OpenGL Fenster neu
                if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))
                {
                    return 0;                // beende, wenn das Fenster nicht erzeugt wurde
                }
            }

Dieser Codeabschnitt überprüft, ob die A-Taste gedrückt wurde und nicht heruntergedrückt bleibt. Wenn 'A' gedrückt wurde, wird ap auf TRUE gesetzt (was unserem Programm mitteilt, dass A gedrückt wurde) und anti wechselt von TRUE auf FALSE oder von FALSE auf TRUE. Denken Sie daran, dass anti im Zeichnen-Code überprüft wird, um zu wissen, ob Antialiasing an oder ausgeschaltet ist.

Wenn die 'A'-Taste losgelassen wurd (FALSE ist), dann wir ap auf FALSE gesetzt, was dem Programm mitteilt, dass die Taste nicht länger gedrückt wird.
 
            if (keys['A'] && !ap)                    // wenn die 'A' Taste gedrückt und nicht gehalten wird
            {
                ap=TRUE;                    // wird ap gleich TRUE
                anti=!anti;                    // wechsel Antialiasing
            }
            if (!keys['A'])                        // wenn die 'A' Taste losgelassen wurde
            {
                ap=FALSE;                    // wird ap gleich FALSE
            }

Nun bewegen wir die Feinde. Ich wollte diesen Codeabschnitt wirklich simpel halten. Es gibt hier nur wenig Logik. Grundsätzlich überprüfen die Feinde wo Sie sind und bewegen sich in diese Richtung. Da ich die eigentliche X und Y Position des Spielers überprüfe und nicht die feinen Werte, scheint es, als ob die Spieler etwas mehr Intelligenz hätten. Sie sehen vielleicht, dass Sie sich oben am Screen befinden. Aber wenn ihre feinen Werte wirklich oben am Screen angelangt sind, können Sie sich schon an einer komplett anderen Position sein. Damit bewegen sich die Feinde manchmal an Ihnen vorbei, bevor sie merken, dass Sie sich gar nicht mehr da befinden, wo sie dachten, dass Sie es wären. Klingt so, als ob die Feinde recht dumm wären, aber gerade weil sie manchmal sich an Ihnen vorbei bewegen, kann es auch mal vorkommen, dass Sie von allen Seiten aus umzingelt werden.

Wir fangen damit an sicherzustellen, dass das Spiel noch nicht vorbei ist und dass das Fenster (wenn man im Fenster-Modus ist) immer noch aktiv ist. Indem überprüft wird, ob das Fenster noch aktiv ist, kann man sicherstellen, dass die Feinde sich nicht bewegen, wenn das Fenster minimiert ist. Damit haben wir gleichzeitig ein Pausen-Feature, für den Fall, dass Sie mal eine Pause benötigen :)

Nachdem wir sichergestellt haben, dass sich die Feinde bewegen sollen, erzeugen wir eine Schleife. Die Schleife wird durch alle sichtbaren Feinde iterieren. Erneut berechnen wir, wie viele Feinde auf dem Screen angezeigt werden sollen, indem wir die aktuelle Ebene mit dem aktuellen internen Level multiplizieren.
 
            if (!gameover && active)                // wenn das Spiel noch nicht vorbei ist und das Programm aktiv ist, bewege die Objekte
            {
                for (loop1=0; loop1// iteriere durch die verschiedenen Ebenen
                {

Nun bewegen wir den aktuellen Feind (enemy[loop1]). Wir fangen damit an zu überprüfen, ob die X-Position des Feindes kleiner als die X-Position des Spielers ist und wir stellen sicher, dass die feine Y-Position des Feindes auf einer horizontalen Linie liegt. Wir können den Feind nicht nach links und rechts bewegen, wenn er nicht auf einer horizontalen Linie ist. Wenn wir es doch täten, würde der Feind direkt durch die Box marschieren, was das Spiel noch schwieriger machen würde :)

Wenn die X-Position des Feindes kleiner als die X-Position des Spielers ist und die feine Y-Position der Feindes auf einer horizontalen Linie liegt, bewegen wir die X-Position des Feindes einen Block näher an die aktuelle Spieler-Position.

Wir machen das auch um den Feind nach links, unten und oben zu bewegen. Wenn wir nach oben und unten bewegen, müssen wir sicherstellen, dass die feine X-Position auf einer vertikalen Linie liegt. Wir wollen schließlich nicht, dass der Feind nach oben oder unten durch die Box bricht.

Anmerkung: das Ändern der x und y Position des Feindes bewegt den Feind nicht auf dem Screen. Denken Sie daran, als wir die Feinde gezeichnet haben, haben wir die feinen Positionen verwendet, um die Feinde auf dem Screen zu positionieren. Das ändern der x und y Position teilt unserem Programm nur mit, wohin wir WOLLEN, dass sich die Feinde bewegen.
 
                    if ((enemy[loop1].x <player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
                    {
                        enemy[loop1].x++;        // bewege den Feind nach rechts
                    }

                    if ((enemy[loop1].x>player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
                    {
                        enemy[loop1].x--;        // bewege den Feind nach links
                    }

                    if ((enemy[loop1].y <player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
                    {
                        enemy[loop1].y++;        // bewege den Feind nach unten
                    }

                    if ((enemy[loop1].y>player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
                    {
                        enemy[loop1].y--;        // bewege den Feind nach oben
                    }

Dieser Code realisiert die eigentliche Bewegung. Wir überprüfen, ob die Variable delay größer als 3 minus das aktuelle interne Level ist. Auf diese Weise würde das Programm, wenn das aktuelle Level 1 wäre, 2 (3-1) Schleifen durchlaufen, bevor sich der Feind erst bewegen würden. In Level 3 (dem höchsten Wert, den Level annehmen kann), bewegen sich die Feinde mit der selben Geschwindigkeit wie der Spieler (keine Verzögerungen). Wir stellen ebenfalls sicher, dass hourglass.fx nicht gleich 2 ist. Denken Sie daran, wenn hourglass.fx gleich 2 ist, bedeutet das, dass der Spieler die Sanduhr berührt hat. Was wiederum bedeutet, dass die Feinde sich nicht bewegensollen.

Wenn delay größer als 3-level ist und der Spieler noch nicht die Sanduhr berührt hat, bewegen wir die Feinde, indem wir die feinen Positionen (fx und fy) der Feinde adjustieren. Als erstes setzen wir delay zurück auf 0, so dass wir mit dem Verzögerungs-Zähler erneut anfangen können zu zählen. Dann erzeugen wir eine Schleife, die durch alle sichtbaren Feinde iteriert (Ebene mal Level).
 
                    if (delay>(3-level) && (hourglass.fx!=2))        // wenn unsere Verzögerung vorbei ist und der Spieler nicht die Sanduhr bekommen hat
                    {
                        delay=0;                    // Resette den Verzögerungszähler zurück auf null
                        for (loop2=0; loop2// iteriere durch alle Feinde
                        {

Um die Feinde zu bewegen, überprüfen wir, ob der aktuelle Feind (enemy[loop2]) sich in eine bestimmte Richtung bewegen muss, um die x und y Position des Feindes zu erreichen, die wir haben wollen. In der ersten folgenden Zeile überprüfen wir, ob die feine Position auf der X-Achse des Feindes kleiner als die gewünschte X-Position mal 60 ist. (Zur Erinnerung: jedes Gitter von links nach rechts ist 60 Pixel auseinander). Wenn die feine X-Position kleiner als die X-Position des Feindes mal 60 ist, bewegen wir den Feind nach rechts um steps[adjust] (die Spielgeschwindigkeit unseres Spiels basiert auf dem Wert von adjust). Wir rotieren auch den Feind im Uhrzeigersinn, damit es so aussieht, als ob er nach rechts rollt. Wir machen das, indem wir enemy[loop2].spin um steps[adjust] inkrementieren (die aktuelle Spielgeschwindigkeit basiert auf adjust).

Wir überprüfen dann, ob der fx Wert des Feindes größer als die X-Position des Feindes mal 60 ist und wenn dem so ist, bewegen wir den Feind nach links und lassen den Feind nach links rotieren.

Wir machen das Selbe beim hoch und runter bewegen. Wenn die Y-Position des Feindes kleiner als die fy-Position des Feindes mal 40 ist (40 Pixel liegen zwischen dem Gitter hoch und runter), inkrementieren wir die fy-Position des Feindes und rotieren den Feind, damit es aussieht, als ob er nach unten rollt. Zu guter Letzt, wenn die Y-Position des Feindes größer als die fy-Position des Feindes mal 40 ist, dekrementieren wir den Wert von fy, um den Feind nach oben zu bewegen. Erneut, die Rotation des Feindes, lässt ihn so ausschauen, als ob er nach oben rollt.
 
                            if (enemy[loop2].fx <enemy[loop2].x*60)    // Ist die feine Position auf der X-Achse niedriger als die gewünschte Position?
                            {
                                enemy[loop2].fx+=steps[adjust];    // wenn ja, inkrementiere die feine Position auf der X-Achse
                                enemy[loop2].spin+=steps[adjust];    // rotiere Feind mit dem Uhrzeigersinn
                            }
                            if (enemy[loop2].fx>enemy[loop2].x*60)    // Ist die feine Position auf der X-Achse größer als die gewünschte Position?
                            {
                                enemy[loop2].fx-=steps[adjust];    // wenn ja, dekrementiere die feine Position auf der X-Achse
                                enemy[loop2].spin-=steps[adjust];    // rotiere Feind entgegen den Uhrzeigersinn
                            }
                            if (enemy[loop2].fy <enemy[loop2].y*40)    // Ist die feine Position auf der Y-Achse niedriger als die gewünschte Position?
                            {
                                enemy[loop2].fy+=steps[adjust];    // wenn ja, inkrementiere die feine Position auf der Y-Achse
                                enemy[loop2].spin+=steps[adjust];    // rotiere Feind mit dem Uhrzeigersinn
                            }
                            if (enemy[loop2].fy>enemy[loop2].y*40)    // Ist die feine Position auf der Y-Achse größer als die gewünschte Position?
                            {
                                enemy[loop2].fy-=steps[adjust];    // wenn ja, dekrementiere die feine Position auf der Y-Achse
                                enemy[loop2].spin-=steps[adjust];    // rotiere Feind entgegen den Uhrzeigersinn
                            }
                        }
                    }

Nachdem die Feinde bewegt wurden, überprüfen wir, ob irgend einer von ihnen den Spieler getroffen hat. Wir wollen Genauigkeit, weshalb wir die feinen Positionen des Feindes und des Spielers vergleichen. Wenn die fx-Position des Feindes gleich der fx-Position des Spielers ist und die fy-Position des Feindes gleich der fy-Position des Spielers ist, ist der Spielr TOD :)

Wenn der Spieler tod ist, dekrementieren wir lives. Dann überprüfen wir, dass dem Spieler die Leben nicht ausgegangen sind. Wir machen das, indem wir überprüfen, ob lives gleich 0 ist. Wenn lives gleich 0 ist, setzen wir gameove auf TRUE.

Wir resetten dann unsere Objekte, indem wir ResetObjects() aufrufen und den Todes-Sound abspielen.

Sound ist etwas neues in diesem Tutorial. Ich habe mich dazu entschieden die einfachste verfügbare Sound-Routine zu verwenden... PlaySound(). PlaySound() erwartet drei Parameter. Als erstes übergeben wir den Namen der Datei, die wir abspielen wollen. In diesem Fall wollen wir die Die.WAV Datei aus dem Data-Verzeichnis abspielen. Der zweite Parameter kann ignoriert werden. Wir setzen ihn auf NULL. Der dritte Parameter ist das Flag wie der Sound gespielt werden soll. Die zwei gebräuchlichsten Flags sind: SND_SYNC was alles andere stoppt, während der Sound abgespielt wird und SND_ASYNC, was den Sound abspielt, aber das Programm nicht anhält. Wir wollen eine kurze Verzögerung haben, nachdem der Spieler gestorben ist, weshalb wir SND_SYNC verwenden. Ziemlich einfach!

Etwas was ich am Anfang des Programms vergessen habe zu erwähnen: Damit PlaySound() und der Timer verwendet werden kann, müssen Sie die WINMM.LIB Datei unter Projekte / Einstellungen / Linker in Visual C++ einfügen. Winmm.lib ist die Windows Multimedia Library. Wenn Sie diese Library nicht inkludieren, werden Sie Fehlermeldungen erhalten, wenn Sie versuchen, das Programm zu kompilieren.
 
                    // Ist irgend ein Feind über dem Spieler?
                    if ((enemy[loop1].fx==player.fx) && (enemy[loop1].fy==player.fy))
                    {
                        lives--;            // wenn ja, verliert der Spieler ein Leben

                        if (lives==0)            // haben wir keine Leben mehr?
                        {
                            gameover=TRUE;        // wenn dem so ist, wird gameover auf TRUE gesetzt
                        }

                        ResetObjects();            // Resette Spieler / Feind Positionen
                        PlaySound("Data/Die.wav", NULL, SND_SYNC);    // spiele den Todes-Sound
                    }
                }

Nun können wir den Spieler bewegen. In der ersten folgenden Codezeile überprüfen wir, ob die rechte Pfeiltaste gedrückt wurde, player.x kleiner als 10 ist (wir wollen das Gitter ja nicht verlassen), dass player.fx gleich player.x mal 60 ist (das wir auf einem Teil des Gitters sind, was die X-Achse kreuzt) und das player.fy gleich player.y mal 40 ist (das der Spieler auf einem Teil des Gitters ist, was die Y-Achse kreuzt).

Wenn wir nicht überprüfen würden, ob der Spieler auf dem Gitter wäre, und wir ihm trotzdem erlauben würden sich überall hinzubewegen, würde der Spieler direkt durch die Boxen marschieren, genauso wie es die Feinde getan hätten, wenn wir nicht sichergestellt hätten, dass sie sich auf einer horizontalen oder auf einer vertikalen Linie befinden. Indem wir das überprüfen, stellen wir auch sicher, dass der Spieler mit seiner Bewegung fertig ist, bevor wir zu einer neuen Position uns bewegen.

Wenn der Spieler an einem Knotenpunkt ist (wo sich eine horizontale und eine vertikale Linie treffen) und er noch nicht ganz rechts ist, markieren wir die aktuelle horizontale Linie, an der wir uns befinden, als durchlaufen. Wir inkrementieren dann den Wert von player.x um eins, was die neue Spieler-Position eine Box nach rechts verschiebt.

Wir machen das Selbe, wenn wir uns nach links bewegen, hoch und runter. Wenn wir uns nach links bewegen, stellen wir sicher, dass wir nicht zu weit nach links auf dem Gitter wandern. Wenn wir uns nach unten bewegen, stellen wir sicher, dass der Spieler das Gitter nicht nach unten verlässt und wenn wir uns nach oben bewegen, stellen wir sicher, dass er nicht das Gitter nach oben hin verlässt.

Wenn wir uns nach links und rechts bewegen, setzen wir die horizontale Linie (hline[ ] [ ]) unter uns auf TRUE, was bedeutet, dass sie durchlaufen wurde. Wenn wir uns hoch und runter bewegen, setzen wir die vertikalen Linie (vline[ ][ ]) unter uns auf TRUE, was bedeutet, dass sie durchlaufen wurde.
 
                if (keys[VK_RIGHT] && (player.x// Markiere den aktuellenhorizontalen Rahmen als gefüllt
                    player.x++;                // bewege den Spieler nach rechts
                }
                if (keys[VK_LEFT] && (player.x>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
                {
                    player.x--;                // bewege den Spieler nach links
                    hline[player.x][player.y]=TRUE;        // Markiere den aktuellenhorizontalen Rahmen als gefüllt
                }
                if (keys[VK_DOWN] && (player.y// Markiere den aktuellenvertikalen Rahmen als gefüllt
                    player.y++;                // bewege den Spieler nach unten
                }
                if (keys[VK_UP] && (player.y>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
                {
                    player.y--;                // bewege den Spieler nach oben
                    vline[player.x][player.y]=TRUE;        // Markiere den aktuellenvertikalen Rahmen als gefüllt
                }

Wir inkrementieren / dekrementieren die feinen fx und fy Variablen des Spielers genauso wie wir die feinen fx und fy Variablen der Feinde inkrementiert / dekrementiert haben.

Wenn der fx Wert des Spielers kleiner als der X-Wert des Spielers mal 60 ist, inkrementieren wir die fx-Position des Spielers um die Schrittgeschwindigkeit unseres Spiels, basierend auf dem Wert von adjust.

Wenn der fx Wert des Spielers größer als der X-Wert des Spielers mal 60 ist, dekrementieren wir die fx-Position des Spielers um die Schrittgeschwindigkeit unseres Spiels, basierend auf dem Wert von adjust.

Wenn der fy Wert des Spielers kleiner als der Y-Wert des Spielers mal 40 ist, inkrementieren wir die fy-Position des Spielers um die Schrittgeschwindigkeit unseres Spiels, basierend auf dem Wert von adjust.

Wenn der fy Wert des Spielers größer als der Y-Wert des Spielers mal 40 ist, dekrementieren wir die fy-Position des Spielers um die Schrittgeschwindigkeit unseres Spiels, basierend auf dem Wert von adjust.
 
                if (player.fx <player.x*60)            // Ist die feine Position auf der X-Achse niedriger als die gewünschte Position?
                {
                    player.fx+=steps[adjust];        // wenn ja, inkrementiere die feine X Position
                }
                if (player.fx>player.x*60)            // Ist die feine Position auf der X-Achse größer als die gewünschte Position?
                {
                    player.fx-=steps[adjust];        // wenn ja, dekrementiere die feine X Position
                }
                if (player.fy <player.y*40)            // Ist die feine Position auf der Y-Achse niedriger als die gewünschte Position?
                {
                    player.fy+=steps[adjust];        // wenn ja, inkrementiere die feine Y Position
                }
                if (player.fy>player.y*40)            // Ist die feine Position auf der Y-Achse größer als die gewünschte Position?
                {
                    player.fy-=steps[adjust];        // wenn ja, dekrementiere die feine Y Position
                }
            }

Wenn das Spiel vorbei ist, läuft der folgende kleine Codeabschnitt. Wir überprüfen, ob die Leertaste gedrückt wurde. Wenn sie es wurde, setzen wir gameover auf FALSE (startet das Spiel neu). Wir setzen filled auf TRUE. Dadurch denkt das Spiel, dass wir mit der Ebene fertig sind, was den Spieler resettet, zusammen mit den Feinden.

Wir setzen dann das Anfangslevel auf 1, zusammen mit dem aktuell anzuzeigenden Level (level2). Wir setzen stage auf 0. Der Grund warum wir das machen, ist der, dass der Computer sieht, dass das Gitter gefüllt ist, er denkt, dass diese Ebene beendet wurde und stage um 1 erhöht. Da wir aber stage auf 0 gesetzt haben, wird stage inkrementiert und gleich 1 sein (genau das wollen wir ja). Zu letze setzen wir die Leben (lives) zurück auf 5.
 
            else                            // Ansonsten
            {
                if (keys[' '])                    // wenn Leertaste gedrückt wurde
                {
                    gameover=FALSE;                // gameover wird gleich FALSE
                    filled=TRUE;                // filled wird gleich TRUE
                    level=1;                // Anfangs-Level wird auf eins gesetzt
                    level2=1;                // angezeigtes Level wird ebenfalls auf eins gesetzt
                    stage=0;                // Game Stage wird auf null gesetzt
                    lives=5;                // Leben werden auf 5 gesetzt
                }
            }

Der folgende Code überprüft, ob das filled Flag gleich TRUE ist (was bedeutet, dass das Gitter gefüllt wurde). filled kann auf zwei Arten auf TRUE gesetzt werden. Entweder wurde das Gitter komplett gefüllt und filled wird auf TRUE gesetzt oder aber das Spiel wurde beendet und die Leertaste wurde gedrückt, um es neu zu starten (Code oben).

Wenn filled gleich TRUE ist, ist das erste was wir machen, den coolen Level-Komplett-Sound abspielen. Ich habe bereits erklärt, wie PlaySound() funktioniert. Diesmal spielen wir die Complete.WAV Datei aus dem DATA-Verzeichnis. Erneut benutzen wir SND_SYNC, so dass es eine Verzögerung gibt, bevor das Spiel in der nächsten Ebenen fortgesetzt wird.

Nachdem der Sound abgespielt wurde, inkrementieren wir stage um eins und überprüfen, ob stage nicht größer als 3 ist. Wenn stage größer als 3 ist, setzen wir stage auf 1 und inkrementieren das interne Level und das sichtbare Level um eins.

Wenn das interne Level größer als 3 ist, setzen wir das intere Level (level) auf 3 und inkrementieren lives um 1. Wenn Sie gut genug sind, das dritte Level hinter sich zu lassen, verdienen Sie ein Extra-Leben :). Nachdem lives inkrementiert wurde, stellen wir sicher, dass der Spieler nicht mehr als 5 Leben hat. Wenn live größer als 5 ist, setzen wir lives zurück auf 5.
 
            if (filled)                        // Ist das Gitter gefüllt?
            {
                PlaySound("Data/Complete.wav", NULL, SND_SYNC);    // wenn ja, spiele den Level-komplett-Sound ab
                stage++;                    // inkrementiere die Ebene
                if (stage>3)                    // Ist die Ebene größer als 3?
                {
                    stage=1;                // wenn ja, setze Ebene auf eins
                    level++;                // inkrementiere Level
                    level2++;                // inkrementiere das angezeigte Level
                    if (level>3)                // Ist Level größer als 3?
                    {
                        level=3;            // wenn ja, setze Level auf 3
                        lives++;            // gebe dem Spieler ein Extra-Leben
                        if (lives>5)            // Hat der Spieler mehr als 5 Leben?
                        {
                            lives=5;        // wenn ja, setze die Anzahl der Leben auf fünf
                        }
                    }
                }

Wir resetten dann alle Objekte (Spieler und Feinde). Damit wird der Spieler zurück in die obere linke Ecke des Gitters gesetzt und die Feinde erhalten eine neue Position auf dem Gitter.

Wir erzeugen zwei Schleifen (loop1 und loop2) um durch das Gitter zu iterieren. Wir setzen alle vertikalen und horizontalen Linien auf FALSE. Wenn wir das nicht machen würden, würde die nächste Ebene beginnen und das Spiel würde denken, dass das Gitter immer noch gefüllt wäre.

Beachten Sie, dass die Routine zum Löschen des Gitters ziemlich ähnlich der Routine zum Zeichnen des Gitters ist. Wir müssen sicherstellen, dass die Linien nicht zu weit nach rechts oder unten gezeichnet werden. Darum gehen wir sicher, dass loop1 kleiner als 10 ist, bevor wir die horizontalen Linien resetten nud wir gehen sicher, dass loop2 kleiner als 10 ist, bevor wir die vertikalen Linien resetten.
 
                ResetObjects();                    // Resette Spieler / Feind Positionen

                for (loop1=0; loop1// iteriere durch die X Koordinaten des Gitters
                {
                    for (loop2=0; loop2// iteriere durch die Y Koordinaten des Gitters
                    {
                        if (loop1// wenn die X Koordinate kleiner als 10 ist
                        {
                            hline[loop1][loop2]=FALSE;    // Setze den aktuellen horizontalen Wert auf FALSE
                        }
                        if (loop2// wenn die Y Koordinate kleiner als 10 ist
                        {
                            vline[loop1][loop2]=FALSE;    // Setze den aktuellen vertikale Wert auf  FALSE
                        }
                    }
                }
            }

Nun überprüfen wir, ob der Spieler die Sanduhr getroffen hat. Wenn der feine Spieler fx-Wert gleich dem X-Wert der Sanduhr mal 60 ist und der feine Spieler fy-Wert gleich dem Y-Wert der Sanduhr mal 40 ist UND hourglass.fx gleich 1 ist (was bedeutet, dass die Sanduhr angezeigt wird), wird der folgende Code ausgeführt.

Die erste Codezeile ist PlaySound("Data/freeze.wav",NULL, SND_ASYNC | SND_LOOP). Diese Zeile spielt die freeze.WAV Datei aus dem DATA Verzeichnis ab. Beachten Sie, dsas wir diesmal SND_ASYNC verwenden. Wir wollen, dass der freez-Sound abgespielt wird, ohne dass das Spiel anhält. SND_LOOP lässt den Sound in einer Endlosschleife spielen, bis wir sagen, dass aufgehört werden soll oder bis ein anderer Sound gespielt wird.

Nachdem wir angefangen haben, den Sound abzuspielen, setzen wir hourglass.fx auf 2. Wenn hourglass.fx gleich 2 ist, wird die Sanduhr nicht länger gezeichnet, die Feinde hören auf sich zu bewegen und der Sound läuft in einer Endlosschleife.

Ebenfalls setzen wir hourglass.fy auf 0. hourglass.fy ist ein Zähler. Wenn er einen bestimmten Wert erreicht, ändert sich der Wert von hourglass.fx.
 
            // wenn der Spieler die Sanduhr trifft, während sie angezeigt wird
            if ((player.fx==hourglass.x*60) && (player.fy==hourglass.y*40) && (hourglass.fx==1))
            {
                // spiele den Freeze-Enemy-Sound
                PlaySound("Data/freeze.wav", NULL, SND_ASYNC | SND_LOOP);
                hourglass.fx=2;                    // Setze die Sanduhr fx Variable auf zwei
                hourglass.fy=0;                    // Setze die Sanduhr fy Variable auf null
            }

Dieses Codestück inkrementiert den Spin-Wert des Spielers um die Hälfte der Geschwindigkeit, mit der das Spiel läuft. Wenn player.spin größer als 360.0f ist, subtrahieren wir 360.0f von player.spin. Damit wird verhindert das der Wert von player.spin zu groß wird.
 
            player.spin+=0.5f*steps[adjust];            // rotiere den Spieler im Uhrzeigersinn
            if (player.spin>360.0f)                    // Ist der spin Wert größer als 360?
            {
                player.spin-=360;                // wenn ja, subtrahiere 360
            }

Der folgende Code dekrementiert den Spin-Wert der Sanduhr um 1/4 der Geschwindigkeit mit der das Spiel läuft. Wenn hourglass.spin kleiner als 0.0f ist, addieren wir 360.0f. Wir wollen nicht, dass der Wert von hourglass.spin eine negative Zahl wird.
 
            hourglass.spin-=0.25f*steps[adjust];            // rotiere die Sanduhr gegen den Uhrzeigersinn
            if (hourglass.spin// Ist der spin-Wert kleiner als 0?
            {
                hourglass.spin+=360.0f;                // wenn ja, addiere 360
            }

Die erste folgende Zeile inkrementiert den Sanduhr-Zähler über den ich vorher gesprochen habe. hourglass.fy wird um die Spielgeschwindigkeit inkrementiert (die Spielgeschwindigkeit ist der steps-Wert basierend auf dem Wert von adjust).

Die zweite Zeile überprüft, ob hourglass.fx gleich 0 (nicht sichtbar) ist und der Sanduhr-Zähler (hourglass.fy) größer als 6000 dividiert durch das aktuelle interne Level (level) ist.

Wenn der fx-Wert gleich 0 ist und der Zähler größer als 6000 dividiert duch das interne Level, spiele wir die hourglass.WAV Datei, aus dem Data-Verzeichnis. Wir wollen nicht, dass diese Aktion gestoppt wird, weshalb wir SND_ASYNC verwenden. Wir wollen allerdings auch nicht, dass dieser Sound wiederholt wird, weshalb er nach einmal spielen, nicht erneut gespielt wird.

Nachdem wir den Sound abgespielt haben, geben wir der Sanduhr einen zufälligen Wert auf der X-Achse. Wir addieren eins zu dem zufälligen Wert, so dass die Sanduhr nicht an der Startposition des Spielers oben links im Gitter erscheint. Wir geben der Sanduhr ebenfalls einen zufälligen Wert auf der Y-Achse. Wir setzen hourglass.fx auf 1, damit die Sanduhr auf dem Screen an seiner neuen Position erscheint. Wir setzen hourglass.fy zurück auf null, damit man mit dem Zählen wieder anfangen kann.

Damit erreichen wir, dass die Sanduhr nach einer bestimmten Zeit auf dem Screen erscheint.
 
            hourglass.fy+=steps[adjust];                // inkrementiere die hourglass fy Variable
            if ((hourglass.fx==0) && (hourglass.fy>6000/level))    // Ist diehourglass fx Variable gleich 0 und die fy
            {                            // Variable größer als 6000 dividiert durch das aktuelle Level?
                PlaySound("Data/hourglass.wav", NULL, SND_ASYNC);    // wenn ja, spiele den Sanduhr-Erscheint-Sound ab
                hourglass.x=rand()%10+1;            // gebe der Sanduhr einen zufälligen X-Wert
                hourglass.y=rand()%11;                // gebe der Sanduhr einen zufälligen Y-Wert
                hourglass.fx=1;                    // Setze hourglass fx Variable auf eins (Hourglass Ebene)
                hourglass.fy=0;                    // Setze hourglass fy Variable auf null (Zähler)
            }

Wenn hourglass.fx gleich null ist und hourglass.fy größer als 6000 dividiert durch das aktuelle interne Level (level), dann setzen wir hourglass.fx zurück auf 0, was die Sanduhr verschwinden lässt. Wir setzen ebenso hourglass.fy auf 0, damit erneut mit dem Zählen begonnen werden kann.

Damit verschwindet die Sanduhr, wenn Sie sie nicht nach einer gewissen Zeit treffen.
 
            if ((hourglass.fx==1) && (hourglass.fy>6000/level))    // Ist die hourglass fx Variable gleich 1 und die fy
            {                            // Variable größer als 6000 dividiert durch das aktuelle Level?
                hourglass.fx=0;                    // wenn ja, setze fx auf null (Sanduhr verschwindet)
                hourglass.fy=0;                    // Setze fy auf null (resettet Zähler)
            }

Nun überprüfen wir, ob der 'Freeze-Enemy' Timer ausgelaufen ist, nachdem der Spieler die Sanduhr berührt hat.

Wenn hourglass.fx gleich 2 ist und hourglass.fy größer als 500 plus 500 mal das aktuelle interne Level, killen wir den Timer-Sound, den wir in einer Endlos-Schleife laufen lassen. Wir killen den Sound mit dem Befehl PlaySound(NULL, NULL, 0). Wir setzen hourglass.fx zurück auf 0 und setzen hourglass.fy auf 0. Indem fx und fy auf 0 gesetzt werden startet der Sanduhr-Zirkel von Neuem. fy muss gleich 6000 dividiert durch das aktuelle interne Level sein, bevor die Sanduhr wieder erscheint.
 
            if ((hourglass.fx==2) && (hourglass.fy>500+(500*level)))// Ist die Sanduhr fx Variable gleich 2 und die fy
            {                            // Variable größer als 500 Plus 500 Mal das aktuelle Level?
                PlaySound(NULL, NULL, 0);            // wenn ja, lösche den Freeze Sound
                hourglass.fx=0;                    // Setze Sanduhr fx Variable auf null
                hourglass.fy=0;                    // Setze Sanduhr fy Variable auf null
            }

Als letztes müssen wir noch die Variable delay inkrementieren. Wie Sie vielleicht erinnern, wird Delay dazu verwendet die Spieler-Bewegung und Animation zu aktualisieren. Wenn unser Programm beendet wurde, killen wir das Fenster und kehren zurück zum Desktop.
 
            delay++;                        // Inkrementiere den Feind-Verzögerungs-Zähler
        }
    }

    // Shutdown
    KillGLWindow();                                // Kill das Fenster
    return (msg.wParam);                            // beende das Programm
}

Ich habe viel Zeit damit verbracht, dieses Tutorial zu schreiben. Es fing mit einem einfachen Tutorial über Linien an und verwandelte sich in ein unterhaltsames Mini-Spiel. Hoffentlich können Sie etwas von dem verwenden, was Sie in diesem Tutorial gelernt haben. Ich weiss, dass viele von Ihnen nach TILE basierten Spielen gefragt haben. Nun, mehr Tile geht wohl nicht :) Ich habe ebenso viele Emails erhalten, die nach exaktem Pixel-Setzen gefragt haben. Ich denke, dass habe ich auch abgedeckt :) Am wichtigsten ist aber, dass dieses Tutorial Ihnen nicht nur neue Dinge über OpenGL beibringt, es bringt Ihnen auch bei, wie man einfache Sounds in Verbindung mit Ihrer visuellen Arbeit bringt! Ich hoffe Sie haben dieses Tutorial genossen. Wenn Sie denken, dass ich etwas nicht korrekt kommentiert habe oder das der Code irgendwo besser gemacht werden könnte, lassen Sie es mich bitte wissen. Ich möchte die beste OpenGL Tutorials machen, die möglich sind und ich bin an Ihrem Feedback interessiert.

Beachten Sie, dass dies ein extrem großes Projekt ist. Ich habe versucht alles so klar wie möglich zu kommentieren, aber die Vorgänge zu erklären ist nicht immer so einfach, wie es scheint. Ich weiß wie alles funktioniert in und auswendig, aber zu versuchen zu erklären, wie es funktioniert, ist eine ganz andere Geschichte :) Wenn Sie das Tutorial durchgelesen haben und eine bessere Formulierung für bestimmte Vorgänge haben oder wenn Sie denken, dass Diagramme einiges klarer machen könnten, dann senden Sie mir bitte Vorschläge. Ich wollte, dass dieses Tutorial leicht nachvollziehbar ist. Beachten Sie auch, dass dies kein Anfänger-Tutorial ist. Wenn Sie nicht die vorherigen Tutorials gelesen haben, senden Sie mir bitte keine Mails mit Fragen, bis Sie sie gelesen haben. Danke.

Jeff Molofee (NeHe)

* DOWNLOAD Visual C++ Code für diese Lektion.

* DOWNLOAD Borland C++ Builder 6 Code für diese Lektion. ( Conversion by Christian Kindahl )
* DOWNLOAD Code Warrior 5.3 Code für diese Lektion. ( Conversion by Scott Lupton )
* DOWNLOAD Delphi Code für diese Lektion. ( Conversion by Michal Tucek )
* DOWNLOAD Dev C++ Code für diese Lektion. ( Conversion by Dan )
* DOWNLOAD Euphoria Code für diese Lektion. ( Conversion by Evan Marshall )
* DOWNLOAD Irix Code für diese Lektion. ( Conversion by Dimi )
* DOWNLOAD Java Code für diese Lektion. ( Conversion by Jeff Kirby )
* DOWNLOAD LCC Win32 Code für diese Lektion. ( Conversion by Robert Wishlaw )
* DOWNLOAD Linux Code für diese Lektion. ( Conversion by Marius Andra )
* DOWNLOAD Linux/GLX Code für diese Lektion. ( Conversion by Mihael Vrbanec )
* DOWNLOAD Linux/SDL Code für diese Lektion. ( Conversion by Ti Leggett )
* DOWNLOAD LWJGL Code für diese Lektion. ( Conversion by Mark Bernard )
* DOWNLOAD Mac OS Code für diese Lektion. ( Conversion by Anthony Parker )
* DOWNLOAD Mac OS X/Cocoa Code für diese Lektion. ( Conversion by Bryan Blackburn )
* DOWNLOAD MASM Code für diese Lektion. ( Conversion by Christophe )
* DOWNLOAD Visual C++ / OpenIL Code für diese Lektion. ( Conversion by Denton Woods )
* DOWNLOAD Visual Studio .NET Code für diese Lektion. ( Conversion by Grant James )

 
Deutsche Übersetzung: Joachim Rohde
Der original Text ist hier zu finden.
Die original OpenGL Tutorials stammen von NeHe's Seite.