Worm-X
Worm-X ist ein 'simples' Arcadespiel, in dem man so viele Punkte wie möglich erreichen muss. Nach jeder durchgespielten 5. Runde kommt man in eine Bonsurunde die wiederrum 3 'Waves' beinhaltet. Wenn die erreichte Punktezahl hoch genug ist, darf man sich in eine Highscorliste eintragen. Das Ganze ist ein wenig an die Arcadeklassiker der 80'er angelegt (ja dort hat man sich so in eine Highscorliste eingetragen ). Die Spieltiefe ist entsprechend flach, was aber meiner Meinung nach nicht schlimm ist, da es so einfacher ist den Quellcode zu verstehen. Moorhuhn hatte wohl die gleiche Spieltiefe !!
Los gehts:
Im Gegensatz zum ersten Teil ( Space Invaders) ist Worm-X in 3D. Auch wenn es im ersten Moment so aussieht als würde man 'flach' auf das Spielgeschehen schauen ist es doch in Wiklichkeit eine Perspektivische Ansicht, allerdings mit einem recht kleinen 'Field of View' Winkel der Kamera (5 Grad um genauer zu sein). Je kleiner dieser Winkel ist um so mehr bekommt man eine Ortho-ähnliche (also ohne Perspektive) Ansicht. Desweiteren ist die Kamera auf der X-Achse um 90 Grad gekippt, das heißt man schaut von oben auf das Spielfeld das auf der XZ- Achse liegt. Ursprünglich wollte ich eine freibewegliche Kamera einbauen, in der man auch eine FPS-Ansicht des Spiels hat, das hab ich aber wieder verworfen, da es nicht den gewünschten Effekt hatte.Übersicht:
Das Spiel besteht im Wesentlichen aus 4 großen Blöcken, diese sind- das Intro
- der Auswahlschirm
- das Spiel ansich
- der Bonuslevel
Spielobjekte / OpenGL-Wrapper:
Alle Klassen die zu den OpenGL-Wrapper Klassen gehören (NEIN ES IST KEINE ENGINE!!!) beginnen mit CFX alle anderen die direkt zum Spiel gehören haben dieses Prefix nicht.Aufbau des Spiels:
Beim Start, wird zuerst versucht, einen OpenGL-Context (CFXOpenGLContext) mit den gewünschten Bildeinstellungen (GlobalDefinitions) zu erstellen. Wenn das geklappt hat wird eine Instanz von der Klasse Game erzeugt. Dort wird dann die Methode 'setupGame' aufgerufen die ihrerseits alle benötigten Objekte erzeugt. Einwurf: Ich habe den Quellcode mit viel Kommentaren versehen, ich hoffe es ist alles verständlich Wenn alles geklappt hat wird in die Methode 'enterGameCycle' gesprungen und das Intro wird abgespielt. Mit der Leertaste kommt man dann in den Auswalschirm, der dann selbsterklärend ist.
Die MainLoop:
Kernstück des Spiels ist die Methode 'enterGameCycle', hier werden im groben 4 Dinge erledigt:- ein AutoreleasePool erstellt
- Die Zeit berechnet, die das System braucht um einen Durchlauf zu rendern.
- Die KeyEvents abgefangen
- Über eine simple Switch-Anweisung das komplette Spiel gesteuert

ACHTUNG:
Während der Film abgespielt wird, dürfen KEINE! anderen OpenGL-Aufrufe gemacht werden.
OpenGL ist NICHT Threadsafe!!
Sonst geht es euch wie mir, das das System einfriert :-(
Highscore:
Je nachdem wie viele Punkte man erreicht hat, darf man sich in die Highscoreliste eintragen. Ich habe dafür 3 Klassen erstellt- HighScore ist im Prinzip ein Eintrag (Name, Punkte) in der Highscoreliste
- HighScoreController lädt und speichert die Highscoreliste
- HighScoreListWriter dort trägt man sich in die Highscoreliste ein

3D-Models:
Die 3D-Models werden über die Klasse CFXWavefrontModel geladen, hier sind einige Dinge zu beachten. Eine Wavefront-Datei ist eine simple Textdatei, die man entsprechend parsen kann. Folgende Einschränkungen sind zu beachten, wenn man die Klasse benutzen will:- Das Model muss 'trianguliert' sein, das heißt das Model muss aus Dreiecken bestehen
- Es müssen Normale vorhanden sein (Ok, im Prinzip kann man die selber berechnen)
- Es müssen Texturekoordinaten vorhanden sein.
- Das Material wird nicht berücksichtigt
- alle Zeilen die mit einem 'v' beginnen, diese beinhalten die Vertexdaten
- alle Zeilen die mit einem 'vt' beginnen, diese beinhalten die Texturekoordinaten
- alle Zeilen die mit einem 'vn' beginnen, diese beinhalten die Vertexnormale
- alle Zeilen die mit einem 'f' beginnen, diese beinhalten die Faceinformation (was nichts anderes als ein Index auf die oben genannten Punkte ist)
tmp = normalizeVector(tmp);eingebaut, die das übernimmt. Wir erinnern uns, um ein Objekt 'sauber' zu beleuchten, muss es über normalisierte Normale (was ein Wort) verfügen. Also wie gesagt wird die Datei mit einem simplen NSScanner gescannt, und die Daten werden entsprechend aufbereitet in 2 Arrays gespeichert. Einmal die Daten (Vertex, Texturekoordinaten, Normale) selbst (_data) und einmal die Indexe (_vertexIndicies) auf diese Daten. Danach wird überprüft ob VBO's (Vertex Buffer Objects) verfügbar sind, wenn ja werden diese erzeugt. Wenn VBO's unterstützt werde, braucht man eigentlich die beiden Arrays (_data, _vertexIndicies) nicht mehr, da die Daten dann im Grafikkartenspeicher liegen. Ich habe sie aber trotzdem generiert, für den Fall das VBO's nicht unterstützt werden. Dann nämlich werden 'normale Vertexarrays mit den beiden Arrays erzeugt. An dieser Stelle noch ein kurze Erklärung zu Vertexarrays / VBO's: Die wohl schnellste Methode um komplexe Geometrie zu rendern sind VBO's im Zusammenhang mit 'Indexed Arrays' Das heißt man hat einmal die Rohdaten (Vertexdaten, Normale, Texturekoordinaten) und einmal die Faces (Indexe auf die Rohdaten) In unserem Fall sieht das dann so aus:
GLfloat *tmp = [_model data]; glVertexPointer( 3, GL_FLOAT, 8*sizeof(GLfloat), tmp );//Stride und Zeiger auf Daten glTexCoordPointer( 2, GL_FLOAT, 8*sizeof(GLfloat), &tmp[3] );// '' glNormalPointer( GL_FLOAT, 8*sizeof(GLfloat), &tmp[5] );// '' glDrawElements( GL_TRIANGLES, [_model numIndexes]*3, GL_UNSIGNED_INT, [_model vertexIndicies] );Die Rohdaten liegen in [_model data] und die Indexe in [_model vertexIndicies]. Das Problem bei dieser Methode ist, das alle Daten (also alle Vertexdaten, Texturekoordinaten und Normale) alle gleich lang sein müssen, da sonst der Stride (also Abstand zwischen den einzelnen Daten nicht mehr stimmen würde) Das ist aber leider nicht immer der Fall (einfach mal eine Wavefront anschauen). Ich habe da ein bisschen nachgeholfen, ich habe die Daten die mit einem 'f' gekennzeichnet sind (also Faces bzw. Indexe) genommen und anhand dieses Wertes einfach die entsprechenden Vertex, Texture und Normale ausgelesen und in meinem Array abgespeichert. Das ist zwar nicht Sinn und Zweck von Indexierten Daten (da durchaus Dubletten vorkommen können), aber für unsere Zwecke die einfachste Methode, außerdem spielt es bei der geringen Anzahl an Dreiecken keine besonders große Rolle, da man so gut wie kein Unterschied bei der Performance bemerkt.
Kollision der Models / Gameobjekte:
Auch diesmal kommt eine simple Box-Box Kollisionserkennung zum Einsatz. Das berechnen der BoundingBox übernimmt die Methode-(void) calculateBBox:(NSArray *)verticesin der Klasse CFXWavefrontModel beim Laden der Geometrie selbständig. Somit hat man keine Arbeit mehr damit. Wie ein BoundingBox-Kollision funktioniert habe ich ja im ersten Teil beschrieben.
Gameobjekte:
Alle Objekte die man im Spiel sieht, sind von der Klasse GameObject abgeleitet. Diese Klasse ist es auch, die die Objekte später auf den Schirm rendert. Die einzelnen Objekte unterscheiden sich in der Regel nur dadurch, das sie sich unterschiedlich bewegen, was man in der Methode- (void)animate:(float)deltaTimeder jeweiligen Objekte sehen kann. Diese 'arbeiten / bewegen' sich relativ autark vom restlichen Spiel. Das bedeutet man muss sich zum Beispiel nicht darum kümmern, das sie sich aus dem Spielfeld bewegen. Die Verwaltung der Objekte übernimmt die Klasse GameObjectsManager (NSMutableArray). Eine Besonderheit ist das BonusObjekt, dieses bewegt sich wie schon im Blog erwähnt über einen Catmull-Rom Spline Die Vertexdaten für den Spline liegen in Splines.h
Goodies:
wie der Name schon sagt, wenn man diese Objekte einsammelt erhält man was Schönes dafür. Ansonsten gilt hier das Selbe wie bei den Gameobjekten (siehe oben)Schüsse:
Es gibt 4 verschiedene Arten von Schüssen die der Spieler abfeuern kann:SIMPLE_SHOT:
der Standarschuss
SUCKER_SHOT:
ein Downgrade des Schusses, dieser bewegt sich langsamer und hat nur eine begrenzte Reichweite
THREE_TIMES_SHOT:
Schießt in 3 Richtungen (dieser ist auch der einzige der ein 'Barrier-Objekt' zerbröseln kann
WAVE_SHOT:
Diesen gibt es in 2 Versionen einmal als einzelnen Schuß und einmal als doppelten (nicht der zum trinken;-) )
Dieser Schuß bewegt sich in einer Schlangenlinie über den Schirm
Alle Schüsse werden im ShootManager (NSMutableArray) verwaltet. Der Grund warum ich für die Schüsse einen extra Manager angelegt und nicht in den GameObjectsManager mit eingebaut habe ist, zum einen die Kollisionserkennung (diese reduziert sich dadurch erheblich) und das Rendern, weil die Schüsse, wie auch die Partikelsysteme mit aktiven Blending und ohne Beleuchtung gerendert werden. Dadurch entfallen einige 'State-Changes' wärend des Rendervorgangs.
Playfield:
Die Klasse Playfield dient dazu die Spielobjekte in einem unsichtbaren Raster zu platzieren. Dies ist nötig, da sonst der Wurm nicht 'sauber' durch die einzelnen Spielobjekte bewegt werden kann. Die wichtigen Methoden darin hab ich nochmals entsprechend kommentiert.Shader:
Ich habe diesmal einen Shader (Per Pixel Lighting) mitteingebaut, damit man mal sieht wie das mit der Einbindung von Shadern funktioniert. Zuerst wird mal geprüft, ob Shader überhaupt vom System unterstützt werden, wenn nicht wird das Spiel mit der Standardbeleuchtung von OpenGL gerendert, was leider nicht ganz so schön aussieht. Die Handhabung der Shaderklasse ist recht einfach und funktioniert so: Prüfen ob Shader unterstützt werden const GLubyte* extensions = glGetString(GL_EXTENSIONS);
if ((GL_FALSE == gluCheckExtension((GLubyte *)"GL_ARB_shader_objects", extensions)) ||
(GL_FALSE == gluCheckExtension((GLubyte *)"GL_ARB_shading_language_100", extensions)) ||
(GL_FALSE == gluCheckExtension((GLubyte *)"GL_ARB_vertex_shader", extensions)) ||
(GL_FALSE == gluCheckExtension((GLubyte *)"GL_ARB_fragment_shader",extensions)))
{
_shadersAvailable = NO;
}
Shader laden
[_shaderManager createShader:@"PerPixelLighting" vertexShader:[bundle pathForResource: @"PerPixelLighting" ofType: @"vert"] fragmentShader:[bundle pathForResource: @"PerPixelLighting" ofType: @"frag"]]; [_shaderManager useShader:@"PerPixelLighting"];Shader einschalten / setzten
if(_glslSupported) [_shaderManager attachCurrentShader]; else [_openGLState setLightState:YES];rendern
glEnableClientState( GL_VERTEX_ARRAY ); glEnableClientState( GL_TEXTURE_COORD_ARRAY ); glEnableClientState(GL_NORMAL_ARRAY) . . .In unserem Beispiel ist das alles recht einfach, da alle Models mit dem selben Shader gerendert werden, in komplexeren Spielen hat man natürlich viel mehr Shader auf einmal. Aber ich denke es zeigt doch schon mal wie man mit Shadern umgeht. Ich kann hier kein Tutorial bzw. nähere Erklärung zu GLSL liefern, dazu gibt's genügend andere Quellen, aber hier mal ein paar Links für diejenigen die sich ein wenig einarbeiten wollen:
- http://www.lighthouse3d.com/opengl/glsl/
- http://www.clockworkcoders.com/oglsl/tutorials.html
- http://www.ozone3d.net/index.php
Partikelsysteme:
Ich habe für jedes Partikelsystem eine eigene Klasse geschrieben, wie schon mal von mir erwähnt gibt es bessere Methoden um ein flexibles Partikelsystem zu schreiben (gescriptet zum Beispiel), aber für unser Beispiel ist es absolut ausreichend.Was noch übrig bleibt
Es gibt eigentlich nicht viel mehr dazu zu sagen, wie schon erwähnt ist der Quellcode gut dokumentiert, ich hoffe ich habe jetzt nichts mehr vergessenverwendete Tools:
Ich will hier nochmals kurz auf die Tools hinweisen, die für Worm-X benutzt wurden. Dies soll nur als Hinweis dienen, natürlich gibt es auch andere Programme die das selbe können.- Xcode (wer hätte das gedacht)
- Cinema 3D (3D Models)
- Modo 301 (3D Models / Intro)
- Wings 3D (3D Models)
- Photoshop (Texturen / Grafiken)
- Gimp (Texturen)
- Audacity (Sounds)
- Logic Pro (Hintergrundmusik)
- OmniGraffle (für das Diagramm)