
Realization
- Abgabe 2
Hier findet sich ein Review zur zweiten Abgabe. Dies ist wohl der Teil, der die Übungsleitung interessiert.
- Abgabe 3
Hier findet sich ein Review zur dritten und finalen Abgabe.
Abgabe 2
Teilziele
Die erste Aufgabe ist es, bis zum 02.05.2005 ein einfaches 3D-Spiel mit Textur und Beleuchtung zu programmieren. Auf unser Spiel gemünzt sollen folgende Dinge realisiert sein:
- das Spielfeld bestehend aus 3D-Modellen der Spielfeldkacheln
- das Kameramodell
- der Akteur (vorerst einfache Kugel)
- die Aktionen des Akteurs auf dem Spielfeld (Feld aufdecken, Bombe vermuten, Bombe markieren, Bewegung in alle Richtungen)
- Texturierung der Kacheln in Abhängigkeit von ihrem Zustand (aufgedeckt, verdeckt, usw.)
- Definition einer Lichtquelle und Festlegung der Materialeigenschaften der Spielobjekte
Stand der Dinge
In den folgenden Abschnitten wird erläutert, wie das Spiel momentan gestartet, konfiguriert und bedient wird.
Programmstart und Konfiguration
Um das Spiel zu starten, muss einfach die Datei "Quaxbomber.exe" im Verzeichnis bin
ausgeführt werden. Beim Start wird die aktuelle Konfiguration aus der Datei config.ini
geladen. Anschließend wird eine Spielfläche inklusive Akteur erzeugt. Folgende Tabelle gibt eine Übersicht der Parameter, die konfiguriert werden können.
xResolution/ yResolution |
Gibt die horizontale und vertikale Bildschirmauflösung an. Ist der Modus auf dem System nicht verfügbar, startet das System mit voreingestellten Werten im Fenstermodus mit 800x600 bei 60 Hz und 16 Bit Farbtiefe. |
colourDepth |
Gibt die Farbtiefe an. Ist der Modus auf dem System nicht verfügbar, startet das System mit voreingestellten Werten im Fenstermodus mit 800x600 bei 60 Hz und 16 Bit Farbtiefe. |
frequency |
Gibt die Bildwiederholrate an. Ist der Modus auf dem System nicht verfügbar, startet das System mit voreingestellten Werten im Fenstermodus mit 800x600 bei 60 Hz und 16 Bit Farbtiefe. |
fullscreenFlag |
Gibt an, ob das System im Vollbild oder im Fenstermodus starten soll. Im Fenstermodus läuft nebenbei die Konsole zu Debugzwecken ( - wird später abgeschalten). Werte größer 0 werden als "An" interpretiert. |
lightningFlag |
Gibt an, ob die Beleuchtung und die Materialien verwendet werden sollen. Werte größer 0 werden als "An" interpretiert. |
defaultAngleX/ defaultAngleY |
Gibt den Startwinkel für die Rotation der Kamera um die x-Achse bzw. y-Achse an. Sinnvolle und gültige Werte liegen zwischen -15 und -90 Grad für die x-Achse bzw. -45° bis 45° für die y-Achse. Die angegebenen Werte werden auch beim Rücksetzen der Kamera verwendet (Taste [Pos1]) |
defaultLevel |
Gibt das Level an, mit dem das Spiel gestartet wird. Damit verbunden ist die Spielfeldgröße, die Anzahl der verteilten Bomben, die Leben und Zeitstopper. 0 steht für "Easy" und erzeugt das kleinste Spielfeld mit 8x8 Kacheln. 1 steht für "Medium" (Voreinstellung in Quaxbomber.h ) und erzeugt ein 10x10 Spielfeld. 2 Steht für "Hard" und erzeugt ein 12x12 Spielfeld. |
[up to abgabe 2] [up]
Interaktion
Nach dem Spielstart erscheint die Spielfläche mit dem Akteur. Der Aktuer ist momentan eine einfache blaue Kugel, die mit Hilfe der Cursortasten über die Kacheln bewegt werden kann. Folgendes Bild zeigt eine mögliche Startkonfiguration.
Möglicher Startbildschirm (abh. von den Einstellungen in der Datei config.ini
)
Ein Druck auf eine Cursortaste entspricht einem Schritt zur nächsten Kachel in Richtung der Pfeiltaste. Auf Polling verzichtet, da die Bewegungen auf einem langsamen System recht träge werden (bei zeitgesteuerter Bewegung) und die Steuerung ziemlich frustrierend wird.
Weiterhin kann die Sicht auf die Spielfläche verändert werden. Der Spieler kann die Sicht um x- und y-Achse rotieren sowie in einem festgelegten Bereich zoomen. Der Zoombereich wurde dabei so festgelegt, dass möglichst immer die gesamte Spielfläche zu sehen ist. Leider ist die Spielfläche dadurch etwas klein - wir müssen evtl. eine Übersichtskarte hinzufügen. Die veränderte Ansicht kann nach Drehungen und Zommen auch zurückgesetzt werden. Folgende Tabelle zeigt alle momentan verfügbaren Tasten sowie deren Wirkung (vgl. concept)
Taste | Aktionen |
Cursor Up (Diskret) | Eine Kachel nach oben gehen |
Cursor Down (Diskret) | Eine Kachel nach unten gehen |
Cursor Left (Diskret) | Eine Kachel nach links gehen |
Cursor Right (Diskret) | Eine Kachel nach rechts gehen |
b (Diskret) | Wechsel des sichtbaren Kachelzustands: Bombe vermuten | Bombe setzen | Nix setzen |
Spacebar (Diskret) | Feld aufdecken |
ESC (Diskret) | Spiel verlassen |
Ende (Polling) | Kamera rechts um Y-Achse drehen |
Entf (Polling) | Kamera links um Y-Achse drehen |
Bild Auf (Polling) | Kamera Rotation um X-Achse nach oben |
Bild Ab (Polling) | Kamera Rotation um X-Achse nach unten |
Pos1 (Diskret) | Kamera zurücksetzen |
- (Polling) | Zoom verringern |
+ (Polling) | Zoom vergrößern |
Das Aufdecken der Kacheln funktioniert bereits korrekt. Wird eine Kachel aufgedeckt, wird überprüft, ob sie sich in der Nähe von Goodies (hellgrüner Hintergrund) bzw. Bomben befindet. Je nachdem, ob dies der Fall ist, werden auch die Nachbarn der Kacheln geändert. Zum Vergleich kann Minesweeper benutzt werden. Der Mechanismus in userem Spiel funktioniert ähnlich, ist jedoch um die Abfrage von Goodies erweitert.
Markierte Felder werden nicht aufgedeckt. Das kann leicht ausprobiert werden, wenn das Spiel im einfachsten Level (0 = Easy) gestartet wird. Da es recht wenige Bomben und Goodies gibt, werden oft viele zusammenhängende Felder "umgedreht". Die markierten Felder sind davon nicht betroffen. Im folgenden Bild ist gezeigt, wie der Spieler ein Feld aufgedeckt hat. Da dieses leer war (keine Bombe, kein Goodie, keine Zahl, ...), wurden auch die Nachbarn "umgedreht".
Aufdecken einer Kachel und Ihrer Nachbarn
Neben Bomben sind unter den Spielfeldern auch Goodies versteckt (Leben, Bonuszeit). Momentan explodiert keine Kachel und der Spieler bekommt auch kein Leben oder einen Zeitbonus gutgeschrieben. Dennoch kann durch die Texturen auf den Kacheln bereits erkannt werden, wann was passieren soll. Würde der Spieler ein Leben erhalten, ist ein Herz zu sehen. Würde er einen Zeitbonus erhalten, ist ein Stern zu sehen. Würde die Kachel explodieren, ist eine Bombe zu sehen. Im folgenden Bild ist dargestellt, wie eine Bombe und ein Leben gefunden wurden.
Der Spieler hat ein Leben und eine Bombe gefunden.
Wie oben beschrieben, kann der Spieler mit der Taste 'b' ein Spielfeld markieren. Er kann vermuten dass eine Bombe unter dem Spielfeld liegt oder er kann es festlegen. Durch mehrmaliges Drücken von 'b' wechselt die Markierung, wie in der obigen Tabelle beschrieben. Im Bild wurde eine Bombe vermutet (blau) und eine Bombe gesetzt (rot).
Momentan ist die Markierung eines Feldes mit einer Bombe erst sichtbar, wenn der Nutzer das Feld verläßt. In der finalen Version werden wir verschiedenfarbige, halbtransparente Würfel verwenden, um die Markierung zu verdeutlichen.
[up to abgabe 2] [up]
Komponenten des Spiels
Beim Zusammenbau des Spiels haben wir Wert darauf gelegt, die einzelnen Komponenten möglichst unabhängig voneinander zu gestalten und eine Art Model View Controller Entwurfsmuster (MVC) zu implementieren. Folgendes Bild zeigt vereinfacht die Abhängigkeiten der Klassen.
Modelle - Logik und Grafik
Gemäß dem MVC-Prinzip gibt es Komponenten zur Darstellung der Logik und Komponenten zur Darstellung des Inhaltes/ der Grafik. Die grafischen Komponenten (View), werden bei uns durch Klassen mit dem Wort Model
dargestellt (Ja, das ist wohl etwas verwirrend aber klingt besser:o). Alle diese Klassen sind von einer Klasse Model
abgeleitet, die eine minimale Schnittstelle darstellt. So besitzt sie eine virtuelle Methode render()
und die konkrete Methode loadTexture()
. Die zu einem Modell gehörige Logik findet sich dann jeweils in der Klasse ohne das Wort Model
- also z.B. Character
und CharacterModel
.
Jede logische Repräsentation hält eine Liste von möglichen grafischen Repräsentationen. Zwischen diesen kann im Spiel gewechselt werden. Genauer gesagt, verwaltet ein logisches Modell nur die Zeiger auf mögliche Darstellungen (Modelle) in einem Vektor. Auf diese Weise brauchen wir z.B. nur ein Modell einer zugedeckten Kachel zu erzeugen und nicht 8x8, 10x10 oder 12x12. Das Wechseln der eines Modells findet zum Beispiel beim Aufdecken eines Feldes statt.
Bisher ist die Geometrie aller Modelle im Code des Programms verdrahtet. Um ein Objekt effizient zu rendern, sind die Punkte eines Objektes als statisches Array abgespeichert und werden mit Hilfe von glDrawElements()
gerendert.
Für die Spielfeldkacheln werden wir die Speicherung im Code auch in der Endversion beibehalten, da diese nicht sehr komplex sind. Für den Character und die Bomben wird jedoch eine loadModel()
Methode implementiert werden. Die wird dann in der Klasse Model
ihren Platz finden.
Texturen
Um Texturen zu laden, wurde eine Methode loadTexture()
in der Klasse Model
untergebracht. Durch den Aufruf, wird eine TGA Datei geladen und als Texturobject in einem Vektor gespeichert. Auf diese Weise müssen auch Texturen nur einmal geladen werden und können - wie die Geometrie - während des Spielverlaufs umgeschalten werden. Alle im Spiel verwendeten Texturen wurden selbst erzeugt.
Zum Laden verwenden wir die freie Bibliothek FreeImage. Daher muß die Datei FreeImage.dll im Programmverzeichnis vorhanden sein. Wir möchten diese Bibliothek wirklich empfehlen, da sie sehr gut dokumentiert ist und auch mit anderen Entwicklungsumgebungen arbeitet als Visual Studio.
Animation
Momentan werden die Markierungen (Bomben) der Spielfelder animiert. Um die Animation unabhängig von der Bildrate zu machen, wurde eine Klasse zur Zeitmessung (GameTimer
) implementiert, welche die Funktion QueryPerformanceCounter
nutzt. Die Klasse bietet die Möglichkeit, die Zeit zu stoppen und die aktuelle Bildrate zu berechnen. In Abhängigkeit vom Timer werden die Markierungen (Bomben) gedreht und sind damit unabhängig von der Bildrate. Auch bei der Rotation um das Spielfeld und beim Zoomen benutzen wir den Timer.
Beleuchtung und Materialien
Um das Kriterium der Beleuchtung und der Materialeigenschaften zu erfüllen, haben wir eine Richtungslichtquelle (positional light) an der Kamera angebracht. Wenn die Kamera bewegt wird, wird auch das Licht bewegt. Dies ist daran erkennbar, dass die Spielfläche dunkler erscheint, wenn sie aus einem flachen Winkel betrachtet wird.
Da die Szene anfangs ohne Texturen implementiert wurde, besitzen alle Kacheln ein Array mit Farbwerten. Wir haben mittels glColorMaterial()
festgelegt, dass diese Befehle auch bei der Beleuchtung zu berücksichtigen sind. Um die Geschwindigkeit nicht zu stark zu bremsen, werden die Farben nur für die Vorderseiten berücksichtigt. Alle Szenenobjekte besitzen diffuse Reflexionseigenschaften.
[up to abgabe 2] [up]
Download
[up to abgabe 2] [up]
Abgabe 3
Ziele
Im 3. Abschnitt sollte die Spiellogik und das Spieldesign verfeinert werden, so dass ein echtes Spiel entsteht. Weiterhin sollten komplexe Objekte und Effekte implementiert werden. Im folgenden folgt die Auflistung der umgesetzten Anforderungen sowie eine kurze Beschreibung mit Screenshot. Es wurden folgende Dinge implementiert:
- Die Logik des Spiels mit 6 verschiedenen Modi, Start, Pause und Ende
- Nichttriviale Objekte für Quax, Minen und Goodies als Quake Modelle
- View-Frustum-Culling mit verschiedenen Modi (zur Demonstration, nicht weil es sinnvoll ist)
- Transparenz Effekte auf Texturen und Objekten
- Verschiedene Texturfilter, und Renderingmodes
- Konfigurierbare Partikelsysteme (3)
- Animierte Texturen
Spieldesign
Wir haben das Spiel nun soweit erweitert, dass es als wirkliches Spiel durchgeführt werden kann. Dazu wurden Startbildschirme, Unterbrechungsmöglichkeiten und Gewinnabfragen eingeführt.
Startbildschirm, Pause und "Spiel verloren"
In der Logik des Spiels wurden die eingesammelten Items berücksichtigt. Tritt der Spieler auf eine Bombe, wird ihm ein Leben abgezogen und geprüft ob das Spiel beendet werden muss. Öffnet der Spieler ein Feld mit einem Leben, bekommt er dieses gutgeschrieben. Genausoverhält es sich mit dem Zeitbonus (Stern). Hier werden 10Sek. von der absolut verstrichenen Zeit abgezogen. Das Spielobjekt hat dazu einen eigenen Timer der unabh. vom Animationstimer (FPS Timer) läuft.
Markiert der Spieler ein Feld, so wird ebenfalls überprüft, ob er bereits alle Bomben gefunden hat. Ist dies der Fall, wird in abh. von der verstrichenen Zeit ein unterschiedlicher Endbildschirm angezeigt. Ist die Zeit größer als die aktuelle Bestzeit des Level, ist der Spieler der "Held", anderenfalls ein "Schleicher".
Weiterhin hat der Spieler sechs verschiedene Spielmodi zur Auswahl: Er kann das Spiel jeweils im "classic mode" starten, indem er den parameter "classic" in der config.ini
auf einen Wert >0 setzt. Dabei wird eine flache Spielfläche erzeugt, wie sie schon im ersten Abschnitt erzeugt wurde. ist der Wert <=0, wird ein pyramidenförmiges Spielfeld erzeugt. Dies soll den Schwierigkeitsgrad erhöhen. Für eine extra Berechnung der Punkte in diesem Spielmodus war jedoch leider keine Zeit mehr.
Klassische Spielansicht vs. Pyramidenansicht bei Spielstart im Level "Easy"
Nichttriviale Objekte
Die Spielfläche wird wie schon bei der ersten Abgabe im Code selbst erzeugt. Einerseits mit Vertex Arrays und andererseites mit glVertex Befehlen (für den Immediate Mode). Um im Spiel nicht triviale Objekte vorkommen zu lassen, hatten wir uns zunächst für das binäre Milkshape-Format entschieden, mit dem wir auch all unsere Objekte erstellt haben. Es war uns allerdings nicht möglich eine Animation für dieses Format zu realisieren, weshalb wir auf das Quake Format md2 umgestiegen sind. Die Objekte (Quax, Minen, Herz und Stern) wurden dazu selbst erstellt und in das md2 Format exportiert.
Einge permanet sichtbare nicht triviale Spielobjekte: Quax und zwei Minen
Der md2 Loader basiert auf dem Tutorial von David Henry und wurde von uns neu implementiert und um einige Funktionen erweitert: Zunächst einmal lädt der Loader das Objekt (Punkte, Frameindizes, Texturkoordinaten, ...) in einen internen Speicher. Die Klasse ist von unserer Basisklasse Model abgeleitet und verfügt somit über die Fähigkeit (24Bit und 32 Bit TGA) Texturen zu laden. Diese werden in einem internen Vector als Display List gespeichert. Das Rendern des Objektes erfolgt mit Hilfe der in der Datei gespeicherten OpenGL Befehle und ist somit sehr effizient.
Jede Animation besteht aus einer Anzahl von Keyframes zwischen denen in Abhängigkeit der verstrichenen Zeit interpoliert wird. Um die möglichen Animationen pro Objekt möglichst flexibel zu halten (Benennung, Dauer), wurde weiterhin ein Parser für eine .md2.keyframes
Datei geschrieben. Diese Datei definiert das Startframe, das Endframe, die Geschwindigkeit, die Art der Wiederholung und einen Bezeichner für jede Animationssequenz. Der Parser lädt die Keyframes Datei beim Laden eines Models und speichert die Daten in einem Vector. Um im Spiel die Animationen zu wechseln rufen wir einfach die Animation eines Objekte mit dem Namen, z.B. "JUMP", auf.
Culling
Um die Anfordrungen zu erfüllen haben wir ein View-Frustum-Culling auf der Basis der Beschreibungen von Mark Morley implementiert und modifiziert. Da sich alle unsere Objekte auf der Spielfläche befinden, werden nur die Kacheln selbst auf sichtbarkeit getestet. Mit der Taste F8 wird das Culling aktiviert. Bei nochmaligem betätigen von F8 erscheinen die Boundingboxen.
Culling im r Modus. Der Spieler wird nicht gestestet.
Transparenz-Effekte
An mehreren Stellen im Spiel sind transparente Texturen definiert. einerseits werden die "Marker" (Würfel wenn der Spieler ein Feld betritt) transparent gezeichnet. Da jeweils nur ein Marker sichtbar sein kann, wird dieser immer am Ende der einer Szenendarstellung erzeugt und in Abh. von der aktuellen Spielerposition gezeichnet, wenn die besuchte Kachel markiert ist. Die Textur wird auf den Würfel im GL_BLEND Modus bei abgeschaltener Beleuchtung gemappt und erscheint damit halb transparent. Dies ergibt die richtige Sichtbarkeit wie man leicht feststellen kann, wenn man durch die Seiten des Würfels auf die dahinterliegende Spielfäche schaut.
Transparente Objekte als Marker für eine Kachel.
Weiterhin werden alle Arten von Schriften und Menus mit Hilfe von teilweise transparenten Texturen erzeugt. Die Schrift besteht aus Teilbildern die auf Quads gemappt werden. Die Menus (das Hud, die Spielnachrichten, wie z.B. "LOOSER") bestehen aus je zwei Texturen - jeweils einer Maske und einer normalen Textur, die ebenfalls auf ein Quad gemappt werden.
Um Schriften ausgeben zu können wurde eine Klasse BitmapString implementiert, die in der Lage ist, eine einen String oder einen Integer als Text auszugeben. Dabei werden kann jeweils die Blending Funktion mit angegeben werden. Die Klasse wird z.B. beim Hud und bei den Statusmeldungen (F1) benutzt.
Weitere Transparenzeffekte haben wir bei dem Partikelsystemen benutzt (siehe Spezialeffekte)
Experimentieren mit OpenGL
Hinweis: Alle Statusmeldungen können mit Hilfe der Taste F1 angeschalten bzw. ausgeschalten werden.
Das Spiel kann in drei Modi gerendert werden: Immediate vs. Vertex Arrays (F6) und mit Hilfe von Display Listen(F7). Die Rendermodi verändern im wesentlichen das Rendern der statischen Objekte - wie z.B. der Spielfeldkacheln (Texturen werden fast ausschließlich mit Hilfe von glCallList
gebunden). Im immediate mode werden z.B. alle Kacheln mit Hilfe von glVertex
, glColor
, usw. erzeugt. Im Vertex Array Mode wird auf VertexPointer zurückgegriffen. Wir haben festgestellt dass sich die Framerate im Displaylisten Modus zeitweise sogar verdoppelt.
"Mipmapping vs. Non Filtered (right).
Mit Hilfe der Tasten F4 und F5 läßt sich die Texturqualität verändern. Mit F4 wechselt man zwischen "Kein Textur Filter", "Bi-lineare Filterung" und "Tri-linearer Filterun(+Mip Mapping)". Mit der Taste F5 wird Mipmapping aktiviert oder alle Texturfilter deaktiviert. Die Mipmaps werden zu bei der Erzeugung der Objekte (beim Texturladen) mit Hilfe von gluBuildMipmaps
erzeugt. Die Qualitätsunterschiede sieht man vor allem auf den Kacheloberflächen. Mip Mapping läßt sich nach dem Abschalten nur wieder aktivieren, wenn der bewegte Nebel ausgeschalten ist!
Mit Hilfe der Taste F3 läßt sich der Wireframe Mode aktivieren bzw. deaktivieren. Durch die Aktivierung werden die Statusmeldungen deaktiviert. Mit Hilfe der Taste F2 kann die Framerate angezeigt werden.
"Wireframemodus: Spieler steht auf Bombe(links) und Explosion (rechts)
Spezialeffekte
Für die Spezialeffekte wurden Partikelsysteme mit Hilfe von OpenGL Extensions (Point Sprites) und bewegte Texturen implementiert.
Für die Darstellung von Explosionen, dem Lebensbonus und Zeitbonus wurde ein Partikelsystem erstellt, dass sich je nach Wunsch konfigurieren läßt. Die Grundidee ist dabei immer die folgende: ausgehend von der Characterposition werden Partikel und/ oder Objektteile in eine bestimmte Entfernung für eine bestimmte Zeit geschleudert. Für die Leuchtspuren und Feuerspuren verwenden wir die OpenGL extension Point Sprite, mit deren Hilfe wir eine kleine Textur an einen Punkt festkleben. Die Bewegungsrichtung der Partikel ist dabei von Typ der Explosion abhängig.
"3 verschiedene Explosionstypen".
Für die Bombenexplosion bewegen sich die Partikel zunächst nach oben und fallen dann mit quadratischer Geschwindigkeit nach unten. Um eine zerplatzende Kachel zu simulieren werden außerdem eine einstellbare Anzahl von GL_TRIANGLE_STRIPS gerendert und langsam mit Hilfe des Alpha Kanals ausgeblendet.
Für die Goodieanimation bewegen sich die Partikel auf einer zylindrischen Oberfläche nach oben. Dazu berechnen wir vorab die Positionen für einen Kreis und verschieben diesen dann in Abh. von der Laufzeit der "Explosion". Zusätzlich wird ein Md2Model entlang der y Achse langsam ein und dann wieder ausgeblendet. Die soll so aussehen wir bei den Super Mario Bros. - wenn man Münzen einsammelt.
Die Explosionen haben einiges an Zeit gekostet, da sie auf meiner ATI Karte einen Fehler im Texture Mapping aller anderen Texturen verursachen. Ich habe auch versucht Multitexturing zu verwenden dies lies sich jedoch nicht deaktivieren und so wurden plötzlich alle Texturen vertauscht. Mir ist nicht klar ob das an meiner implementierung lag oder nicht. Auf anderen Rechnern (mit GeForce) gab es meist keine Probleme.
Um ein geeignetes Ambiente zu schaffen, wurde eine düstere Umgebung kreiert. Nach einigem Hin und Her haben wir uns für eine vereinfachte Skybox entschieden. In einer fixen eintfernung wir ein Bild auf ein quad gemappt. Die Textur wurde darin vorab verzerrt und simuliert damit Tiefe. Da sich ein ordentlicher Übergang der Texturebenen nicht realisieren ließ, haben wir es bei der einen Ebene belassen. Um die Illusion eines unendlich Tiefen bodens zu Schaffen, wird "Nebel" schräg unter der Spielfläche animiert. Dazu werden die Texturkoordinaten schrittweise erhöht und erniedrigt - so als würde Lava unter dem Spielfeld schwappen, die sich bewegt und den Nebel darüber anstrahlt.
[up to task 3] [up]