Space Invaders OpenGL

Sucht man Informationen zur Spieleprogrammierung, sei es in einem Buchladen oder im Internet, wird man geradezu erschlagen damit.
Leider sind diese fast ausschließlich auf Windows mit C/C++ zugeschnitten. Um diese Lücke mal ein bisschen zu füllen, hab ich mich entschlossen, hier mal einige Tutorials zu Spieleprogrammierung mit Obj.C / OpenGL/OpenAL zu erstellen. Generell kann man sagen, das sich die Entwicklung von Spielen auf einem Mac, nicht sonderlich zu anderen Systemen unterscheidet.
Weshalb man natürlich auch Tutorials die eigentlich für Windows gemacht sind, sehr gut als Lernhilfe nehmen kann. Gerade im Bezug auf OpenGL/OpenAL gibt es so gut wie keine Unterschiede zwischen den einzelnen Plattformen (das ist ja auch das Schöne daran). Hat man erst einmal einen Grafik-Kontext (OpenGL) erstellt, ist das Zeichnen von Objekten auf allen Systemen absolut identisch.
Also wird ein Aufruf von:
glBegin( GL_TRIANGLES );            
   glVertex3f(  0.0f,  1.0f, 0.0f );    
   glVertex3f( -1.0f, -1.0f, 0.0f );    
   glVertex3f(  1.0f, -1.0f, 0.0f );   
glEnd();
auf eine Linuxrechner genauso ein Dreieck erstellen wie auf einem Win-/Mac.

OpenGL

Die Integration von OpenGL, ist auf keinem System so gut wie auf einem Mac. Gerade in Verbindung mit Obj.C lassen sich in kürzester Zeit z.B. Editoren bauen. Wer einmal mit dem Windows API einen Editor gebastelt hat, der weiss was ich meine.

OpenAL

Apple selbst empfiehlt OpenAL als Soundbibliothek wenn es darum geht, Spiele oder ähnliches zu vertonen. CoreAudio wäre in diesem Zusammenhang zu „Low-Level“. Etliche sehr bekannte Spiele wie z.B. Doom3, Unreal2 oder Hitman2 benutzen OpenAL. Gerade hierzu werden noch einige kleinere Tutorials folgen, da es hier meiner Meinung nach, nicht sehr viel Informationen gibt.

Los gehts:

Im ersten Teil der Serie habe ich mich entschlossen, einen kleinen Klassiker aus der Mottenkiste zu graben.
Es wird hier darum gehen, Space Invaders auf den Mac zu bringen. Aber warum gerade Space Invaders und warum gerade so was „einfaches“??.
Zum einen bin ich ein großer Retrofan und zum anderen finde ich, dass es ein guter und vor allem einfacher Einstieg in das Thema ist.
Das Internet ist voll von angefangenen Spielen, bei denen irgendwann keiner mehr Lust hatte sie fertig zu machen. Ich bin auch der Meinung das, wenn man was angefangen hat, es auch zu Ende bringen soll. Und mit so einem „kleinen Spielchen“ ist es wahrscheinlicher es fertig zu stellen, als wenn man sich gleich an ein riesen Projekt wagt, das dann aus mangelnder Selbsteinschätzung nie das Licht der Welt erblickt.
Wer Kenntnisse in Obj.C hat sollte keinerlei Probleme haben, den Code zu verstehen. Ich habe das Spiel absichtlich über mehrere Klassen verteilt, damit es schön übersichtlich bleibt. So lassen sich auch später sehr einfach Dinge dazubauen.
Das Ganze ist ein simples 2D Spiel, also müssen wir uns zu Anfang nicht mit irgendwelchen „schwierigen“ 3D-Berechnungen rumplagen
Natürlich hätte man es auch komplett ohne OpenGL realisieren können, aber da ich später ein wenig anspruchsvollerer Spiel (in 3D) machen will, kann es nichts schaden sich schonmal damit anzufreunden
Da ich mal davon ausgehe, das jeder Space Invaders kennt, werde ich nicht näher auf den Inhalt des Spiels eingehen
Beginnen wir mit dem GameLoop eines einfachen Spiels der unabhängig von der Art der Sprach in Pseudocode in etwa so aussieht:
Initialisierung
GameLoop:
	Prüfe Spieler-Input
	Animiere und bewege Spielobjekte
	Kollisionstests / Spielephysik
	Rendern / Soundausgabe
Gehe zurück nach GameLoop:
Aufräumen
Ende
In unserem Beispiel wäre der Ablauf wie folgt:
Wir erstellen uns einen OpenGL-Grafikkontext den wir ja zum rendern brauchen. Danach wird das Spiel initialisiert und anschließend in den GameLoop gesprungen.
Hier mal der GameLoop aus in Objective C und Cocoa:
// Immer wahr, Endlosschleife
while (1) 
{	
        // Speicherverwaltung soll das System übernehmen	
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        // Gibt die aktuelle Zeit
	start = [NSDate timeIntervalSinceReferenceDate];
 
        // Resetet OpenGL
	glLoadIdentity();
	glClear( GL_COLOR_BUFFER_BIT );
 
        // Escape beendet die while-Schleife		   
	if([self pollingKeyboard] == ESC)
	{
		break;
	}
 
        // Spiellogik	
	switch (_gameState)
	{
	       case GAMESTATE_INTRO:
		{
			[_intro renderIntro:_timeDelta];
		}break;
		case GAMESTATE_PLAY:
		{		
			[self recycleDeadObjects];
 
			if([_player isDead])
			{
				[self playerIsDead];
			}
			else
			{
				[self process];		
				[self render];	
			}
		}break;
		case GAMESTATE_GAMEOVER:
		{
		  [self renderInfoText];
		  [_gameOver renderGameOver:_timeDelta];				
		}break;
	}
	// Sendet die Daten an die Grafikkarte
	[context flushBuffer];
        // Gibt die aktuelle Zeit
	end = [NSDate timeIntervalSinceReferenceDate];
        // Errechnet, wieviel Zeit der GameLoop brauchte
	_timeDelta = end-start;
        // Speicherverwaltung
	[pool release];
}

Erklärung:

Da wir uns in einer Endlosschleife befinden müssen wir mindestens 2 Punkte bachten
einen AutoreleasePool erstellen (da wir den Standard AutoreleasePool durch unsere Endlosschleife ja aushebeln) Uns um das EventProcessing selbst kümmern (später mehr dazu) aus dem selben Grund wie Punkt 1 Eine andere Möglichkeit wäre einen Timer zu installieren und den RunLoop über diesen zu triggern, was meiner Meinung nach aber nicht die beste Lösung wäre. Da doch sehr viel an Rechenzeit auf der Strecke bleiben würde.
Nun zum Code wie gesagt wir erstellen einen AutoreleasePool 1) und holen uns danach die ReferenzZeit. Diese benötigen wir, weil unser Spiel unabhängig von den erzielten Frames Per Seconds (FPS) auf allen Systemen gleich schnell laufen soll.

Einwurf FPS:

Ich habe in diesem Spiel komplett auf einen Framecounter verzichtet, in der Praxis ist er aber in so fern sehr wichtig, als das man zu jeder Zeit wissen sollte wie schnell ein Spiel denn läuft. Hier geht es nicht ums Protzen ala MEINE ENGINE SCHAFFT 3000 FPS, sondern schlicht darum, herauszufinden warum z.B. das Rendern eines simplen Models unsere Framerate total in den Keller zieht.
Eine noch spielbare Framerate liegt bei ca. 30, das heißt alles was stark unter diesen Wert fallen würde, würde nur noch unspielbar über den Schirm ruckeln. Interresant ist auch, das ein Spiel auch durchaus zu viel Frames erreichen kann. Was sich jetzt vielleicht seltsam anhört, läßt sich einfach erklären. Wir speichern die Zeit und die Bewegung der einzelnen Objekte in float-Werten, wenn nun aber sehr viele Frames erreicht werden (der Zeitwert ist z.B. 0.000001) käme es irgendwann aufgrund der Genauigkeit von float-Werten dazu, das sich unsere Objekte gar nicht mehr bewegen würden, weil der Zeitwert einfach zu viele Nachkommastellen hat, die unser float nicht mehr darstellen kann.
Ein Ausweg wäre z.B. einen double zu nehmen, da dieser die doppelte Genauigkeit eines float-Wertes hat, allerdings braucht dieser auch mehr Speicher.
Eine andere Möglichkeit wäre, ein Framebremse einzubauen, die z.B. bei einer Framerate über 120, solange irgendwas machen würde, bis die Rate wieder darunter fallen würde.

Weiter im Code
Danach folgen

glLoadIdentity();
glClear( GL_COLOR_BUFFER_BIT );
womit wir die Einheits Matrix laden und den Bildschrim löschen. Danach prüfen wir in
if([self pollingKeyboard] == ESC)
{
   break;
}
ob der Anwender die ESC-Taste gedrückt hat, wenn dem so ist, springen wir aus der while-Schleife raus und kehren zu OpenGLContext.m zurück, dort werden dann die benötigten Aufräumarbeiten durchgeführt und das Spiel beendet. Innerhalb der switch-Anweisung prüfen wir, in welchem Zustand unser Spiel ist:
  • GAMESTATE_INTRO: zeigt den Introschirm an
  • GAMESTATE_PLAY: das eigentliche Spiel
  • GAMESTATE_GAMEOVER: der Gameoverschirm
und zum Schluß führen wir
[context flushBuffer];
end = [NSDate timeIntervalSinceReferenceDate];
_timeDelta = end-start;
[pool release];
aus.
Beim Anlegen unseres Grafik-Kontext haben wir mit
ADD_ATTR(NSOpenGLPFADoubleBuffer);
OpenGL angewiesen einen Doublebuffer anzulegen, was so viel bedeutet, das ein „zweiter unsichtbarer Bildschirm“ erzeugt wird, auf den dann gezeichnet wird. Wenn dann der Zeichenvorgang beendet ist, werden beide „Schirme“ mittels
[context flushBuffer];
getauscht, also der unsichtbare Hintergrund wird in den Vordergrund gebracht. Danach holen wir uns wieder die Zeit und berechnen die Differenz die gebraucht wurde um einmal durch unseren RunLoop zu gehen, mit dieser Differenz werden dann alle Objekte animiert und bewegt.
Zum Schluß wird der Pool released und es geht wieder von vorne los.

Erklärung der Klassen

Ich will hier nun auf die einzelnen Klassen etwas genauer eingehen.

GlobalDefinitions.h

globale Definitionen die im Spiel gebraucht werden

SoundManager

Diesen hab ich als typischen Singleton angelegt, weil er ja nur einmal pro Spiel gebraucht wird. Zu Anfang werden alle benötigten Sounds geladen (loadSounds) Zum Abspielen eines Sounds reicht dann ein simples playSound mit dem angegebenen SoundIndex.

GameObjectsHandler

Alle Objekte die auf dem Schirm angezeigt werden (Spieler, Gegner Schüsse. usw…) werden von GameObject abgeleitet. Dieser GameObjectsHandler hat ein simples MutableArray das eben diese Objekte aufnehmen kann. So ist es sehr einfach alle Objekte in einem Durchlauf zu animieren, bewegen, auf Kollision zu prüfen und natürlich auch zu rendern.

GameObject

wie eben schon erwähnt sind alle Objekte von GameObject abgeleitet. GameObject ist im Prinzip die abstrakte Klasse für alle Objekte auf dem Schirm. In C-PlusPlus wäre das dann eine virtual-Class Diese speichert unter anderem die Position, die Größe und die Geschwidigkeit eines Objektes

Player

Hierzu muss nicht viel gesagt werden, der Spieler besitzt alle Attribute und Methoden von Gameobject und wird demenstrechend bewegt und gerendert, eine Animation gibt es nicht, der Spieler besteht aus einem einzigen simplen Bild. Damit der Spieler nicht ständig schießen kann, hab ich eine kleine Zeitsperre eingebaut die so aussieht:

if(!_canShot)
{
_counter += 1.5*timeDelta;
if(_counter >1.0f)
{
 _canShot = YES;
 _counter = 0.0f;
}
}
Was bedeutet wenn er gerade geschossen hat (_canShot) dann wird ein counter hochgezählt. Hat dieser einen bestimmten Wert (>1.0f) dann kann er wieder schießen. Hat er geschossen wird ein Shot-Objekt erzeugt un dem GameObjectsHandler übergeben. Und zum Schluß noch ein Schuß-Sound abgespielt.

Invader

Der Invader macht zu Anfang 8 und später dann immer 16 Steps in eine Richtung, danach rutscht er um 35 Pixel nach unten und bewegt sich in die entgegengesetzte Richtung. Zu jeder Bewegung wird immer der passende Sound abgespielt. Und mit

long r=random();
if(r/1000 < 45000)
.
.
ziehen wir eine Zufallszahl, sollte diese kleiner als als der genannte Wert sein, wird ein Schuß erzeugt dieser wieder dem GameObjectsHandler übergeben. Die Animation besteht aus nur 2 Bilder weshalb das Rendern auch ziemlich einfach ausfällt:
glBindTexture(GL_TEXTURE_2D, _texture);
glBegin(GL_QUADS);
 
if(_animSequence)
{
        glTexCoord2f(0, 0);		glVertex2i(_xPos,_yPos);	                // unten links
	glTexCoord2f(0.5f, 0);		glVertex2i(_xPos+_xSize, _yPos);		// unten rechts
	glTexCoord2f(0.5f, 1.0f);	glVertex2i(_xPos+_xSize, _yPos+_ySize);		// oben rechts
	glTexCoord2f(0, 1.0f);		glVertex2i(_xPos,_yPos+_ySize);	                // oben links
}
else
{
	glTexCoord2f(0.5, 0);		glVertex2i(_xPos,yPos);	                        // unten links
	glTexCoord2f(1.0f, 0);		glVertex2i(_xPos+_xSize, _yPos);		// unten rechts
	glTexCoord2f(1.0f, 1.0f);	glVertex2i(_xPos+_xSize, _yPos+_ySize);		// oben rechts
	glTexCoord2f(0.5f, 1.0f);	glVertex2i(_xPos,yPos+_ySize);	                // oben links
}	
glEnd();
Wir binden unsere Texture und rendern abhängig von _animSequence, entweder den ersten oder den zweiten Bildausschnitt der Textur. Das realisieren wir, indem wir die TexturKoordinaten in

glTexCoord2f
anpassen.

Shot / ShotInvader

Interressant hier ist nur, das wenn der Schuß sich ausserhalb einer bestimmten Y-Koordinate befindet, das er dann automatisch gelöscht wird:

//Bewegen
_yPos+=_velocity*timeDelta;
 
//Schuss außerhalb des Bildschirms dann löschen
if( _yPos >(HEIGHT-100) || (_yPos+_ySize) < 0)
{
	_isDead=YES;	
}

Explosion

Diese wird immer dann erzeugt wenn der Spieler / Invader getroffen wird. Nach Ablauf einer bestimmten Zeit wird sie wieder gelöscht:

if(_animCounter > _holdAnimation)
	_isDead = YES;

Saucer

Nichts besonderes, auch hier wird geprüft ob er ausserhalb des Bildschirms liegt und entsprechend gelöscht:

_xPos += _velocity*timeDelta;
 
if( (_xPos > WIDTH+100) || (_xPos <-100) )
	_isDead = YES;

Shield / Bullethole

Hier wird zuerst ein Pattern aufgebaut:

unsigned char stips[]={	1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,
			1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,
			1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
			1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
			1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
			1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
			0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,
			0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
			};	
Dieses Muster wird dann „geparst“, sollte eine 1 dort stehen wird ein Block (BulletHole) erzeugt. Der vom Spieler oder vom Gegner abgeschossen werden kann.
Anders als bei z.B. dem Spieler gibt es hier keine Textur, hier wird lediglich pro Block ein weises Rechteck gerendert. Weshalb auch zuerst die Texturierung ausgeschaltet wird.
	
	[[OpenGLState sharedManager]setTextureState2D:NO];
Im Prinzip hätte man die Texturierung nicht explizit ausschalten müssen, da wenn man mit aktivem TexturStatus rendert und keine gültige Texture mittels

	glBindTexture(GL_TEXTURE_2D, _texture);
gesetzt hat, OpenGL einfach das Objekt ohne Texture rendert.

Info zu Texturen unter OpenGL

Man sollte darauf achten das wenn man eine Textur erzeugt diese „Power of 2“ ist, also z.B. 128×128 oder 512×512 usw… Es gibt zwar eine Extension ARB_texture_non_power_of_two die es erlaubt Texturen in jeder Größe zu erstellen, diese wird aber nicht von allen Grafikkarten unterstützt (meiner z.B.)

Kleine Einlage Wichtig!!

OpenGL arbeitet bekanntlich wie eine Statemachine, das heisst, man setzt bestimmte Parameter durch explizites ein/ausschalten. Beispiel

	glEnable(GL_TEXTURE_2D);
würde die Texturierung einschalten. Was jetzt aber interessant ist, ist das OpenGL NICHT prüft ob ein Status gesetzt ist. Das bedeutet, ein Aufruf von
glEnable(GL_TEXTURE_2D);
glEnable(GL_TEXTURE_2D);
würde tatsächlich 2 mal ausgeführt, was natürlich Quatsch wäre und Geschwindigkeitseinbusen mit sich bringt. Um dies Vorzubeugen gibt es 2 Möglichkeiten, entweder man holt sich einen bestimmten Zustand direkt über 1.

 GLboolean glIsEnabled( GLenum	 cap);
bzw.

 void glGetBooleanv( GLenum pname, GLboolean *params);
Nachdem was ich bis jetzt über OpenGL gelesen habe, sollt man wann immer es möglich ist, es unterlassen, Werte auszulesen, da dies die Performance in den Keller zieht. 2. Man baut sich etwas, das eben genau diesen Status prüft und bei Bedarf updated.
Genau das tut in unserem Fall die Klasse OpenGLState. Dort wird geprüft ob ein bestimmter Status gesetzt ist, wenn ja wird nichts weiter gemacht, oder eben bei Bedarf aktualisiert.

- (void)setTextureState2D:(BOOL)value 
{
   if (_textureState2D != value) 
   {
       _textureState2D = value;
	if(value)
		glEnable(GL_TEXTURE_2D);
	else
		glDisable(GL_TEXTURE_2D);
   }
}
Wichtig ist nun aber, das der Status von OpenGL über diese Klasse gesetzt wird. Wenn wir einmal direkt den Status über

glEnable(GL_TEXTURE_2D);
setzten und einmal über unsere Klasse, würde das ganze System durcheinader geraten und würde somit nicht mehr richtig funktionieren. In diesem Zusammenhang, sollte man noch erwähnen, das auch eine ständige Veränderung dieser OpenGL-States vermieden werden sollte. Ein schlechtes Beispiel wäre folgendes (Pseudo):

schlalte Licht aus
rendere Text
	
schalte Licht ein
rendere Models
	
schalte Licht aus 
rendere 2D Sprites
	
schalte Licht ein
rendere andere Models
Besser wäre folgendes

schlalte Licht aus
rendere Text
rendere 2D Sprites
	
schalte Licht ein
rendere Models
rendere andere Models
Natürlich wäre das der Idealfall, der sich aber leider nicht immer einhalten läßt. Man sollte sich deshalb vorher Gedanken machen, wann was gerendert werden soll (Hier hilft manchmal ein Blatt Papier und ein Bleistift).

Intro

Wie der Name schon sagt, wird über diese Klasse das Intro erzeugt und gerendert und zwar so lange bis der Anwender die Eingabetaste gedrückt hat. Ich habe hier noch einen kleinen Schreibmaschinen-Effekt eingebaut

//zweite Zeile SPACE INVADERS
if(_lineOneIsDone && !_lineTwoIsDone)
{
	if(counter >NEXTCHARACTERATTIME)
	{
		counter =0.0f;
		_charCountLineTwo++;
		if (_charCountLineTwo >[_lineTwo length]) 
		{
			_charCountLineTwo = [_lineTwo length];
			_lineTwoIsDone = YES;
		}
	}
}
[_introText drawTextToScreen:[_lineTwo substringToIndex:_charCountLineTwo]
			onXPosition:WIDTH / 2  -  ( (FONTSIZE*[_lineTwo length])/2)
			onYPosition: 550
			withColor:[NSColor colorWithCalibratedRed:1.0 green:1.0 blue:1.0 alpha:1.0]];
Wer sowas mal am 64'er gemacht hat, wird sich freuen, wie einfach man es hier hat

Game

Diese Klasse hatten wir schon weiter oben angesprochen. Was ich noch kurz erklären will ist das Event-Handling. Wie schon erwähnt, wird durch die Endlos-Schleife (while(1) ) das Standard EventHandling komplett ausgehebelt. Weshalb wir uns selbst kümmern müssen. Was so aussieht:

/**
Tastatur abfragen
**/
-(int)pollingKeyboard
{
	NSEvent *event;
	event = [NSApp nextEventMatchingMask:NSAnyEventMask 
						untilDate:[NSDate distantPast] 
						inMode:NSDefaultRunLoopMode dequeue:YES];		
 
	NSEventType type = [event type];		
 
	if(type == NSKeyDown)
	{
Im Prinzip interessiert uns nur ein KeyUp, KeyDown und ein NSWindowMovedEventType (im Windowmodus) und genau diese verarbeiten wir auch, der Rest geht an uns vorbei. Eine kleine Unschöhnheit gibts es noch mit der Standard KeyUp bzw. KeyDown und zwar wird ja beim Halten einer Taste kurz nachdem das Event gesendet wurde, ein Aussetzter gemacht (einfach mal in einem Texteditor ne Taste gedrückt halten, dann wisst ihr was ich meine), dies ist in unserem Spiel aber nicht ewünscht, wehalb ich die beiden Hilfsvariablen:

BOOL				_leftArrowIsPressed;	//linke Pfeiltaste gedrückt
BOOL				_rightArrowIsPressed;	//rechte Pfeiltaste gedrückt
eingebaut habe, diese werden immer dann gesetzt wenn eine Taste gedrückt/losgelassen wurde. So haben wir eine Ruckelfreie Bewegung unseres Players.

Kollisionserkennung

Unser Spiel arbeitet mit einer simplen 2D Rect→Rect Kollisionsprüfung, diese hat den Vorteil das sie sehr schnell ist. Allerdings manchmal etwas ungenau. Im Prinzip wird um jedes Objekt ein Rect-Bereich definiert, dieser wird dann für die Kollisionserkennung hergenommen

Dort wird einfach nur geprüft, ob beide Rechtecke überlappen, wenn ja dann lösen wir eine Kollision aus.

Kollisionserkennung

In unserem Beispiel wird auf folgende Kollisionen geprüft:

Invaderschuss → Spielerschuss → Spieler → Shield
Saucerschuss → Spielerschuss → Spieler → Shield
Spielerschuss → Invaderschuss → Saucerschuss → Invader → Saucer → Shield
Zu den Shields will ich noch was anmerken, hier wird zuerst geprüft ob ein Schuss das Shield überhaupt trifft. Wenn ja wird durch die einzelnen Blocks (Bulletholes) gegangen und dort auf Kollision geprüft. Die Klasse Shield selbst verwaltet einen Array der die Blocks enthält, dort wird dann auch diese 2. Kollisionsprüfung gemacht. Das hat folgenden Vorteil, wenn ein Schuss nicht das Shield trifft, kann er auch nicht die Blocks getroffen haben die im Shield liegen, somit können wir uns eine Kollisionsprüfung sparen (clever gelle ).

GameOver

Im Prinzip das gleiche wie die Intro-Klasse nur eben für einen GameOver Schirm

OpenGLContext

Diese Klasse ist ürsprünglich von der OmniGroup, die es im Original erlaubt, über einen Dialog bestimmte Einstellungen zu machen (Bildschirmauflösung…) diesen Teil habe ich herraus genommen und diese Parameter in die GlobalDefinitions.h hinterlegt. Wie gesagt legt diese Klasse einen OpenGL Kontext an (Windowed Fullscreen).

TextToScreen

Wie der Name schon sagt, kann man hierrüber 2D Text rendern. Diese Klasse ist nicht von mir Ich hab sie um das Feature erweitert, das man auch Fonts laden kann die nicht im System registriert sind.

MAGTextureObject / MAGTextureManager

Der TextureManager verwaltet TexturObjekte. Um ein Texture für OpenGL zu erstellen, „frägt“ man den TextureManager über

- (GLuint) textureByName:(NSString *)textureName ofType:(NSString*)imageType;
nach eben dieser, dazu gibt man den Namen (es wird im App Bundle gesucht) und das Suffix an Besipiel:

_playerTexture = [[MAGTextureManager sharedManager]textureByName:@"player" ofType:@"tif"];
lädt die Texture 'player.tif', bei Erfolg bekommt man einen GLuint-Wert größer 0. Sollte diese noch nicht bestehen wird ein neues TextureObjekt erzeugt und in einem Array verwaltet, gibts die Texture schon, wird sie nicht neu geladen (was ja der Sinn eines TextureManagers ist ) sondern die schon bestehende zurückgegeben. Ich hab in Space Invaders mit tif's gearbeitet, weil diese einen Alpha-Kanal mitspeichern können, damit läßt sich sehr einfach eine transparenete Farbe bestimmen, die beim Rendern nicht angezeigt wird. Dies erreichen wir über Blending:

[[OpenGLState sharedManager]setBlending:YES];
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
löschen der toten Objekte: Hier wird einfach durch den ObjektManager gegangen und alle Objekte die „isDead“ sind werden gelöscht.

-(void)recycleDeadObjects
{
	NSEnumerator *recycler = [[[GameObjectsHandler sharedManager]allObjects] objectEnumerator];
	GameObject *item;				
 
	while (item = [recycler nextObject]) 
	{
	           if([item isDead])
			[[GameObjectsHandler sharedManager]removeObject: item];
	}	
}
Jetzt fragt sich vielleicht der eine oder andere, warum ich die Objekte nicht gleich lösche wenn sie getroffen wurden. Weil wir sonst auf nicht mehr existierende Objekte in unserem ObjektManager zugreifen würden, was natürlich einen Fehler verursachen würde.

Der Schluß

ich habe den Code nochmal schön kommentiert, falls es doch Schwierigkeiten geben sollte. So, ich hoffe es hat ein bischen Spaß gemacht, für die nächsten Teile werde ich mir dann was Besonderes einfallen lassen. Ich hoffe ich hab keine Bugs vergessen. Etwaige Schreibfehler sind beabsichtigt. Über Verbesserungsvorschläge freue ich mich immer. In diesem Sinne, viel Spaß beim coden. wolf_10de

Projekt runterladen

SpaceInvaders OpenGL
der Quellcode steht unter der GPL-Lizenz

1) sicherlich sollte man so entwicklen, das man OHNE einen AutoreleasePool auskommt, da dieser doch einiges an Performance braucht, was heissen würde, man müsste den Code so umbauen, dass KEINE Objekte mehr per CA erstellt werden.