Lektion 32
Willkommen zu Lektion 32. Dieses Tutorial wird wahrscheinlich das umfangreichste Tutorial, dass ich bisher geschrieben habe. Über 1000 Zeile Code und mehr als 1540 Zeile HTML. Dies ist ebenfalls das erste Tutorial, dass meinen neuen NeHeGL Basecode verwendet. Dieses Tutorial hat eine ganze Zeit gebraucht, bis es geschrieben war, aber ich denke es war die Zeit des Wartens wert. Einige Themen, die ich in diesem Tutorial abdecke sind: Alpha Blending, Alpha Testing, Verwenden der Maus, die Verwendung von Ortho und Perspective zur selben Zeit, das Anzeigen eines eigenen Cursors, manuelles Sortieren von Objekten nach Tiefe, Frames einer einzelnen Textur animieren und das wichtigste, Sie werden alles über PICKING lernen!
Die original Version dieses Tutorials zeigte drei Objekte auf dem Screen an, die ihre Farbe änderten, wenn man sie anklickte. Wie aufregend ist das!?! Überhaupt nicht! Wie immer wollte ich euch mit einem super coolem Tutorial beeindrucken. Ich wollte das Tutorial aufregend, vollgepackt mit Informationen und selbstverständlich... gut aussehend machen. So, nach ein paar Wochen des programmierens, ist das Tutorial fertig. Selbst wenn Sie nicht programmieren, werden Sie dieses Tutorial vielleicht geniessen. Es ist ein komplettes Spiel. Es geht in diesem Spiel darum so viele Ziele abzuschießen, bis Sie ein schlechtes Gewissen bekommen oder Ihre Hand verkrampft und Sie nicht weiter den Mausbutton drücken können.
Ich bin sicher, dass es ein paar Kritiken geben wird, aber ich bin sehr zufrieden mit diesem Tutorial! Ich habe langweilige Themen wie Picking und Tiefensortierung von Objekten gewählt und diese mit Spaß verbunden!
Einige kurze Anmerkungen über den Code. Ich werde nur den Code aus lesson32.cpp hier abhandeln. Es gab ein paar kleine Änderungen im NeHeGL Code. Die wichtigste Änderung ist, dass ich Maus-Unterstützung der WindowProc() hinzugefügt habe. Ich habe ebenfalls int mouse_x, mouse_y zur Speicherung der Mausbewegung hinzugefügt. In NeHeGL.h wurden die folgenden zwei Codezeilen hinzugefügt: extern int mouse_x; & extern int mouse_y;
Die Texturen, die in diesem Tutorial verwendet werden, wurden mit Adobe Photoshop gemacht. Jede .TGA Datei ist ein 32 Bit Image mit einem Alpha-Kanal. Wenn Sie nicht sicher sind, wie Sie selbst einen Alpha-Kanal einem Bild hinzufügen, kaufen Sie sich ein gutes Buch, durchsuchen Sie das Netz oder lesen Sie in der Hilfe von Adobe Photoshop nach. Der gesamte Prozess ist ähnlich dem, den ich beim erzeugen der Masken im Masken-Tutorial verwendet habe. Laden Sie ihre Objekte in Adobe Photoshop (oder einem anderen Grafikprogramm, welches den Alpha-Kanal unterstützt). Benutzen Sie Auswahl durch Farbbereich (Select by Color Range), um die Fläche um Ihr Objekt herum zu markieren. Kopieren Sie diesen Bereich. Erzeugen Sie ein neues Bild. Fügen Sie das Kopierte in das neue Bild ein. Negieren Sie das Bild, so dass der Bereich, wo ihr Bild sein sollte, schwarz ist. Machen Sie die Flächen drumherum weiß. Markieren Sie das gesamte Bild und kopieren Sie es. Gehen Sie zurück zum original Bild und erzeugen Sie einen Alpha-Kanal. Fügen Sie die Schwarz-Weiß Maske, die Sie gerade erzeugt haben, in den Alpha-Kanal ein. Speichern Sie das Bild als 32 Bit .TGA Datei. Stellen Sie sicher, dass die Option 'Preserve Transperency' (~behalte Transparenz bei) ausgewählt ist und dass Sie die Datei unkomprimiert speichern!
Wie immer hoffe ich, dass Sie das Tutorial genießen. Ich bin daran interessiert, was Sie darüber denken. Wenn Sie irgendwelche Fragen haben oder Fehler finden, lassen Sie es mich wissen. Durch einige Teile des Tutorials bin ich recht zügig durchgegangen, wenn Sie also einen Teil recht schwer verstehen, senden Sie mir eine E-Mail und ich werde versuchen, diese detaillierter oder anders zu erklären!
#include <windows.h> // Header Datei für Windows
#include <stdio.h> // Header Datei für Standard Input / Output
#include <stdarg.h> // Header Datei für Variable 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 <time.h> // für zufällige Wertebereiche
#include "NeHeGL.h" // Header Datei für NeHeGL
In Lektion 1 habe ich gepredigt, wie man die OpenGL Libraries korrekt linkt. In Visual C++ auf Projekt klicken, Einstellungen und dann den Linker-Reiter auswählen. Runter zu den Objekt/Library Modulen gehen und OpenGL32.lib, GLu32.lib sowie GLaux.lib hinzufügen. Fügt man eine benötigte Library nicht hinzu, wird der Compiler ein Fehler nach dem anderen ausspucken. Etwas was Sie nicht wollen! Um die Umstände noch zu verschlimmern: wenn Sie die Libraries nur im Debug-Modus einbinden und irgendwer versucht, den Code im Release-Modus zu übersetzen... noch mehr Fehler. Viele Leute schauen sich fremden Code an. Die meisten sind Programmierneulinge. Sie nehmen sich Ihren Code und versuchen ihn zu kompilieren. Wenn sie Fehler erhalten, löschen sie den Code und suchen weiter.
Der folgende Code teilt dem Compiler mit, die benötigte Libraries zu linken. Etwas mehr Tipparbeit, aber wesentlich weniger Kopfschmerzen auf lange Sicht. Für dieses Tutorial müssen wir die Libraries OpenGL32, Glu32 und WinMM (zum Sound abspielen) linken. In diesem Tutorial werden wir .TGA Dateien laden, weshalb wir auf die GLaux Library verzichten können.
#pragma comment( lib, "opengl32.lib" ) // suche nach OpenGL32.lib während des Linkens
#pragma comment( lib, "glu32.lib" ) // suche nach GLu32.lib während des Linkens
#pragma comment( lib, "winmm.lib" ) // suche nach der WinMM Library während des Linkens
Die folgenden 3 Zeilen überprüfen, ob CDS_FULLSCRFEEN vom Compiler bereits definiert wurde. Wenn es nicht definiert wurde, weisen wir CDS_FULLSCREEN den Wert 4 zu. Für die, die gerade nichts verstehen... einige Compiler weisen CDS_FULLSCREEN keinen Wert zu und würden einen Fehler melden, wenn CDS_FULLSCREEN verwendet werden würde! Um eine Fehlermeldung zu vermeiden, überprüfen wir, ob CDS_FULLSCREEN bereits definiert wurde und wenn nicht, definieren wir es manuell. Das macht das Leben für jeden einfacher.
Wir deklarieren dann DrawTargets und setzen die Variablen für unser Fenster- und Tastatur-Handling. Wenn Sie Deklarationen nicht verstehen, lesen Sie sich durch das MSDN. Behalten Sie im Hinterkopf, dass ich niemandem C/C++ beibringe, kaufen Sie sich ein gutes Buch, wenn Sie Hilfe mit dem Code benötigen, der KEIN GL Code ist!
#ifndef CDS_FULLSCREEN // CDS_FULLSCREEN ist nicht definiert von
#define CDS_FULLSCREEN 4 // einigen Compilern. Indem wir es hier deklarieren
#endif // können wir Fehler vermeiden
void DrawTargets(); // Deklaration
GL_Window* g_window;
Keys* g_keys;
Im folgenden Codeabschnitt definieren wir unsere benutzerdefinierten Variablen. base wird für unsere Font Display Listen verwendet. roll wird dazu verwendet den Hintergrund zu bewegen und die Illusion zu vermitteln, dass sich die Wolken bewegen. level sollte klar sein (wir fangen bei Level 1 an). miss enthält die Anzahl der verfehlten Objekte. Außerdem verwenden wir sie, um die Moral des Spielers anzuzeigen (keine Verfehlungen bedeutet eine hohe Moral). kills enthält die Anzahl der getroffenen Ziele in jedem Level. score enthält die Gesamtanzahl der getroffenen Objekte und game wird dazu verwendet, um ein Game Over zu signalisieren!
Die letzte Zeile lässt uns Strukturen an unsere Vergleichs-Funktion übergeben. Die qsort Routine erwartet den letzten Parameter vom Typen type (const *void, const *void).
// benutzerdefinierte Variablen
GLuint base; // Font Display Liste
GLfloat roll; // vorbeiziehende Wolken
GLint level=1; // aktuelles Level
GLint miss; // Verfehlte Ziele
GLint kills; // Anzahl der Kills im Level
GLint score; // aktueller Score
bool game; // Game Over?
typedef int (*compfn)(const void*, const void*); // Typedef für unsere Vergleichs-Funktion
Nun zu unserer Objekte-Struktur. Diese Struktur enthält alle Informationen über ein Objekt. Die Richtung, in die es rotiert, ob es getroffen wurde, seine Koordinaten auf dem Screen, etc.
Ein Schnelldurchlauf, durch die Variablen... rot spezifiziert die Richtung, in der wir das Objekt rotieren lassen wollen. hit wird gleich FALSE sein, wenn es noch nicht getroffen wurde. Wenn das Objekt getroffen wurde oder das Flag manuell gesetzt wird, als ob es getroffen wurde, ist der Wert von hit gleich TRUE.
Die Variable frame wird benutzt, um durch die Frames der Animation unserer Explosion zu iterieren. Wenn frame inkrementiert wird, ändert sich die Explosions-Textur. Mehr dazu später in diesem Tutorial.
Um zu verfolgen in welche Richtung unser Objekt sich bewegt, haben wir eine Variable namens dir. dir kann einen von 4 Werten haben: 0 - Objekt bewegt sich nach links, 1 - Objekt bewegt sich nach rechts, 2 - Objekt bewegt sich nach oben und zu guter Letzt 3 - Objekt bewegt sich nach unten.
texid kann eine Nummer zwischen 0 und 4 sein. Null repräsentiert die BlueFace Textur, 1 ist die Bucket-Textur, 2 die Target (Ziel) Textur, 3 ist Coladosen-Textur und 4 ist die Vase Textur. Später im Textur-Lade-Code werden Sie sehen, dass die ersten 5 Texturen die Ziel-Bilder sind.
X und Y werden dazu verwendet, das Objekt auf dem Screen zu positionieren. X repräsentiert den Wert wo sich das Objekt auf der X-Achse befindet und Y den Y-Achsen-Wert.
Die Objekte rotieren auf der Z-Achse, basierend auf dem Wert von spin. Später im Code werden wir spin inkrementieren oder dekrementieren, basierend auf der Richtung, in die sich das Objekt bewegt.
Zum Schluss haben wir noch distance welche uns mitteilt, wie tief hinein im Screen sich das Objekt befindet. distance ist eine extrem wichtige Variable, die wir dazu verwenden werden, um die linke und rechte Seite des Screens zu berechnen und um die Objekte zu sortieren, so dass die Objekte in der Ferne vor dem Objekten die nicht so weit weg sind, gezeichnet werden.
struct objects {
GLuint rot; // Rotation (0-Keine, 1-im Uhrzeigersinn, 2-gegen den Uhrzeigersinn)
bool hit; // Objekt getroffen?
GLuint frame; // aktueller Explosions Frame
GLuint dir; // Objekt Richtung (0-links, 1-rechts, 2-hoch, 3-runter)
GLuint texid; // Objekt Textur ID
GLfloat x; // Objekt X Position
GLfloat y; // Objekt Y Position
GLfloat spin; // Objekt Spin
GLfloat distance; // Objekt Entfernung
};
Eigentlich ist es nicht nötig, den folgenden Code zu erklären. Wir laden in diesem Tutorial TGA-Bilder anstatt von Bitmaps. Die folgende Struktur wird dazu verwendet, die Bild-Daten zu speichern, sowie die Informationen über das TGA-Bild. Lesen Sie das Tutorial über das Laden von TGA-Dateien, wenn Sie hier detailiertere Erklärungen zum folgenden Code benötigen.
typedef struct // erzeuge eine Struktur
{
GLubyte *imageData; // Bild Daten (bis zu 32 Bits)
GLuint bpp; // Bild Farbtiefe in Bits Pro Pixel.
GLuint width; // Bildbreite
GLuint height; // Bildhöhe
GLuint texID; // Textur ID die verwendet wir, um eine Textur auszuwählen
} TextureImage; // Struktur Name
Der folgende Code reserviert Speicherplatz für unsere 10 Texturen und 30 Objekte. Wenn Sie gedenken mehr Objekte im Spiel zu verwenden, stellen Sie sicher, dass Sie den Wert von 30 auf die gewünschte Anzahl der Objekte erhöhen.
TextureImage textures[10]; // Speicherplatz für 10 Texturen
objects object[30]; // Speicherplatz für 30 Objekte
Ich möchte die Größe eines jeden Objektes nicht beschränken. Ich wollte die Vase größer als die Dose und ich wollte den Eimer breiter als die Vase haben. Um es uns einfach zu machen, habe ich eine Struktur erzeugt, die die Objektbreite (w) und -höhe (h) enthält.
Dann setze ich die Breite und Höhe von jedem Objekt in der letzten Zeile Code. Um die Cokedosen-Breite zu erhalten, würde ich size[3].w ansprechen. Das Blueface ist 0, der Eimer 1 und das Ziel 2, etc. Die Breite wird durch w repräsentiert. Verstanden?
struct dimensions { // Objekt Dimensionen
GLfloat w; // Objekt Breite
GLfloat h; // Objekt Höhe
};
// Größe von jedem Objekt: Blueface, Eimer, Ziel, Coke, Vase
dimensions size[5] = { {1.0f,1.0f}, {1.0f,1.0f}, {1.0f,1.0f}, {0.5f,1.0f}, {0.75f,1.5f} };
Der folgende große Codeabschnitt lädt unsere TGA-Bilder und konvertiert diese in Texturen. Es ist der selbe Code, den ich in Lektion 25 verwendet habe, wenn Sie also genauere Beschreibungen benötigen, gehen Sie zurück zu Lektion 25 und lesen diese nochmal.
Ich habe TGA-Bilder verwendet, weil diese einen Alpha-Kanal haben können. Der Alpha-Kanal teilt OpenGL mit, welche Teile des Bildes transparent sind und welche undurchsichtig. Der Alpha-Kanal wird in einem Grafik-Programm erzeugt und innerhalb des .TGA-Bildes gespeichert. OpenGL lädt das Bild und benutzt den Alpha-Kanal, um die Menge der Transparenz für jeden Pixel im Bild zu setzen.
bool LoadTGA(TextureImage *texture, char *filename) // Lädt eine TGA Datei in den Speicher
{
GLubyte TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0}; // unkomprimierter TGA Header
GLubyte TGAcompare[12]; // wird verwendet um den TGA Header zu vergleichen
GLubyte header[6]; // die ersten 6 nützlichen Bytes vom Header
GLuint bytesPerPixel; // enthält die Anzahl der Bytes Pro Pixel die in der TGA Datei verwendet werden
GLuint imageSize; // wird verwendet, um die Bildgröße zu speichern, wenn Speicher reserviert wird
GLuint temp; // Temporäre Variable
GLuint type=GL_RGBA; // Setze den Standard GL Modus auf RBGA (32 BPP)
FILE *file = fopen(filename, "rb"); // öffne die TGA Datei
if( file==NULL || // Existiert die Datei überhaupt?
fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) || // gibt es 12 Bytes die wir lesen können?
memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0 || // stimmt der Header mit dem überein, was wir haben wollen?
fread(header,1,sizeof(header),file)!=sizeof(header)) // wenn ja, lese die nächsten 6 Header Bytes ein
{
if (file == NULL) // existiert die Datei überhaupt hat? *hinzugefügt von Jim Strong*
return FALSE; // gebe False zurück
else // ansonsten
{
fclose(file); // wenn irgendwas fehl schlug, schließe die Datei
return FALSE; // gebe False zurück
}
}
texture->width = header[1] * 256 + header[0]; // bestimme die TGA Breite (highbyte*256+lowbyte)
texture->height = header[3] * 256 + header[2]; // bestimme die TGA Höhe (highbyte*256+lowbyte)
if( texture->width <=0 || // Ist die Breite kleiner oder gleich Null
texture->height <=0 || // Ist die Höhe kleiner oder gleich null
(header[4]!=24 && header[4]!=32)) // Ist das TGA 24 oder 32 Bit?
{
fclose(file); // wenn irgendwas fehl schlug, schließe die Datei
return FALSE; // gebe False zurück
}
texture->bpp = header[4]; // ermittle die TGA's Bits Pro Pixel (24 oder 32)
bytesPerPixel = texture->bpp/8; // Dividiere durch 8 um die Anzahl der Bytes pro Pixel zu erhalten
imageSize = texture->width*texture->height*bytesPerPixel; // berechne den benötigten Speicher für die TGA Daten
texture->imageData=(GLubyte *)malloc(imageSize); // Reserviere Speicher, um die TGA Daten aufzunehmen
if( texture->imageData==NULL || // existiert der Speicherplatz?
fread(texture->imageData, 1, imageSize, file)!=imageSize) // stimmt die Bildgröße mit der Größe des reservierten Speichers überein?
{
if(texture->imageData!=NULL) // wurden die Image Daten geladen
free(texture->imageData); // wenn ja, gebe die Bild Daten frei
fclose(file); // schließe die Datei
return FALSE; // gebe False zurück
}
for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel) // iteriere durch die Bilddaten
{ // tausche die ersten und dritten Bytes ('R'ot und 'B'lau)
temp=texture->imageData[i]; // Speichere temporär den i-ten Wert in den Bild-Daten
texture->imageData[i] = texture->imageData[i + 2]; // Setze das erste Byte gleich dem Wert des dritten Bytes
texture->imageData[i + 2] = temp; // Setze das dritte Byte gleich dem Wert aus 'temp' (Wert des ersten Bytes)
}
fclose (file); // Schließe die Datei
// erzeuge eine Textur aus den Daten
glGenTextures(1, &texture[0].texID); // Generiere die OpenGL Textur IDs
glBindTexture(GL_TEXTURE_2D, texture[0].texID); // Binde unsere Textur
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // Linear gefiltert
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Linear gefiltert
if (texture[0].bpp==24) // Ist das TGA 24 Bits
{
type=GL_RGB; // wenn ja, setze 'type' auf GL_RGB
}
glTexImage2D(GL_TEXTURE_2D, 0, type, texture[0].width, texture[0].height, 0, type, GL_UNSIGNED_BYTE, texture[0].imageData);
return true; // Textur Erzeugung war OK, gebe True zurück
}
Der 2D Textur-Font-Code ist der selbe Code den ich auch im vorherigen Tutorial verwendet habe. Wie dem auch sei, gibt es dennoch ein paar kleinere Änderungen. Was Sie bemerken werden, ist, dass wir nur noch 95 Display Listen erzeugen. Wenn Sie sich die Font-Textur anschauen, werden Sie sehen, dass es nur 95 Zeichen inklusive des Leerzeichens oben links im Bild gibt. Als zweites werden Sie bemerken, dass wir bei cx durch 16.0f dividieren und für cy dividieren wir nur durch 8.0f. Der Grund dafür ist, dass die Font-Textur 256 Pixel breit ist aber nur halb so hoch (128 Pixel). Deshalb dividieren wir bei der Berechnung von cx durch 16.0f und bei cy dividieren wir durch die Hälfte (8.0f).
Wenn Sie den folgenden Code nicht verstehen, gehen Sie zurück zu Lektion 17 und lesen Sie diese. Der Font-Erzeugungs-Code wurde in Lektion 17 im Detail erklärt!
GLvoid BuildFont(GLvoid) // erzeuge unsere Font Display Liste
{
base=glGenLists(95); // erzeuge 95 Display Listen
glBindTexture(GL_TEXTURE_2D, textures[9].texID); // Binde unsere Font Textur
for (int loop=0; loop// iteriere durch alle 95 Listen
{
float cx=float(loop%16)/16.0f; // X Position des aktuellen Zeichens
float cy=float(loop/16)/8.0f; // Y Position des aktuellen Zeichens
glNewList(base+loop,GL_COMPILE); // fange an eine Liste zu erzeugen
glBegin(GL_QUADS); // benutze einen Quad für jedes Zeichen
glTexCoord2f(cx, 1.0f-cy-0.120f); glVertex2i(0,0); // Textur / Vertex Coord (unten links)
glTexCoord2f(cx+0.0625f, 1.0f-cy-0.120f); glVertex2i(16,0); // Texutr / Vertex Coord (unten rechts)
glTexCoord2f(cx+0.0625f, 1.0f-cy); glVertex2i(16,16); // Textur / Vertex Coord (oben rechts)
glTexCoord2f(cx, 1.0f-cy); glVertex2i(0,16); // Textur / Vertex Coord (oben links)
glEnd(); // wir sind fertig mit dem Erzeugen unseres Quad (Zeichens)
glTranslated(10,0,0); // gehe auf die rechte Seite des Zeichens
glEndList(); // fertig mit dem Erzeugen er Display Liste
} // Schleife die solange durchläuft, bis alle 256 erzeugt wurden
}
Der Ausgabe-Code ist der selbe Code wie in Lektion 17, wurde allerdings etwas geändert, um den Score, Level und die Moral auf dem Screen auszugeben (Variablen, die sich kontinuierlich ändern).
GLvoid glPrint(GLint x, GLint y, const char *string, ...) // Hier passiert die Ausgabe
{
char text[256]; // enthält unseren String
va_list ap; // Zeiger auf die Argumentenliste
if (string == NULL) // Wenn es da keinen Text gibt
return; // mache nichts
va_start(ap, string); // Parse den String nach Variablen
vsprintf(text, string, ap); // und konvertiere Symbole in die eigentlichen Zahlen
va_end(ap); // Ergebnisse werden im Text gespeichert
glBindTexture(GL_TEXTURE_2D, textures[9].texID); // wähle unsere Font Textur aus
glPushMatrix(); // speicher die Modelview Matrix
glLoadIdentity(); // Resette die Modelview Matrix
glTranslated(x,y,0); // Positioniere den Text(0,0 - unten links)
glListBase(base-32); // wähle den Font-Satz
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // zeichnet den Display Listen Text
glPopMatrix(); // stelle die alte Projektions Matrix wieder her
}
Dieser Code wird später im Programm von qsort aufgerufen. Er vergleicht den Abstand in zwei Strukturen und gibt -1 zurück, wenn die Distanz der ersten Struktur kleiner als die Distanz der zweiten Struktur war und 1 wenn die Distanz der ersten Struktur größer als die Distanz der zweiten Struktur ist und 0 wenn die Distanz in beiden Strukturen gleich ist.
int Compare(struct objects *elem1, struct objects *elem2) // Vergleichs Funktion *** MSDN CODE MODIFIZIERT FÜR DIESES TUT ***
{
if ( elem1->distance <elem2->distance) // wenn die Distanz der ersten Struktur kleiner als die zweite ist
return -1; // gebe -1 zurück
else if (elem1->distance > elem2->distance) // wenn die Distanz der ersten Struktur größer als die zweite ist
return 1; // gebe 1 zurück
else // Ansonsten (wenn die Distanz gleich ist)
return 0; // gebe 0 zurück
}
Im InitObject() Code wird jedes Objekt initalisiert. Wir fangen damit an rot auf 1 zu setzen. Damit rotiert das Objekt mit dem Uhrzeigersinn. Dann setzen wir die Explosions-Animation auf Frame 0 (wir wollen ja nicht, dass die Explosion mitten in der Animation anfängt). Als nächstes setzen wir hit auf FALSE, was bedeutet, dass das Objekt noch nicht getroffen wurde. Um eine Objekt-Textur auszuwählen, weisen wir texid einen zufälligen Wert von 0 bis 4 zu. Null ist die Blueface Textur und 4 die Vasen Textur. Damit erhalten wir eins von 5 zufälligen Objekten.
Die Variable distance erhält einen zufälligen Wert zwischen -0.0f und -40.0f (4000/100 ist gleich 40). Wenn wir das Objekt tatsächlich zeichnen, translatieren wir um weitere 10 Einheiten in den Screen hinein. Wenn die Objekte also gezeichnet werden, werden sie zwischen -10.0f und -50.0f Einheiten in den Screen hinein gezeichnet (nicht zu nah und nich zu weit weg). Ich dividiere die zufällige Zahl durch 100.0f um einen genaueren Fließkomma-Wert zu erhalten.
Nachdem wir eine zufällige Distanz zugewiesen haben, geben wir dem Objekt einen zufälligen Y-Wert. Wir wollen das Objekt nicht tiefer als -1.5f haben, ansonsten sind wir unter dem GRund und wir wollen das Objekt nicht höher als 3.0f haben. Um also in diesem Bereich zu bleiben, kann die Zahl nicht höher als 4.5f sein (-1.5f+4.5f=3.0f).
Um die X-Position zu berechnen benutzen wir etwas trickreiche Mathematik. Wir nehmen unsere Distanz und wir subtrahieren davon 15.0f. Dann dividieren wir das Ergebniss durch 2 und subtrahieren 5*level. Und zu guter Letzt subtrahieren wir einen zufälligen Wert zwischen 0.0f und 5 multpliziert mit dem aktuellen Level. Wir subtrahieren das 5*level und den zufälligen Wert zwischen 0.0f und 5*level, so dass unser Objekt weiter weg erscheint, in höheren Level. Wenn wir das nicht machen würden, würden die Objekte nach und nach erscheinen, was es noch schwieriger machen würde, alle Ziele zu treffen, als es ohnehin schon ist.
Zu guter Letzt wählen wir eine zufällige Richtung (dir), die entweder 0 (links) oder 1 (rechts) ist.
Um die Dinge verständlicher zu machen, gibt es nun ein kleines Beispiel zur Berechnung der x-Position. Sagen wir unsere Distanz ist -30.0f und das aktuelle Level ist 1: object[num].x=((-30.0f-15.0f)/2.0f)-(5*1)-float(rand()%(5*1));
object[num].x=(-45.0f/2.0f)-5-float(rand()%5);
object[num].x=(-22.5f)-5-{lets say 3.0f};
object[num].x=(-22.5f)-5-{3.0f};
object[num].x=-27.5f-{3.0f};
object[num].x=-30.5f;
Nun denken Sie daran, dass wir uns 10 Einheiten in den Screen hinein bewegen, bevor wir unsere Objekte zeichnen und die Distanz im obigen Beispiel ist -30.0f. Man kann sagen, dass die eigentliche Distanz in den Screen hinein -40.0f sein wird. Indem wir den Perspektiven-Code aus der NeHeGL.cpp Datei verwenden, ist es sicher anzunehmen, dass die Distanz -40.0f ist, die äußerste linke Ecke wird -20.0f sein und die äußerste rechte wird +20.0f sein. Im obigen Code ist unser X-Wert gleich -22.5f (was gerade außerhalb des Screens ist). Wir subtrahieren dann 5 und unser zufälliger Wert von 3 garantiert, dass das Objekt außerhalb des Screens (bei -30.5f) startet, was bedeuten würde, dass sich das Objekt ungefähr 8 Einheiten nach rechts bewegen müsste, bevor es auf dem Screen erscheinen würde.
GLvoid InitObject(int num) // Initialisiere ein Objekt
{
object[num].rot=1; // Rotation im Uhrzeigersinn
object[num].frame=0; // Resette den Explosions Frame auf null
object[num].hit=FALSE; // Resette den Objekt-wurde-getroffen-Status auf FALSE
object[num].texid=rand()%5; // weise eine neue Textur zu
object[num].distance=-(float(rand()%4001)/100.0f); // zufällige Distanz
object[num].y=-1.5f+(float(rand()%451)/100.0f); // zufällige Y Position
// zufällige Start-X-Position basierend auf der Distanz des Objekts und einem zufälligen Verzögerungswert (positiver Wert)
object[num].x=((object[num].distance-15.0f)/2.0f)-(5*level)-float(rand()%(5*level));
object[num].dir=(rand()%2); // wähle eine zufällige Richtung
Nun überprüfen wir, in welche Richtung sich das Objekt bewegen wird. Der folgende Code überprüft, ob sich das Objekt nach links bewegt. Wenn dem so ist, müssen wir die Rotation ändern, so dass das Objekt gegen den Uhrzeigersinn rotiert. Wir machen das, indem wir den Wert von rot auf 2 ändern.
Unser X-Wert ist standardmäßig ein negativer Wert. Wie dem auch sei, die rechte Seite des Screens wäre ein positiver Wert. Als letztes negieren wir den aktuellen X-Wert. Wir machen also aus dem X-Wert einen positiven Wert anstatt eines negativen Wertes.
if (object[num].dir==0) // ist die zufällige Richtung rechts
{
object[num].rot=2; // Rotations gegen den Uhrzeigersinn
object[num].x=-object[num].x; // fange auf der linken Seite an (negativer Wert)
}
Nun überprüfen wir texid um herauszufinden, welches Objekt der Computer zufällig ausgewählt hat. Wenn texid gleich 0 ist, hat der Computer das Blueface Objekt ausgewählt. Die Blueface-Objekte rollen über den Boden. Um sicher zu gehen, dass diese auch auf dem Boden starten, setzen wir den Y-Wert manuell auf -2.0f.
if (object[num].texid==0) // Blue Face
object[num].y=-2.0f; // rollen immer über den Boden
Als nächstes überprüfen wir, ob texid gleich 1 ist. Wenn ja, hat der Computer den Eimer ausgewählt. Der Eimer wandert nicht von links nach rechts, sondern fällt vom Himmel. Als erstes setzen wir dir also auf 3. Damit teilen wir dem Computer mit, dass unser Eimer herunterfällt respektive sich nach unten bewegt.
Unser Initialisierungs-Code nimmt an, dass sich das Objekt von links nach rechts bewegt. Da der Eimer herunterfällt, müssen wir ihm einen neuen zufälligen X-Wert zuweisen. Wenn wir das nicht machen würden, würde der Eimer nie sichtbar werden. Er würde entweder zuweit links des Screens oder zuweit rechts vom Screen herunterfallen. Um einen neuen Wert zuzuweisen, wählen wir einen zufälligen Wert basierend auf der Distanz in den Screen hinein. Anstatt 15 zu subtrahieren, subtrahieren wir lediglich 10. Damit erhalten wir eine etwas kleinere Spanne und somit bleibt das Objekt AUF dem Screen, anstatt außerhalb des Screens. Angenommen unsere Distanz wäre -30.0f, würden wir einen zufälligen Wert zwischen 0.0f und 40.0f erhalten. Wenn Sie sich selbst fragen, warum zwischen 0.0f und 40.0f? Sollte es nicht zwischen 0.0f und -40.0f sein? Die Antwort darauf ist einfach. Die rand() Funktion gibt immer eine positive Zahl zurück. Egal welche Zahl wir also erhalten, es wird auf jeden Fall ein positiver Wert sein. Wie dem auch sei... zurück zum Thema. Wir haben also eine positive Zahl zwischen 0.0f und 40.0f. Wir addieren die Distanz (einen negativen Wert) minus 10.0f dividiert durch 2. Als ein Beispiel... angenommen der zufällig zurückgegebene Wert ist 14 und die Distanz ist -30.0f:
object[num].x=float(rand()%int(-30.0f-10.0f))+((-30.0f-10.0f)/2.0f);
object[num].x=float(rand()%int(-40.0f)+(-40.0f)/2.0f);
object[num].x=float(15 {angenommen 15 wurde zurückgegeben))+(-20.0f);
object[num].x=15.0f-20.0f;
object[num].x=-5.0f;
Als letztes müssen wir den Y-Wert setzen. Wir wollen, dass der Eimer vom Himmel fällt. Wir wollen allerdings nicht, dass er durch die Wolken fällt. Deshalb setzen wir den Y-Wert auf 4.5f. Etwas unterhalb der Wolken.
if (object[num].texid==1) // Eimer
{
object[num].dir=3; // fällt herunter
object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f);
object[num].y=4.5f; // zufälliges X, fängt oben im Screen an
}
Wir wollen, dass das Ziel aus dem Boden herauskommt und in die Luft geht. Wir überprüfen, ob das Objekt wirklich ein Ziel ist (texid gleich 2). Wenn dem so ist, setzen wir die Richtung (dir) auf 2 (hoch). Wir benutzen exakt den selben Code wie oben, um ein zufälliges X zu erhalten.
Wir wollen nicht, dass das Ziel überhalb des Bodens startet. Deshalb setzen wir den Y-Anfangs-Wert auf -3.0f (unterhalb des Bodens). Wir subtrahieren einen zufälligen Wert zwischen 0.0f und 5, multipliziert mi dem aktuellen Level. Wir machen das, damit das Ziel nicht UNVERZÜGLICH erscheint. In späteren Levels wollen wir eine Verzögerung haben, bevor das Ziel erscheint. Ohne Verzögerung, würden die Ziele eins nach dem anderen direkt erscheinen, was einem recht wenig Zeit gibt, sie zu treffen.
if (object[num].texid==2) // Ziel
{
object[num].dir=2; // fange an nach oben zu fliegen
object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f);
object[num].y=-3.0f-float(rand()%(5*level)); // zufälliges X, fange unterhalb des Bodens an + zufälliger Wert
}
Alle anderen Objekte wandern von links nach rechts, weshalb es nicht nötig ist, den übrigen Objekten neue Werte zuzuweisen. Diese sollten mit den zufällig zugewiesenen Werte funktionieren.
Nun zum spaßigen Teil! "Damit die Alpha-Blending-Technik korrekt funktioniert, müssen die transparenten Primitive von hinten nach vorne gezeichnet werden und dürfen sich nicht schneiden". Wenn Alpha-geblendete Objekte gezeichnet werden, ist es sehr wichtig, dass Objekte in der Ferne als erstes gezeichnet werden und die Objekte in der Nähe als letztes.
Der Grund dafür ist einfach... Der Z Bufffer hindert OpenGL daran Pixel zu zeichnen die hinter anderen Dingen sind, die bereits gezeichnet wurden. Daraus resultiert, dass Objekte die hinter transparenten Objekten gezeichnet werden, nicht gezeigt werden. Was Sie sehen würden, wäre eine quadratische Form über den überlappenden Objekten... nicht wirklich schön!
Wir kennen bereits die Tiefe von jedem Objekt. Nachdem wir das neue Objekt initialisiert haben, können wir das Problem umgehen, indem wir die Objekte mittels der qsort Funktion (QuickSort) sortieren. Dadurch das wir die Objekte sortieren, können wir sicher sein, dass das Objekt das als erstes gezeichnet wird, das am weitesten entfernteste Objekt ist. Auf diese Weise zeichnen wir die Objekte, beginnend mit dem ersten Objekt, die Objekte in der Ferne werden als erstes gezeichnet. Objekte die näher sind (später gezeichnet werden) werden die vorherigen gezeichneten Objekte hinter sich selbst sehen und somit korrekt blenden!
Wie in der Kommentierung angemerkt habe ich diesen Code im MSDN gefunden, nachdem ich stundenlang nach einer Lösung für dieses Problem geuscht habe. Sie arbeitet ganz gut und erlaubt es Ihnen, die gesamten Strukturen zu sortieren. qsort erwartet 4 Parameter. Der erste Parameter zeigt auf das Objekt Array (das zu sortierende Array). Der zweite Parameter ist die Anzahl der Arrays, die wir sortieren wollen... natürlich wollen wir alle Objekte die gerade angezeigt werden sortieren (was level entspricht). Der dritte Parameter spezifiziert die Größe unserer Objekt-Struktur und der vierte Parameter zeigt auf unsere Compare()-Funktion.
Es gibt wahrscheinlich eine bessere Methode um die Strukturen zu sortieren, aber qsort() erledigt die Arbeit auch... es ist schnell, bequem und einfach zu benutzen!
Es ist wichtig anzumerken, dass, wenn Sie glAlphaFunc() und glEnable(GL_ALPHA_TEST) verwenden wollen, eine Sortierung nicht nötig ist. Wie dem auch sei, wenn Sie die Alpha-Funktion verwenden, ist es Ihnen nur erlaubt entweder komplette transparent oder komplette Undurchsichtigkeit zu verwenden, es gibt nichts dazwischen. Sortierung und die Verwendung der Blendfunc() ist etwas mehr Arbeit, aber erlaubt auch Semi-Transparente Objekte.
// Sortier Objekte nach Distanz: Anfangsadresse unseres Objekt-Arrays *** MSDN CODE MODIFIZIERT FÜR DIESES TUT ***
// Anzahl der zu sortierenden Elemente
// Größe eines Elements
// Zeiger auf unsere Vergleichs-Funktion
qsort((void *) &object, level, sizeof(struct objects), (compfn)Compare );
}
Der Init-Code ist der Selbe wie immer. Die ersten beiden Zeilen holen Informationen über unser Fenster und unserer Keyboard-Handler. Wir benutzen dann srand(), um mehr Zufall in das Spiel, basierend auf der Zeit, hineinzubringen. Danach laden wir unsere TGA Bilder und konvertieren sie in Texturen, mittels LoadTGA(). Die ersten 5 Bilder sind Objekte die über unseren Screen wandern. Explode ist unsere Explosions Animation, ground und sky bilden die Hintergrundszene, crosshair ist das Fadenkreuz das Sie auf dem Screen sehen werden und welches die Maus-Position repräsentiert und zu guter Letzt ist im font Bild der Font zu finden, den wir verwenden, um Score, Titel und Moral anzuzeigen. Wenn eins der Bilder nicht geladen werden konnte, wird FALSE zurückgegeben und das Programm wird beendet. Es ist wichtig anzumerken, dass dieser Base-Code keine INIT FAILED Fehlernachricht zurückgibt.
BOOL Initialize (GL_Window* window, Keys* keys) // Jegliche OpenGL Initialisierung kommt hier hin
{
g_window = window;
g_keys = keys;
srand( (unsigned)time( NULL ) ); // füge den Dingen etwas Zufall hinzu
if ((!LoadTGA(&textures[0],"Data/BlueFace.tga")) || // Lade die BlueFace Texture
(!LoadTGA(&textures[1],"Data/Bucket.tga")) || // Lade die Eimer Textur
(!LoadTGA(&textures[2],"Data/Target.tga")) || // Lade die Ziel Textur
(!LoadTGA(&textures[3],"Data/Coke.tga")) || // Lade die Coke Textur
(!LoadTGA(&textures[4],"Data/Vase.tga")) || // Lade die Vase Textur
(!LoadTGA(&textures[5],"Data/Explode.tga")) || // Lade die Explosion Textur
(!LoadTGA(&textures[6],"Data/Ground.tga")) || // Lade die Boden Textur
(!LoadTGA(&textures[7],"Data/Sky.tga")) || // Lade die Himmels Textur
(!LoadTGA(&textures[8],"Data/Crosshair.tga")) || // Lade die Fadenkreuz Textur
(!LoadTGA(&textures[9],"Data/Font.tga"))) // Lade die Font Textur
{
return FALSE; // wenn das Laden fehl schlug, gebe False zurück
}
Wenn alle Bilder geladen wurden und erfolgreich in Texturen umgewandelt wurden, können wir mit der Initialisierung fortfahren. Die Font-Textur wurde geladen, weshalb wir nun unseren Font erzeugen können. Wir machen das, indem wir den BuildFont()-Code aufrufen.
Wir initialisieren dann OpenGL. Die Hintergrundfarbe wird auf schwarz gesetzt, Alpha wird ebenfalls auf 0.0f gesetzt. Der Depth Buffer wird initialisiert und aktiviert, mit kleiner-gleich-Test.
Die glBlendFunc() ist eine SEHR wichtige Codezeile. Wir setzen die Blend-Funktion auf (GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA). Damit werden die Objekte mit dem geblendet, was auf dem Screen ist, unter der Verwendung von Alpha-Werten, die in den Objekt-Texturen gespeichert sind. Nachdem der Blend-Modus gesetzt wurde, aktivieren wir Blending. Wir aktivieren dann 2D Textur-Mapping und zu letzt aktivieren wir GL_CULL_FACE. Damit entfernen wir die Rückseite von jedem Objekt (es gibt keinen Grund Zeit damit zu verschwenden, etwas zu zeichnen, was wir nicht sehen). Wir zeichnen alle Quads gegen den Uhrzeigersinn, so dass die richtige Seite nicht gezeichnet wird.
Vorher habe ich in diesem Tutorial über die Verwendung von glAlphaFunc() gesprochen anstatt von Alpha-Blending. Wenn Sie die Alpha-Funktion verwenden wollen, kommentieren Sie die 2 Zeilen des Blending Codes aus und kommentieren Sie die 2 Zeilen unter glEnable(GL_BLEND) wieder ein. Sie können ebenfalls die qsort()-Funktion im InitObject()-Code auskommentieren.
Das Programm sollte dann laufen, aber die Sky-Textur wird nicht da sein. Der Grund dafür ist, dass die Sky-Textur einen Alpha-Wert von 0.5f hat. Ale ich über die Alpha-Funktion vorher gesprochen habe, habe ich erwähnt, dass es nur mit Alpha-Werten von 0 oder 1 funktioniert. Sie müssten den Alpha-Kanal für die Sky-Textur ändern, wenn Sie wollen, dass diese erscheint! Erneut, wenn Sie sich dafür entscheiden die Alpha-Funktion zu verwenden, müssen Sie die Objekte nicht Sortieren. Beide Methoden haben ihre Vorteile! Ein kurzes Kommentar dazu von der SGI-Seite:
"Die Alpha Funktion verwirft Fragmente anstatt sie in den Frame-Buffer zu zeichnen. Deshalb ist es nicht nötig die Primitive zu sortieren (es sei denn ein anderer Modus wie Alpha-Blending ist aktiviert). Der Nachteil ist, dass die Pixel entweder komplett undurchsichtig oder komplett transaparent sein müssen.".
BuildFont(); // erzeuge unsere Font Display Liste
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // schwarzer Hintergrund
glClearDepth(1.0f); // Depth Buffer Setup
glDepthFunc(GL_LEQUAL); // Art des Depth Testing
glEnable(GL_DEPTH_TEST); // aktiviere Depth Testing
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // aktiviere Alpha Blending (deaktiviere alpha testing)
glEnable(GL_BLEND); // aktiviere Blending (deaktiviere alpha testing)
// glAlphaFunc(GL_GREATER,0.1f); // Setze Alpha Testing (deaktiviere blending)
// glEnable(GL_ALPHA_TEST); // aktiviere Alpha Testing (deaktiviere blending)
glEnable(GL_TEXTURE_2D); // aktiviere Textur Mapping
glEnable(GL_CULL_FACE); // entferne Back Face
Bis jetzt wurden bisher keine Objekte definiert. Deshalb iterieren wir durch alle 30 Objekte und rufen für jedes Objekte InitObject() auf.
for (int loop=0; loop// iteriere durch alle 30 Objekte
InitObject(loop); // Initialisiere jedes Objekt
return TRUE; // gebe TRUE zurück(Initialisierung war erfolgreich)
}
In unserem Init Code haben wir BuildFont() aufgerufen, was unsere 95 Display Listen erzeugt. Die folgende Codezeile löscht alle 95 Display Listen, bevor das Programm beendet wird.
void Deinitialize (void) // jegliche Benutzer DeInitialisierung kommt hier hin
{
glDeleteLists(base,95); // lösche alle 95 Font Display Listen
}
Nun zum trickreichen Teil... Der Code der die eigentliche Auswahl des Objekts realisiert. Die erste folgende Codezeile alloziert einen Buffer, den wir verwenden können, um Informationen über unsere ausgewählte Objekte aufzunehmen. Die Variable hits wird die Anzahl der registrierten Treffer enthalten, während wir im Selektions-Modus sind.
void Selection(void) // Hier wird die Selektion gemacht
{
GLuint buffer[512]; // Setze eine Selektions-Buffer auf
GLint hits; // Die Anzahl der OBjekte die wir selektiert haben
Im folgenden Code überprüfen wir, ob das Spiel bereits vorbei ist (FALSE). Wenn dem so ist, gibt es keinen Grund etwas zu selektieren, weshalb wir zurückkehren. Wenn das Spiel immer nocht aktiv (TRUE) ist, spielen wir einen Gunshot-Sound mit dem Playsound()-Befehl ab. Selection() wird nur dann aufgerufen, wenn der Maus-Button gedrückt wurde und jedes mal, wenn der Button gedrückt wurde, wollen wir den Gunshot-Sound abspielen. Der Sound wird im async-Modus abgespielt, was bedeutet, dass das Programm nicht angehalten wird, wenn der Sound gespielt wird.
if (game) // Game Over?
return; // wenn ja, dann überprüfe nicht mehr auf Treffer
PlaySound("data/shot.wav",NULL,SND_ASYNC); // spiele Gun Shot Sound
Nun setzen wir einen Viewport auf. viewport[] wird die aktuelle x, y, Länge und Breite des aktuellen Viewports (OpenGL Fensters) enthalten.
glGetIntegerv(GL_VIEWPORT, viewport) ermittelt die aktuellen Viewport Grenzen und speichert sie in viewport[]. Anfangs sind die Grenzen gleich den Dimensionen des OpenGL-Fensters. glSelectBuffer(512, buffer) teilt OpenGL mit, dass ein Buffer für den Selektions-Buffer verwendet werden soll.
// Die Größe des Viewport. [0] ist , [1] ist , [2] ist , [3] ist
GLint viewport[4];
// Dies setzt das Array auf die Größe und die Lokation auf dem Screen relativ zum Fenster
glGetIntegerv(GL_VIEWPORT, viewport);
glSelectBuffer(512, buffer); // Teile OpenGL mit, dass unser Array für die Selektion verwendet werden soll
Der gesamte folgende Code ist sehr wichtig. Die erste Zeile bringt OpenGL in den Selektions-Modus. Im Selektions-Modus wird nicht auf den Screen gezeichnet. Statt dessen werden Informationen über die Objekte im Selektions-Buffer gespeichert, die gerendert werden, während man im Selektions-Modus ist.
Als nächstes initialisieren wir den Name Stack indem wir glInitNames und glPushName(0) aufrufen. Es ist wichtig anzumerken, dass das Programm, wenn es nicht im Selektions-Modus ist, einen Aufruf von glPushName() ignoriert. Natürlich sind wir im Selektions-Modus, aber das ist etwas was Sie sich merken sollten.
// bringt OpenGL in den Selektions Modus. Nichts wird gezeichnet. Objekt ID's und Weiteres wird im Buffer gespeichert.
(void) glRenderMode(GL_SELECT);
glInitNames(); // Initialisiere den Name Stack
glPushName(0); // Pushe 0 (zumindest ein Eintrag) auf den Stack
Nachdem wir den Name-Stack vorbereitet haben, müssen wir das Zeichnen auf die Fläche unter dem Fadenkreuz beschränken. Dafür müssen wir die Projektions-Matrix auswählen. Nachdem die Projektions-Matrix ausgewählt wurde, pushen wir diese auf den Stack. Wir resetten dann mittels glLoadIdentity() die Projektions-Matrix.
Wir beschränken das Zeichnen mittels gluPickMatrix(). Der erste Paramter ist unsere aktuelle Maus-Position auf der X-Achse, der zweite Parameter ist die aktuelle Maus-Position auf der Y-Position, dann die Breite und Höhe der Picking-Region. Zu letzt der aktuelle viewport[]. Der viewport[] indiziert die aktuellen Viewport-Grenzen. mouse_x und mouse_y werden das Zentrum der Picking-Region sein.
glMatrixMode(GL_PROJECTION); // wähle die Projektions Matrix aus
glPushMatrix(); // Pushe die Projektions Matrix
glLoadIdentity(); // Resette die Matrix
// dies erzeugt eine Matrix die an einen kleinen Teil auf dem Screen heranzoomt, da wo die Maus ist.
gluPickMatrix((GLdouble) mouse_x, (GLdouble) (viewport[3]-mouse_y), 1.0f, 1.0f, viewport);
Der Aufruf von gluPerspective() multipliziert die Perspektiven Matrix mit der Pick-Matrix welche das Zeichnen auf die Fläche von gluPickMatrix() beschränkt.
Wir wechseln dann zur Modelview-Matrix und zeichnen unsere Ziele mittels DrawTargets(). Wir zeichnen die Ziele in DrawTargets() und nicht in Draw(), da selection nur auf Treffer von Objekten (targets) überprüfen soll und nicht beim Himmel, dem Boden oder dem Fadenkreuz.
Nachdem wir unsere Ziele gezeichnet haben, wechseln wir zurück zur Projektions-Matrix und poppen die gespeicherte Matrix vom Stack. Wir wechseln dann zurück zur Modelview-Matrix.
Die letzte Codezeile wechselt zurück in den Render-Modus, so dass die Objete, die wir zeichnen, wirklich auf dem Screen erscheinen. hits wird die Anzahl der Objekte enthalten, die in der Fläche von gluPickMatrix() gerendert wurden.
// wende die Perspectivische Matrix an
gluPerspective(45.0f, (GLfloat) (viewport[2]-viewport[0])/(GLfloat) (viewport[3]-viewport[1]), 0.1f, 100.0f);
glMatrixMode(GL_MODELVIEW); // wähle die Modelview Matrix aus
DrawTargets(); // Render unsere Ziele in den Selection Buffer
glMatrixMode(GL_PROJECTION); // wähle die Projektions Matrix aus
glPopMatrix(); // Poppe die Projektions Matrix
glMatrixMode(GL_MODELVIEW); // wähle die Modelview Matrix aus
hits=glRenderMode(GL_RENDER); // wechsel in den Render Modus, finde heraus wieviele
Nun überprüfen wir, ob mehr als 0 Treffer wahrgenommen wurden. Wenn dem so ist, setzen wir choose gleich dem Namen des ersten Objekts, welches in der Picking-Fläche gezeichnet wurde. Depth enthält die Tiefe des Objekts.
Jeder Treffer enthält vier Elemente im Buffer. Das erste ist die Anzahl der Namen auf dem Name Stack, als der Treffer auftrat. Das zweite ist der minimale Z-Wert aller Vertices, die die Viewing-Fläche zum Zeitpunkt des Treffers geschnitten haben. Das dritte ist der maximale Z-Wert aller Vertices, die die Viewing-Fläche während des Treffers geschnitten haben und das letzte ist der Inhalt des Name-Stacks zum Zeitpunkt des Treffers (Name des Objekts). Wir sind nur am minimalen Z-Wert und dem Objekt-Namen in diesem Tutorial interessiert.
if (hits > 0) // wenn es mehr als 0 Treffer gab
{
int choose = buffer[3]; // unsere Auswahl ist das erste Objekt
int depth = buffer[1]; // speichere, wie weit entfernt es ist
Wir iterieren dann durch alle Treffer, um sicher zu gehen, dass kein Objekt näher als das erste getroffene Objekt ist. Wenn wir das nicht machen würden und zwei Objekte sich überlappen würden, könnte das erste getroffene Objekt hinter einem anderen Objekt sein und durch das Klicken der Maus, würde das erste Objekte weg nehmen, obwohl es hinter einem anderen Objekt wäre. Wenn Sie auf etwas schießen, sollte das Objekt welches am nähsten ist, getroffen werden.
Deshalb überprüfen wir alle Treffer. Erinnern Sie sich daran, dass jedes Objekte 4 Elemente im Buffer hat, weshalb wir für jeden Treffer den aktuellen Schleifen-Wert mit 4 multiplizieren müssen. Wir addieren 1, um die Tiefe des getroffenen Objektes zu erhalten. Wenn die Tiefe kleiner als die aktuell ausgewählte Objekt-Tiefe ist, speichern wir den Namen des näheren Objektes in choose und wir speichern die Tiefe des näheren Objekts in depth. Nachdem wir durch alle Treffer iteriert haben, enthält choose den Namen des getroffenen Objektes welches am nähsten ist und depth die Tiefe dieses Objektes.
for (int loop = 1; loop <hits; loop++) // iteriere durch alle bemerkten Treffer
{
// wenn dieses Objekt näher zu uns ist, als jenes, welches wir zur Zeit ausgewählt haben
if (buffer[loop*4+1] <GLuint(depth))
{
choose = buffer[loop*4+3]; // wähle das nähere Objekt aus
depth = buffer[loop*4+1]; // speicher wie weit es weg ist
}
}
Alles was wir noch machen müssen, ist, das Objekt als getroffen zu kennzeichnen. Wir überprüfen, ob das Objekt noch nicht getroffen wurde. Wenn es noch nicht getroffen wurde, markieren wir es als getroffen, indem wir hit auf TRUE setzen. Wir inkrementieren den Score um 1 Punkt und wir inkrementieren den Treffer-Zähler um 1.
if (!object[choose].hit) // wenn das Objekt noch nicht getroffen wurde
{
object[choose].hit=TRUE; // Markiere das Objekt als getroffen
score+=1; // inkrementiere Score
kills+=1; // inkrementiere Level Kills
Ich verwende kills dazu, um zu verfolgen, wieviele Objekte in jedem Level zerstört wurden. Ich wollte, dass jedes Level mehr Objekte hat (um es schwieriger zu machen, durch das Level zu kommen). Deshalb überprüfe ich, ob kills größer als das aktuelle Level multipliziert mit 5 ist. In Level 1 muss der Spieler nur 5 Objekte treffen (1*5). Im zweiten Level muss der Spieler 10 Objekte treffen (2*5), so dass es in jedem Level schwieriger wird.
Die erste Codezeile überprüft also, ob kills größer als das Level multipliziert mit 5 ist. Wenn ja, setzen wir miss auf 0. Das setzt die Spieler-Moral zurück auf 10 von 10 (die Moral ist gleich 10-miss). Wir setzen kills dann auf 0 (was den Zähl-Prozess von vorne beginnen lässt).
Zu letzt inkrementieren wir den Wert von level um 1 und überprüfen, ob wir das letzte Level geschafft haben. Ich habe das höchste Level auf 30 gesetzt und zwar aus zwei Gründen... Level 30 ist wahnsinnig schwierig. Ich bin mir ziemlich sicher, dass es keiner bis dahin schafft. Der zweite Grund... am Anfang des Codes haben wir nur 30 Objekte initialisiert. Wenn Sie mehr Objekte haben wollen, müssen Sie den Wert entsprechend erhöhen.
Es ist SEHR wichtig anzumerken, dass Sie maximal 64 Objekte auf dem Screen haben können (0-63). Wenn Sie versuchen 65 oder mehr Objekte zu rendern, kommt das Picking durcheinander und komische Dinge werden geschehen. Vom zufälligen explodieren von Objekten bis zum Computer-Crash. Es ist ein physikalisches Limit in OpenGL (genauso wie das Limit der 8 Lichtquellen).
Wenn Sie ein Gott sind und irgendwie Level 30 beenden sollten, wird level nicht weiter inkrementiert, aber der Score wird es. Ihre Moral wird ebenfalls immer wieder zurück auf 10 gesetzt, wenn Sie das 30te Level beenden.
if (kills>level*5) // Neues Level?
{
miss=0; // Verfehlungen zurück auf 0 setzen
kills=0; // Resette Level Kills
level+=1; // inkrementiere Level
if (level>30) // größer als 30?
level=30; // Setze Level auf 30 (sind Sie Gott?)
}
}
}
}
In Update() überprüfe ich die Tastendrücke und aktualisiere die Objekt-Bewegungen. Was ganz nett an Update() ist, ist der Millisekunden Timer. Sie können den Millisekunden Timer dazu verwenden, die Objekte, basierend auf der verstrichenen Zeit seit dem letzten Aufruf von Update(), bewegen. Es ist wichtig anzumerken, dass die Bewegung der Objekte basierend auf der Zeit, auf allen Prozessoren gleich schnell ist... ABER es gibt ein paar Einschnitte. Sagen wir, wir haben ein Objekt, dass sich 5 Einheiten in 10 Sekunden bewegt. Auf einem schnellen System wird der Computer das Objekt jede Sekunde eine halbe Einheit weiter bewegen. Auf einem langsamen System könnten es 2 Sekunden sein, bis die Update Prozedur überhaupt erst aufgerufen wird. Wenn sich das Objekt dann bewegt, sieht es aus, als ob es etwas 'springt'. Die Animation wäre nicht so weich auf langsameren Systemen. (Anmerkung: dies ist nur ein übertriebenes Beispiel... Computer aktualisieren WESENTLICH schneller als nur jede zwei Sekunden).
Wie dem auch sei... nachdem das geklärt wurde... weiter zum Code. Der folgende Code überprüft, ob die Escape-Taste gedrückt wurde. Wenn dem so ist, beenden wir die Applikation, indem wir TerminateApplication() aufrufen. g_window enthält die Informationen über unser Fenster.
void Update(DWORD milliseconds) // führe Bewegungs-Aktualisierungen hier durch
{
if (g_keys->keyDown[VK_ESCAPE]) // wurde ESC gedrückt?
{
TerminateApplication (g_window); // beende das Programm
}
Der folgende Code überprüft, ob die Leertaste gedrückt wurde und ob das Spiel vorbei ist. Wenn beide Bedingungen wahr sind, initialisieren wir alle 30 Objekte (geben ihnen neue Richtungen, Texturen, etc.). Wir setzen game auf FALSE, was dem Programm mitteilt, dass das Spiel nicht länger vorbei ist. Wir setzen score zurück auf 0, das level zurück auf 1, die Spieler kills auf 0 und zu guter letzt setzen wir die miss Variable zurück auf 0. Damit starten wir das Spiel erneut im ersten Level mit voller Moral und einem Score von 0.
if (g_keys->keyDown[' '] && game) // wurde die Leertaste gedrückt, nachdem das Spiel beendet wurde?
{
for (int loop=0; loop// iteriere durch alle 30 Objekte
InitObject(loop); // Initialisiere jedes Objekt
game=FALSE; // Setze game (Game Over) auf False
score=0; // Setze score auf 0
level=1; // Setze level zurück auf 1
kills=0; // Null Treffer
miss=0; // Setze miss (verfehlte Schüsse) auf 0
}
Der folgende Code überprüft, ob die F1 Taste gedrückt wurde. Wenn F1 gedrückt wurde, wechselt ToggleFullscreen vom Fenster-Modus in den Fullscreen-Modus oder vom Fullscreen-Modus in den Fenster-Modus.
if (g_keys->keyDown[VK_F1]) // wurde F1 gedrückt?
{
ToggleFullscreen (g_window); // wechsel Fullscreen Modus
}
Um die Illusion von vorbeiziehenden Wolken zu vermitteln und von einem bewegenden Boden, dekrementieren wir roll um .00005f multipliziert mit der Anzahl der Millisekunden, die verstrichen sind. Damit bewegen sich die Wolken mit der gleichen Geschwindigkeit auf allen Systemen (schnell oder langsam).
Wir setzen dann eine Schleife auf, die durch alle Objekte auf dem Screen iteriert. Level 1 hat ein Objekt, Level 10 hat 10 Objekte, etc.
roll-=milliseconds*0.00005f; // bewege die Wolken
for (int loop=0; loop// iteriere durch alle Objekte
{
Wir müssen herausfinden in welche Richtung das Objekt rotiert. Wir machen das, indem wir den Wert von rot überprüfen. Wenn rot gleich 1 ist, müssen wir das Objekt mit dem Uhrzeigersinn rotieren. Um das zu machen, dekrementieren wir den Wert von spin. Wir dekrementieren spin um 0.2f multipliziert mit dem Wert von loop plus der Anzahl der Millisekunden die verstrichen sind. Indem wir Millisekunden verwenden, rotieren die Objekte mit der selben Geschwindigkeit auf allen Systemen. Das Addieren von loop lässt jedes NEUE Objekt etwas schneller rotieren, als das letzte Objekt. Deshalb rotiert Objekt 2 schneller als Objekt 1 und Objekt 3 wird schneller rotieren als Objekt 2.
if (object[loop].rot==1) // wenn Rotation im Uhrzeigersinn ist
object[loop].spin-=0.2f*(float(loop+milliseconds)); // rotiere im Uhrzeigersinn
Als nächstes überprüfen wir, ob rot gleich 2 ist. Wenn rot gleich 2 ist, müssen wir gegen den Uhrzeigersinn rotieren. Der einzige Unterschied zum obigen Code ist, dass wir den Wert von spin inkrementieren anstatt zu dekrementieren. Dadurch rotiert das Objekte in die entgegengesetzte Richtung.
if (object[loop].rot==2) // wenn Rotation gegen den Uhrzeigersinn ist
object[loop].spin+=0.2f*(float(loop+milliseconds)); // rotiere gegen den Uhrzeigersinn
Nun zum Bewegungs-Code. Wir überprüfen, ob der Wert von dir gleich 1 ist, wir inkrementieren den X-Wert des Objekts basierend auf den verstrichenen Millisekunden multipliziert mit 0.012f. Das bewegt das Objekt nach rechts. Da wir Millisekunden verwenden, sollte das Objekt sich auf allen Systemen mit der selben Geschwindigkeit bewegen.
if (object[loop].dir==1) // wenn die Richtung rechts ist
object[loop].x+=0.012f*float(milliseconds); // bewege nach rechts
Wenn dir gleich 0 ist, bewegt sich das Objekt nach links. Wir bewegen das Objekt nach links, indem wir den X-Wert des Objektes dekrementieren. Erneut dekrementieren wir X basierend auf der verstrichenen Zeit in Millisekunden multpliziert mit unserem fixen Wert 0.012f.
if (object[loop].dir==0) // wenn die Richtung links ist
object[loop].x-=0.012f*float(milliseconds); // bewege nach links
Nur noch zwei Richtungen, die wir übeprüfen müssen. Diesmal überprüfen wir, ob dir gleich 2 ist. Wenn dem so ist, inkrementieren wir den Y-Wert des Objektes. Damit wandert das Objekt auf dem Screen nach OBEN. Behalten Sie im Hinterkopfm dass die positive Y-Achse der obere Teil des Screens ist und die negative Y-Achse der untere Teil. Eine Inkrementierung von Y bewegt ein Objekt also von unten nach oben. Erneut basiert die Bewegung auf der verstrichenen Zeit.
if (object[loop].dir==2) // wenn die Richtung oben ist
object[loop].y+=0.012f*float(milliseconds); // bewege nach oben
Weiter zu Teil 2