Spieleentwicklung 101 - Android Wiki - AndroidPIT

90
2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 1/90 Spielentwicklung 101 von Mario Zechner steht unter einer Creative Commons NamensnennungNicht kommerziellWeitergabe unter gleichen Bedingungen 3.0 Unported Lizenz . [Verstecken] Seite Diskussion Quelltext betrachten Versionen/Autoren Spieleentwicklung 101 Inhaltsverzeichnis 1 Danksagung 2 Einleitung 3 Was muss ich vorab wissen? 4 Wo fang ich an? 5 Und auf Android? 4.1 Das Applikationsgerüst 4.2 Das Eingabe Modul 4.3 Das Datei I/O Modul 4.4 Das Grafik Modul 4.5 Das SoundModul 4.6 Das NetzwerkModul 4.7 Das SimulationsModul 5.1 Android Activity 5.2 Touch Screen & Accelerometer 5.3 Ressourcen, Assets und die SDKarte 5.4 OpenGL ES 5.3.1 Ressource 5.3.2 Assets 5.3.3 SD Karte 5.4.1 Grundlegendes zur Grafikprogrammierung 5.4.2 Ein wenig Mathematik 5.4.3 Das erste Dreieck 5.4.4 Farbspiele 5.4.5 Texturen 5.4.6 Mesh & Textur Klasse 5.4.7 Projektionen 5.4.8 Kamera, ZBuffer und wie lösche ich den Schirm 5.4.9 Licht und Schatten 5.4.10 Transformationen 5.4.11 Text zeichnen FO K LOGIN Übersicht Forenregeln Mods + Admins Wiki Community

description

Android, Wiki, 101, Beginner

Transcript of Spieleentwicklung 101 - Android Wiki - AndroidPIT

Page 1: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 1/90

Spielentwicklung 101 von Mario Zechner steht unter einer Creative Commons NamensnennungNichtkommerziellWeitergabe unter gleichen Bedingungen 3.0 Unported Lizenz .

[Verstecken]

Seite Diskussion Quelltext betrachten Versionen/Autoren

Spieleentwicklung 101

Inhaltsverzeichnis

1 Danksagung

2 Einleitung

3 Was muss ich vorab wissen?

4 Wo fang ich an?

5 Und auf Android?

4.1 Das Applikationsgerüst4.2 Das Eingabe Modul4.3 Das Datei I/O Modul4.4 Das Grafik Modul4.5 Das SoundModul4.6 Das NetzwerkModul4.7 Das SimulationsModul

5.1 Android Activity5.2 Touch Screen & Accelerometer5.3 Ressourcen, Assets und die SDKarte

5.4 OpenGL ES

5.3.1 Ressource5.3.2 Assets5.3.3 SD Karte

5.4.1 Grundlegendes zur Grafikprogrammierung5.4.2 Ein wenig Mathematik5.4.3 Das erste Dreieck5.4.4 Farbspiele5.4.5 Texturen5.4.6 Mesh & Textur Klasse5.4.7 Projektionen5.4.8 Kamera, ZBuffer und wie lösche ich den Schirm5.4.9 Licht und Schatten5.4.10 Transformationen5.4.11 Text zeichnen

FOLGE UNS:

Kategorien

MAGAZIN APPS HARDWARE FORUM LOGIN

Übersicht Forenregeln Mods + Admins Wiki

Community

Page 2: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 2/90

Großer Dank gebührt Antonia Wagner. Sie hat in minutiöser Kleinstarbeit all die großen und kleinenorthographischen und grammatischen Drachen aus diesem Artikel entfernt. Danke Antonia!

Dieser Artikel soll in mehreren Etappen an das Thema Spieleentwicklung heranführen. Eine allgemeineEinführung in die Thematik bildet dabei den Grundstock. Diese soll Grundbegriffe und Mechanismen im Überblickerklären, anschließend werden die einzelnen Teilaufgaben bei der Entwicklung eines Spiels erklärt. Abschließendwerden die so erarbeiteten Themen in eine kleine Space InvadersVariante gegossen.

Als kleine Vorwarnung: Ich schreibe seit ca. zehn Jahren kleinere und größere Spiele. Meine Herangehensweiseist sicher nicht die Optimalste. Auch können mir im Rahmen dieses Artikels faktische Fehler unterlaufen, ich werdeaber versuchen, diese zu vermeiden. Auch werde ich, wo angebracht, Anglizismen verwenden, da diese dasGooglen nach weiterführendem Material erleichtern. Außerdem werde ich, für Begriffe, die man eventuellNachschlagen möchte, einige WikipediaLinks einbauen. Also: Los geht's.

In diesem Artikel setze ich ein paar Dinge als Minimum voraus. Keine Angst, höhere Mathematik gehört nicht dazu;)

6 Space Invaders

5.5 SoundPool und MediaPlayer

5.4.12 Meshes laden

6.1 Analyse des Originals6.2 Das Spielfeld6.3 Die Simulation

6.4 Das Rendering

6.5 Sound und Musik

6.6 SpaceInvaders Activity & Screens

6.7 Performance Tipps6.8 Abschließende Worte

6.3.1 BlockKlasse6.3.2 ExplosionKlasse6.3.3 ShotKlasse6.3.4 ShipKlasse6.3.5 InvaderKlasse6.3.6 SimulationKlasse

6.4.1 Renderer Klasse

6.5.1 SoundManager Klasse

6.6.1 StartScreen Klasse6.6.2 GameOverScreenKlasse6.6.3 GameLoopKlasse6.6.4 SpaceInvaders Activity

6.8.1 Allgemeine Spieleentwicklung6.8.2 OpenGL ES

Danksagung

Einleitung

Was muss ich vorab wissen?

JavaHandhabung von Eclipse

Spezialseiten

Page 3: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 3/90

Den Code zu diesem Tutorial könnt ihr euch per SVN von der Adresse http://androidgamedev.googlecode.com/svn/trunk/ holen. Im Projekt gibt es eine MainActivity die euch gleich wie in den SDKDemos eine Liste an Applikationen zeigt. Einfach das Projekt aus dem SVN herunterladen, in Eclipse importierenund eine Run Configuration anlegen und die Default Launch Activity starten.

Am Anfang jedes Spiels steht eine Idee. Wird es ein Puzzler? Ein Rundenstrategiespiel? Ein FirstPersonShooter? Auch wenn diese Genres unterschiedlicher nicht sein könnten, so unterscheiden sie sich im Grundeihres Daseins oft wenig. Ein Spiel kann in mehrere Module zur Erledigung diverser Aufgaben eingeteilt werden:

Im Folgenden wollen wir uns mit diesen sechs Modulen etwas genauer beschäftigen.

Das Applikationsgerüst stellt die Basis für das Spiel dar. In der Regel ähnelt dieses herkömmlichenApplikationsgerüsten nicht. Spiele sind in den meisten Fällen nicht Eventbasiert, d.h. sie laufen ständig, zeichnendabei die Spielwelt permanent neu, holen sich dauernd neue Benutzereingaben und simulieren die Welt (fürKartenspiele und Ähnliches muss dies natürlich nicht gelten). Wenn man nicht gerade für eine Konsoleprogrammiert, stellt sich einem jedoch das Problem, dass die meisten Betriebssysteme eventbasierteProgrammierung als Paradigma gewählt haben. So auch auf Android. Applikationen werden dabei nur bei Bedarfneu gezeichnet, zum Beispiel wenn der Anwender Text eingibt, einen Button drückt und so weiter. Es wird also nurdann Code ausgeführt, wenn es eine Benutzereingabe gibt. Im Allgemeinen hebelt man dies aus, indem maneinen separaten Thread startet, der das Betriebssystem veranlasst, die Applikation permanent neu zu zeichnen. Indiesem Thread befindet sich so gut wie immer eine Schleife, auch Main Loop genannt, innerhalb derer sich immerdasselbe abspielt. Das sieht stark vereinfacht so aus:

while( !done ) processInput( ) simulateWorld( ) renderWorld( )

Wie dieser Main Loop genau aussieht, hängt von vielerlei Faktoren ab, zum Beispiel dem verwendetenBetriebssystem, dem Spiel selbst und so weiter.

Im Rahmen dieses Artikels werden wir sehen, wie man dieses Konzept äußerst einfach auf Androidimplementieren kann.

Installiertes Android SDK sowie Eclipse PluginActivity Life CycleErstellen eines neuen Android Projekts in Eclipse

Wo fang ich an?

ApplikationsgerüstEingabe ModulDatei I/O ModulGrafik ModulSound ModulNetzwerk ModulSimulationsModul

Das Applikationsgerüst

Das Eingabe Modul

Page 4: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 4/90

Um dem Spieler die Möglichkeit zu geben, in das Spielsystem eingreifen zu können, müssen dessen Eingabenirgendwie gelesen werden. Diese Aufgabe übernimmt das EingabeModul. Tastaturen, Mäuse, TouchScreens,Joysticks, Gamepads und einige andere exotische Möglichkeiten stehen hier zur Verfügung. Wie man an dieEingabe kommt, ist dabei wieder betriebssystemabhängig.

Android verfügt über einige Eingabe Möglichkeiten, wir werden uns mit den beiden wichtigsten beschäftigen

Alle Ressourcen eines Spieles müssen in irgendeiner Form der Applikation zugänglich gemacht werden. Inspeziellen Fällen kann das Spiel diese Onthefly zur Laufzeit prozedural selbst erstellen, meistens liegen dieseaber in Form von Dateien auf einem Datenträger vor. Auch hier gibt es verschiedene Möglichkeiten der Ablage:Dateien können schön geordnet in Ordnern abgelegt werden, wild in einem einzigen Ordner gespeichert seinoder gar in einer ZipDatei fein säuberlich gepackt vorliegen. Das I/OModul für Dateien soll dies abstrahieren,damit im Programmcode der Zugriff auf die Ressourcen erleichtert wird.

Android bietet hier mit seinem Ressourcen und AssetSystem schon einen netten Ansatz, auf das wir später nochzu sprechen kommen werden.

In unseren modernen Zeiten ist für viele Spieleentwickler dieser Teil eines Spiels wohl der wichtigste (teils zuLasten des Spielspaßes). Dieses Modul übernimmt die Darstellung sämtlicher grafischen Inhalte des Spieles, seidies das User Interface, welches in der Regel zweidimensional ist, oder die Spielewelt selbst, meist in der drittenDimension. Da Letzteres oft rechenintensiv ist, wird spezielle Hardware verwendet, um das Ganze zubeschleunigen. Die Kommunikation mit dieser Hardware, ihr klar zu machen, wie, wo, was gezeichnet werden soll,sowie die Verwaltung von grafischen Ressourcen, wie Bitmaps und Geometry (auch Meshes genannt) ist dieHauptaufgabe dieses Systems. Hierunter fallen auch Dinge, wie das Zeichnen von PartikelEffekten oder derEinsatz von so genannten Shadern (auf Android noch nicht möglich). Ganz allgemein kann festgehaltenwerden, dass die meisten Objekte, die simuliert werden auch eine grafische Entsprechung haben. MeinerErfahrung nach ist es äußerst hilfreich die simulierte Welt komplett unabhängig von der grafischen Darstellung zumachen. Das Grafikmodul holt sich lediglich Information von der WeltSimulation, hat aber auf diese keinenEinfluss. Wem das etwas zu vage ist: keine Angst, der Zusammenhang sollte spätestens beim Entwickeln desSpace InvaderKlons erkenntlich werden. Es sei jedoch gesagt, dass dieser Ansatz es erlaubt, das Grafik Modulbeliebig auszutauschen, zum Beispiel statt einer 2DDarstellung das ganze auf 3D zu portieren, ohne dass dabeider Simulationsteil geändert werden muss.

Aufmerksamen Lesern ist vielleicht der Zusammenhang zwischen dem GrafikModul und dem Main Loop bereitsaufgefallen. Neuen Grafikkarten wird meist mit Benchmarks zu Leibe gerückt, die die so genannten Frames perSecond (kurz FPS) oder Frame Rate messen. Diese geben an, wie oft der Main Loop in einer Sekundedurchlaufen wurde. Es wird also gezählt, wie oft der Main Loop (Input verarbeiten, Welt simulieren und das Ganzedann zeichnen) in einer Sekunde durchlaufen wird. Im Zuge unserer Unternehmung werden wir immer ein Augeauf die Frame Rate werfen, um etwaige Engpässe in unserem Spiel identifizieren zu können.

Später werden wir sehen, wie wir Android 2D und 3DGrafiken über OpenGL ES entlocken können.

Soundeffekte und Musik gehören zu jedem guten Spiel. Dementsprechend kümmert sich das SoundModul umdas Abspielen solcher Ressourcen. Dabei gibt es zwischen Soundeffekten und Musik einen wichtigenUnterschied. Soundeffekte sind in der Regel sehr klein (im KilobyteBereich) und werden direkt im Hauptspeichergehalten, da sie oft verwendet werden. Beispielsweise das Feuergeräusch einer Kanone. Musik wiederum liegt oftin komprimierter Form vor (mp3, ogg) und braucht unkomprimiert (und damit abspielbar) sehr viel Speicherplatz.

1. dem TouchScreen2. dem Accelerometer

Das Datei I/O Modul

Das Grafik Modul

Das SoundModul

Page 5: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 5/90

Sie wird daher meist gestreamed, das heißt bei Bedarf stückweise von der Festplatte oder einem anderen Medium(DVD, Internet) nachgeladen. Auf Implementationssbene macht dies oft einen Unterschied, da dieserNachlademechanismus meist selbst realisiert werden muss.

In Zeiten von SurroundSoundHeimsystemen legen Spieleentwickler auch Wert auf dreidimensionalen Klang.Meistens, so wie bei der Grafik, hardwarebeschleunigt. Android bietet diese Möglichkeit noch nicht, erlaubt aberrelativ schmerzlos das Abspielen von Soundeffekten und das Streamen von Musik, wie wir später noch sehenwerden.

Seit World of Warcraft und Counter Strike ist klar: an MultiplayerMöglichkeiten kommt kein modernes Spiel vorbei!Das NetzwerkModul hat dabei gleich mehrere Aufgaben zu stemmen. Auf der einen Seite handhabt es dieKommunikation mit etwaigen Servern, sendet Mitteilungen von Spielern herum, lädt von Spielern gebastelteLevels ins Netz und so weiter. Dies sind quasi administrative Aufgaben und haben nur indirekt mit demSpielgeschehen selbst zu tun. Auf der anderen Seite gilt es Spieledaten, wie aktuelle Positionen, das Abfeuernvon Kugeln, das entsenden von Truppen und vieles mehr, das den anderen Rechnern im Netz mitgeteilt werdenmuss, die an der Partie teilnehmen. Abhängig vom Genre des Spiels kommen hier verschiedene Methoden zumEinsatz, um das Spielgeschehen zu synchronisieren. Dieser Themenbereich ist so groß und komplex, dass ihmam besten ein eigener Artikel gewidmet werden sollte. Im Rahmen dieses Textes werde ich nicht weiter auf dieseKomponente eingehen.

Damit sich die Dinge im Spiel bewegen, muss man sie auch irgendwie antreiben. Das ist die Aufgabe desSimulationsmoduls. Es beinhaltet sämtliche Informationen zum Spielgeschehen selbst, wie die Position vonSpielfiguren, deren aktuelle Aktion, wie viel Munition noch übrig ist und so weiter. Auf Basis derBenutzereingaben, sowie der Entscheidungen einer möglicherweise implementierten Künstlichen Intelligenz, wirddas Verhalten der Spielobjekte simuliert. Die Simulation läuft dabei immer Schrittweise ab. In jedem Durchgangdes Main Loops wird die Simulation um einen Schritt vorangetrieben. Dies passiert zumeist zeitbasiert, das heißt,man simuliert eine bestimmte Zeitspanne. Für ein weiches Ablaufen wird als Zeitspanne meist die Zeit, die seitdem Zeichnen des letzten Frames vergangen ist, herangezogen. Ein kleines Beispiel: gegeben eineKanonenkugel, die mit 10m/s nach rechts fliegt, schreiten wir einen Schritt in der Simulation weiter. Die Zeitspanneseit dem letzten Frame beträgt 0.016s (16 Millisekunden, entspricht einer Frame Rate von 60fps). 10m/s * 0.016s =0.16m, das heißt die Kanonenkugel ist nach Abschluss dieses Simulationsschrittes um 16 Zentimeter weiter links,im Vergleich zum letzten Frame. Diese Art des Simulationsschrittes nennt man Frame Independent Movementund sollte Bestandteil jedes SimulationsModuls sein. Wie der Name schon sagt, ist es egal, wie viel FPS dasSpiel schafft, die Kanonenkugel wird sich auf allen Systemen gleich verhalten (wenn auch die Zwischenschritteandere sein mögen). Es sei angemerkt, dass man bei Verwendung von PhysikSystemen meist fixe Zeitschritteverwendet, da die kleinen Schwankungen beim Messen der Zeitspanne zwischen dem aktuellen und dem letztenFrame viele PhysikSysteme instabil machen können. Wir werden in unserem Space InvadersKlon keinegrandiosen Physikspielereien implementieren, daher bleiben wir bei der herkömmlichen zeitbasierten Methode,die die Frame Zeitspanne heranzieht.

Abhängig vom Spieletyp gehört auch die künstliche Intelligenz zum SimulationsModul. Diese kann sehr simpelAusfallen, zum Beispiel das Verhalten der Goombas in Super Mario, die nur dumm nach rechts und links laufen. InEchtzeitstrategiespielen kann diese schon um einiges komplexer werden. Der Terminus künstliche Intelligenz isthier streng gesehen auch nicht ganz korrekt, in Ermangelung eines besseren Begriffs bleiben wir aber einfachdabei.

Wir wollen nur für all die oben genannten Module eine Entsprechung auf Android entwickeln. Wir beginnen mit derActivity selbst und versuchen das Main LoopMuster dort zu implementieren. Das Verwalten von Dateien über

Das NetzwerkModul

Das SimulationsModul

Und auf Android?

Page 6: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 6/90

Ressourcen und Assets werden wir uns als nächstes ansehen. Anschließend werden wir uns näher mit OpenGLES beschäftigen und Android dazu bringen, für uns interessante Dinge zu zeichnen. Die Ausgabe vonSoundeffekten und Musik bildet den Abschluss dieses Kapitels, womit wir dann für unseren Space InvadersKlongerüstet sind.

Im Zuge dieses Kapitels werden wir wiederverwendbare Komponenten entwickeln, schließlich wäre es nichtsinnvoll, jedes Mal das Rad neu zu erfinden. Alle Codes könnt ihr unter [http://code.google.com/p/androidgamedev/ http://code.google.com/p/androidgamedev/ ] finden und per SVN auschecken. Das Projekt beinhaltetein paar Beispielprogramme zu den einzelnen Abschnitten, sowie den Space InvadersKlon selbst.

Das Grundgerüst unseres Spiels bildet eine simple Activity. Dabei ergibt sich ein klassisches Henne/Ei Problem:wir müssen hier schon mit OpenGL ES beginnen, ohne uns damit auszukennen. Aber keine Angst, das Ganzeerweist sich als relativ einfach.

Ziel dieses Kapitels wird es sein, eine lauffähige OpenGL ESActivity zu erstellen, die den grundlegenden ActivityLifeCycle respektiert. Seit SDK 1.5 gibt es die so genannten GLSurfaceView . Sie ist ein GUIBaustein undähnelt einer List View, die man einfach in die Activity einhängt. Die Initialisierung von OpenGL mit halbwegs gutenParametern wird dabei für uns übernommen. Des Weiteren startet sie einen zweiten Thread neben dem GUIThread der Activity, der das Neuzeichnen des Geschehens permanent anstößt. Hier sieht man schon ersteParallelen zum Main Loop Konzept. Wir werden davon Gebrauch machen.

Damit wir eine Möglichkeit haben, das Neuzeichnen selbst zu übernehmen, bietet die GLSurfaceView einListenerKonzept (auch Observer Design Pattern genannt). Eine Applikation, die sich in den RenderingThreadder GLSurfaceView einhängen möchte, registriert bei dieser eine Implementierung des Interface Renderer .Dieses Interface hat drei Methoden, die abhängig vom Status der GLSurfaceView aufgerufen werden.

public abstract void onDrawFrame(GL10 gl)public abstract void onSurfaceCreated(GL10 gl, EGLConfig config)public abstract void onSurfaceChanged(GL10 gl, int width, int height)

Die Methode onDrawFrame ist jene, die die GLSurfaceView jedes Mal beim Neuzeichnen aufruft. DenParameter gl, den wir dabei erhalten, werden wir später genauer besprechen.

Die Methode onSurfaceCreated wird aufgerufen, sobald die GLSurfaceView fertig initialisiert ist. Hier kann manverschiedene SetupAufgaben erledigen, wie zum Beispiel das Laden von Ressourcen.

Die Methode onSurfaceChanged wird aufgerufen, wenn sich die Abmessungen der GLSurfaceView ändern.Dies passiert, wenn der Benutzer das AndroidGerät kippt und so in den Portrait oder LandscapeModus schaltet.Die Parameter width und height geben uns dabei die Breite und Höhe des Bereiches an, auf den wir zeichnen undzwar in Pixeln. Diese Information werden wir später noch benötigen.

Unsere erste Activity hat also ein paar Aufgaben:

Für eine saubere Implementierung werden wir einfach die Klasse Activity ableiten und diese GameActivitynennen. Dieser verpassen wir ein Attribut vom Typ GLSurfaceView, den wir in der onCreate Methode der Klasseinstanzieren und in die Activity einhängen. Weiters implementiert unsere abgeleitete Activity das InterfaceRenderer. In zwei weiteren AttributVariablen speichern wir die aktuelle Größe des zu bemalenden Bereichs, diewir beim Aufruf der Methode onSurfaceChanged in Erfahrung bringen. Diesen Bereich nennt man im Übrigenauch Viewport . Wir werden diese Terminologie fortan übernehmen. Außerdem fügen wir noch GetterMethodenin die Klasse ein, damit wir später auf die Abmessungen zugreifen können.

Android Activity

Erstellen einer GLSurfaceView und Einhängen in die ActivitySetzen einer Renderer Implementierung für die GLSurfaceViewImplementierung der RenderererImplementierung

Page 7: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 7/90

Um den Activity Life Cycle auch sauber zu implementieren, müssen wir die Methoden onPause und onResumenoch überschreiben. In diesen Rufen wir dieselben Methoden auch für unsere GLSurfaceView auf. Dies ist nötig,damit verschiedene Ressourcen sauber verwalten werden können.

In unserer Activity werden wir auch gleich die Frame Rate und die Zeitspanne zwischen dem aktuellen und demletzten Frame messen. Die Zeitspanne nennt man auch Delta Time, wieder ein Begriff, den wir uns ab jetzt merkenwerden. Um eine genaue Zeitmessung im Millisekundenbereich zu gewährleisten, verwenden wir dieSystem.nanoTime Methode. Diese liefert uns die aktuelle Zeit in Nanosekunden als longTyp zurück. Für dieDelta Time merken wir uns den Zeitpunkt des letzten Frames als eigenes Attribut in der Klasse. Die Delta Timeselbst errechnen wir dann in der onDrawFrameMethode, indem wir einfach die aktuelle Zeit abzüglich der zuvorgespeicherten Zeit nehmen. Diese Delta Time speichern wir in einem weiteren Attribut, um später im Spiel einfachdarauf zugreifen zu können. Dies ist notwendig, da wir sie für das Frame Independent Movement benötigen. ZumAbschluss schreiben wir die aktuelle Zeit wieder in das Attribut , das für die Speicherung der Delta TimeBerechnung im nächsten Frame vorgesehen ist.

Als letzten PuzzleStein werden wir noch an unserem Design etwas feilen. Wir wollen unsere Activity ja nicht jedesMal neu schreiben, darum führen wir ein eigenes Listener Konzept ein. Dies machen wir über ein kleinesInterface, das zwei Methoden hat.

public interface GameListener public void setup( GameActivity activity, GL10 gl ); public void mainLoopIteration( GameActivity activity, GL10 gl );

Der GameActivity spendieren wir eine Methode setGameListener, der wir eine GameListenerImplementierungübergeben können. Die Activity merkt sich diesen Listener und ruft seine Methoden entsprechend auf. DieMethode Setup wird dabei nach dem Start des Spiels aufgerufen und ermöglicht es uns, Ressourcen zu laden, diewir dann später im Main Loop brauchen. In der GameActivity rufen wir diese Methode in onSurfaceCreated auf,falls ein GameListener gesetzt wurde. Die Methode mainLoopIteration implementiert den Körper des Main Loop.Hier werden wir später dann alles für unser Spiel nötige erledigen, wie die Welt zu simulieren oder diese zuzeichnen. Diese Methode wird in der Activity in onDrawFrame aufgerufen. Fangen wir mit der Programmierungeines neuen Spiels an: Als erstes implementieren wir lediglich eine Activity. Diese leitet direkt von GameActivity abund wir setzen ihr einen GameListener in der onCreate Methode. Der GameListener ist also das eigentlich Spiel.

Damit haben wir vorerst den Grundstock für unser erstes Spiel gelegt, eine voll Funktionsfähige Activity, die unsdie Verwendung von OpenGL erlaubt. Wir werden die GameActivityKlasse gleich noch ein wenig ausbauen, umdort auch Eingaben entgegennehmen zu können. Den Code für die Klasse könnt ihr euch unter[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/tools/GameActivity.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/tools/GameActivity.java ] ansehen.

Das Lesen von Benutzereingaben auf Android ist, wie vieles anderes, wieder über ein ListenerKonzeptimplementiert. Wir vernachlässigen hier Eingaben über den Trackball, die Tastatur oder das DPad, da dies denRahmen dieses Artikels wohl sprengen würde. Wir konzentrieren uns zuerst auf TouchEingaben und gehenspäter zum Accelerometer über.

Für die Eingabe per Touch brauchen wir ein GUIElement, das diese auch entgegennimmt. Mit der GLSurfaceViewin unserer GameActivity haben wir bereits einen geeigneten Kandidaten. Es gilt somit nur einen entsprechendenListener bei der GLSurfaceView zu registrieren. Das Interface, das wir implementieren wollen, nennt sichOnTouchListener und hat nur eine einzige Methode

Touch Screen & Accelerometer

Page 8: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 8/90

public abstract boolean onTouch(View v, MotionEvent event)

Für uns interessant ist der Parameter event vom Typ MotionEvent . Dieser beinhaltet die Koordinaten des TouchEvents, sowie die Aktion. Also ob der Finger gerade aufgesetzt wurde, ob er gezogen wird oder ob er wieder vomDisplay genommen wurde. Die Koordinaten sind dabei zweidimensional und relativ zum View, für den wir denListener registriert haben. Über die Methoden MotionEvent.getX() und MotionEvent.getY() erhalten wird dieWerte. Abhängig davon, ob wir im Landscape oder PortraitModus sind, sind die x und yAchse ausgerichtet. Diepositive xAchse zeigt dabei immer nach rechts, die positive yAchse nach unten. Der Nullpunkt befindet sich alsoin der oberen linken Ecke. Dies ist ein wichtiges Faktum, das vielen Neulingen am Anfang Probleme macht, da esnicht mit dem in der Schule gelernten, klassischen kartesischen Koordinatensystem übereinstimmt. DieKoordinaten werden dabei wieder in Pixel angegeben.

Welche Aktion gerade aktuell ist, liefert uns die Methode MotionEvent.getAction(). Wir werden auf die AktionenMotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP und MotionEvent.ACTION_MOVE reagieren. DieGameActivity lassen wir das Interface OnMotionListener implementieren. Wir spendieren ihr auch drei neueAttribute touchX, touchY und isTouched, in denen wir den aktuellen Status des TouchScreen speichern. Kommtein MotionEvent.ACTION_DOWNEvent daher, speichern wir die x und die yKoordinate in touchX bzw. touchYund setzen isTouched auf true, bei einem MotionEvent.ACTION_MOVE machen wir dasselbe und im Falle vonMotionEvent.ACTION_UP setzen wir isTouched auf false. Damit wir im GameListener auf den aktuellen Statuszugreifen können, geben wir der GameActivity auch noch GetterMethoden, um die Werte auslesen zu können.Das Auslesen des aktuellen Status nennt man allgemein auch Polling .

Es sei angemerkt, dass die onTouch Methode im GUIThread und nicht im RenderThread der GLSurfaceViewvom Betriebssystem aufgerufen wird. Normalerweise müsste man sich hier Sorgen um etwaige ThreadSynchronisierung machen. Da es sich bei den Attributen, die den Status halten aber um Plain Old Datatypeshandelt und das Schreiben auf diese atomar ist, können wir das hier einfach übersehen.

Die GameActivity registriert sich selbst als OnTouchListener bei der GLSurfaceView in der onCreateMethode, diewir dementsprechend erweitern.

Der Accelerometer ist ebenfalls wieder über ein ListenerKonzept ansprechbar (ja, das zieht sich so ziemlichdurch alles durch). Das entsprechende Interface nennt sich SensorEventListener . Diesen registriert man abernicht bei einem View, sondern beim SensorManager . Zugriff erhalten wir auf diesen wie folgt:

SensorManager manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);

Bevor wir uns dort registrieren können, müssen wir aber zuerst einmal prüfen, ob der Accelerometer überhauptverfügbar ist. Dies funktioniert so:

boolean accelerometerAvailable = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() > 0;

Ist ein Accelerometer vorhanden, können wir uns ohne große Probleme bei diesem registrieren:

Sensor accelerometer = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0);if(!manager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME ) ) accelerometerAvailable = false;

Vom Manager holen wir uns den ersten AccelerometerSensor, den wir finden (in der Regel gibt es davon nureinen). Danach registrieren wir uns über die SensorManager.registerListener() Methode. Dieser Vorgang kannfehlschlagen, deshalb prüfen wir auch, ob es geklappt hat. Der ParameterSensorManager.SENSOR_DELAY_GAME gibt dabei an, wie oft das Betriebssystem den Accelerometer abtastensoll, in diesem Fall oft genug, um für ein Spiel zu genügen.

Page 9: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 9/90

Was noch bleibt, ist das Verarbeiten der SensorEvents. Das machen wir in derSensorEventListener.onSensorChanged() Methode, die wir implementieren.

public abstract void onSensorChanged(SensorEvent event)

Ähnlich wie beim Verarbeiten von Touch bekommen wir hier wieder ein Event, in diesem Fall vom TypSensorEvent . Diese Klasse besitzt ein öffentliches Attribut namens values, das die für uns relevanten Werteenthält. Von diesen gibt es drei Stück, gespeichert an den Indizes 0 bis 2. Diese drei Werte geben dabei dieBeschleunigung in Meter pro Sekunde entlang der x, y und zAchse des AndroidGeräts an. Der maximal Wertbeträgt dabei jeweils +9.81m/s, was der Erdbeschleunigung entspricht. Hält man das AndroidGerät im PortraitMode, so gehen die positive xAchse nach rechts, die positive yAchse nach oben und die positive ZAchsegerade aus durch das Gerät.

Accelerometer Achsen

Dies bleibt auch so, wenn man das Gerät im LandscapeModus hält. Wir werden dann bei der Space InvadersUmsetzung sehen, wie wir diese Werte einsetzen können.

Gleich wie für TouchEvents spendieren wir der GameActivity einige neue Dinge. Zu Beginn wäre da ein neuesAttribut vom Typ floatArray. Diese hält unsere drei AccelerometerWerte. Außerdem lassen wir die Activity dasSensorEventListenerInterface implementieren. Zum Abschluss brauchen wir noch drei Methoden, die uns jeweilsden AccelerometerWert für eine Achse liefern und wir sind fertig. Gleich wie für TouchEvents können wir damitden AccelerometerStatus auslesen.

Eine BeispielApplikation, die den aktuellen Touch und AccelerometerStatus per LogCat ausgibt, findet ihr unter[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/InputSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/InputSample.java ]. Diese zeigt auch gleich,wie wir ab jetzt neue Samples und das eigentlich Spiel aufbauen werden. Wir leiten zuerst von GameActivity ab,registrieren uns in der onCreate Methode als GameListener bei uns selbst und befüllen dann die setup undrenderMethode mit unserem ApplikationsCode. Einfach und elegant.

DateiEin und Ausgabe auf Android ist ein weites Land. Mehrere Möglichkeiten stehen uns zur Verfügung, wirwerden kurz auf alle eingehen, kleine CodeStücke sollen illustrieren, wie man die einzelnen Möglichkeitenanwenden kann.

Ressource

Ressourcen stellen den von Google gewünschten Weg zur Verwaltung von Dateien dar. Sie werden im AndroidProjekt in speziell dafür vorgesehene Ordner gespeichert und sind dann im ApplikationsCode über Identifierdirekt ansprechbar. Für die Spieleentwicklung sind sie meiner Ansicht nach nur bedingt von Nutzen, da die

Ressourcen, Assets und die SDKarte

Page 10: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 10/90

vorgegebene Verzeichnisstruktur etwas einschränkt. Auf Ressourcen gibt es nur Lesezugriff, da sie direkt in derAPKDatei der Applikation abgelegt werden. Ähnlich wie Ressourcen in normalen Java JarDateien. Einenschönen Überblick bietet folgender Link , wir lassen Ressourcen einmal außen vor und wenden uns demnächsten Kandidaten zu.

Assets

Assets werden ebenfalls wie Ressourcen direkt in der APKDatei eingepackt. Der Vorteil liegt hier in der freienWahl der Verzeichnisstruktur. Sie ähneln damit viel mehr dem herkömmlichen Java RessourcenMechanismus(und sind mir daher auch sympathischer). Im AndroidProjekt kann man unter dem AssetsVerzeichnis seineeigene Struktur beliebig anlegen. Der Zugriff auf ein Asset läuft dabei, wie gewohnt, über InputStreams:

InputStream in = activity.getAssets().open( "path/to/resource" );

Wie Ressourcen sind auch Assets nur lesbar.

SD Karte

Wenn der Besitzer des AndroidGeräts eine SDKarte eingelegt hat, kann man in der Regel auf dieser schreibenund lesen. Dazu bedarf es in der Datei AndroidManifest.xml des Projektes eines Zusatzes:

<uses‐permission android:name="Android.permission.WRITE_EXTERNAL_STORAGE"/>

Ein und Ausgabe funktioniert dann über die herkömmlichen Java Klassen.

FileInputStream in = new FileInputStream( "/sdcard/path/to/file" );FileOutputStream out = new FileOutputStream( "/sdcard/path/to/file" );

Jetzt geht's ans Eingemachte. OpenGL ES ist eine Schnittstelle, die es uns erlaubt direkt mit der Grafikkarteeines mobilen Geräts zu sprechen. Der Standard wurde von mehreren Herstellern gemeinsam entworfen undlehnt sich stark an die Variante an, die in herkömmlichen PCs, aber auch in Workstations zum Einsatz kommt(genauer an die Version 1.3). Im Rahmen dieses Artikels werden wir uns die wichtigsten Dinge zu Gemüte führen,verständlicherweise kann ich hier aber nicht auf alles und jedes eingehen. Bevor wir uns in die Untiefen vonOpenGL ES stürzen, müssen wir uns aber noch ein paar grundlegende Dinge anschauen, die allgemein in derComputergrafik gelten.

Grundlegendes zur Grafikprogrammierung

Die Entwicklung im Grafikbereich war in den letzten Jahrzehnten extrem. Viele Dinge haben sich geändert, bei derProgrammierung blieb aber auch einiges gleich. Grundlage für so ziemlich jede Art von Grafikprogrammierung istder so genannte Frame Buffer . Dieser ist ein Teil des VideoRAM und entspricht in Java Termen einem großeneindimensionalen Array, in dem die Farbwerte jedes Pixels für das aktuell am Bildschirm angezeigte Bildgespeichert werden. Wie die Farbwerte codiert werden, hängt vom Bildschirmmodus ab. Hier kommt der Begriffder Farbtiefe ins Spiel. Diese spezifiziert, wie viele Bits pro Pixel verwendet werden. Herkömmlicherweise sinddas bei Desktop Systemen 24 bzw. 32Bit. Auf mobilen Geräten sind 16Bit Farbtiefen weit verbreitet. Die Farbeselbst wird als RotGrünBlauTriple bzw. RotGrünBlauAlphaQuadruple in diesen 16, 24 oder 32Bit abgelegt.Je nach Farbtiefe kann für jede der Komponenten natürlich eine größere oder kleinere Reichweite entstehen. Wirmüssen uns aber Spaghettimonster sei dank bei OpenGL ES nicht oft und vor allem nicht so intensiv, wie zu DOSZeiten mit der Thematik auseinandersetzen. Farben werden in OpenGL ES normalerweise normiert, d.h. imBereich zwischen 0 und 1 für jede der Komponenten der Farbe (rot, grün, blau, alpha = Transparenz) angegeben.

OpenGL ES

Page 11: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 11/90

Wollen wir also die Ausgabe am Bildschirm ändern, so müssen wir den Framebuffer, genauer die Pixel imFramebuffer, manipulieren. Früher geschah das wirklich quasi noch per Hand, heutzutage wird uns diese direkteManipulation des Framebuffer von Bibliotheken, wie OpenGL abgenommen. Grob gesagt zeichnet OpenGL füruns gefärbte, texturierte Dreiecke in den Framebuffer und das ganze hardwarebeschleunigt. Wir haben Einflussdarauf, wo im Framebuffer diese Dreiecke wie gezeichnet werden, wie wir später noch sehen werden.

Pixel müssen natürlich adressiert werden können. Man verwendet dazu ein zweidimensionales KoordinatenSystem. Koordinaten in diesem System werden dabei in eine lineare Adresse im Framebuffer umgerechnet. Undzwar mit der einfachen Formel:

Adresse = x + y * Breite

Wobei mit Breite die Breite des Bildschirms in Pixel gemeint ist. Hier erklärt sich auch, wieso die yAchse in diesemKoordinatenSystem nach unten zeigt (wie jenes, das wir für TouchEvents verwenden). Die Adresse 0 im FrameBuffer entspricht dem Pixel in der oberen linken Ecke des Bildschirms und hat die Koordinaten 0,0. Bei CRTMonitoren fängt der Kathodenstrahl in dieser Ecke an, die Daten für die Intensität, die er haben soll bekommt ervereinfacht gesagt aus dem Frame Buffer, wobei natürlich am Anfang dieses Frame Buffers zu lesen begonnenwird (Position 0, Koordinate 0,0).

OpenGL arbeitet intern auch in diesem KoordinatenSystem, nach außen aber mit einem anderen. Unkonfiguriertliegt der Ursprung in der Mitte des Bildschirms, die positive xAchse geht nach rechts und die positive yAchsegeht nach oben, dabei bewegen sich die x und y Koordinaten im Bereich 1, 1, ähnlich zu den Bereichen beiFarben. Wollen wir Pixelperfekt arbeiten, müssen wir das OpenGL erst beibringen. Wir werden später nochsehen, wie wir das bewerkstelligen.

Bewegung am Bildschirm entsteht ähnlich wie bei einem Zeichentrickfilm. Es werden verschiedeneAnimationsphasen, oder Frames nacheinander in den Framebuffer geschrieben. Ist die Frequenz mit der wir dieFrames schreiben hoch genug, entsteht beim Betrachter die Illusion von Bewegung. 24 Bilder pro Sekundewerden in der Regel bei Filmen gezeigt.

Ein wenig Mathematik

Ja, ohne Mathematik kommen wir leider nicht aus. Konkret brauchen wir ein wenig lineare Algebra. Klingtgrauslich, ist es aber eigentlich gar nicht. Den Stoff, den wir uns hier zu Gemüte führen, sollten viele schon einmalin der Schule gehört haben. Wir werden uns kurz mit Vektoren in der dritten Dimension beschäftigen.

Definieren wir zuerst das KoordinatenSystem von OpenGL, in dem wir uns dann bewegen werden. Die positive xAchse zeigt nach rechts, die positive yAchse zeigt nach oben und die positive zAchse zeigt aus der Ebeneheraus. Siehe dazu die nächste Grafik:

Einen Punkt in diesem System gibt man über die Verschiebung auf den drei Achsen an, d.h. ein Punkt hat 3Koordinaten, x, y und z. Ein Vektor gibt eine Richtung im KoordinatenSystem an und ist nicht mit einem Punktgleichzusetzen. Vektoren können im System beliebig verschoben werden. Trotzdem werden wir die Termini Vektorund Punkt ein wenig durchmischen,da man im Alltag in der Regel mit Vektoren arbeitet. Wir werden im Artikel

Page 12: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 12/90

Vektoren wie folgt anschreiben:

v = [vx, vy, vz]

Vektoren werden fett gedruckt, Skalare (also einfach Zahlen) werden wir normal drucken.

Mit Vektoren kann man auch wunderbar rechnen. Als erstes wollen wir die Länge eines Vektors bestimmen:

|v| = Math.sqrt( vx * vx + vy * vy + vz * vz );

Das sollte euch bekannt vorkommen: die Länge eines Vektors leitet sich von Pythagoras' Satz ab. Die Notation aufder rechten Seite bedeutet "Länge des Vektors v".

Vektoren kann man auch addieren und subtrahieren:

a + b = [ax + bx, ay + by, az + bz]a ‐ b = [ax ‐ bx, ay ‐ by, az ‐ bz]

Bei der Multiplikation sieht das ganz ähnlich aus:

a * b = ax * bx + ay * by + az * bz

Das nennt man auch das Skalarprodukt zweier Vektoren. Mit einem kleinen Kniff kann man mit diesemSkalarprodukt den Winkel zwischen zwei Vektoren messen:

winkel = Math.acos( a * b / ( |a| * |b| ) );

Ich mische hier Java und mathematische Notation etwas,da mir die Formatierungsmöglichkeiten fehlen und es soetwas verständlicher wird. Der Winkel ist dabei immer <= 180 Grad. Math.acos() liefert diesen Winkel jedoch nichtin Grad sondern in Bogenmaß welches man recht einfach mit Math.toDegrees() in Grad umrechnen kann. Alletrigonometrischen Funktionen der Klasse Math arbeiten übrigens mit Bogenmaß, sowohl was Parameter als auchwas Rückgabewerte betrifft. Das sorgt oft für schwer zu findende Bugs, also immer daran denken.

Zum Schluss wollen wir noch auf Einheitsvektoren eingehen. Dies sind Vektoren, die die Länge eins haben. Umeinen beliebigen Vektor zu einem Einheitsvektor zu machen, müssen wir dessen Komponenten einfach durchseine Länge dividieren.

a' = [ax / |a|, ay / |a|, az / |a|]

Der Apostroph nach dem a zeigt an dass es sich um einen Einheitsvektor handelt. Wir werden Einheitsvektorenspäter für ein paar Kleinigkeiten benötigen.

Und damit sind wir mit dem MathematikKapitel auch schon fertig. Ich hoffe es war nicht gar zu schlimm. BeiUnsicherheiten empfehle ich euch im Netz ein wenig in Material zum Thema zu stöbern.

Das erste Dreieck

Wie Eingangs schon erwähnt, ist OpenGL im Grunde seines Herzens eine DreieckZeichenmaschine. In diesemAbschnitt wollen wir uns daran machen, das erste Dreieck auf den Bildschirm zu zaubern. Dazu erstellen wir eineneue Activity, die von GameActivity ableitet und die sich selbst als GameListener in der onCreate()Methoderegistriert.

Wie wir schon Eingangs erwähnt haben nennen sich die Dinge, die wir zeichnen Meshes. Ein solches Mesh wirddurch so genannte Vertices definiert. Ein Vertex entspricht dabei einem Punkt des Meshes mit verschiedenen

Page 13: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 13/90

Attributen. Anfangs wollen wir uns nur um die wichtigste Komponente kümmern, der Position. Die Position einesVertex wird, ihr habt es erraten, als dreidimensionaler Vektor angegeben. Ein Punkt alleine macht noch kein Mesh,darum brauchen wir mindestens drei. Mehrere Dreiecke sind natürlich auch kein Problem, wir wollen aber kleinanfangen.

OpenGL ES erwartet in seiner Basisversion 1.1 die Vertex Positionen in einem direct ByteBuffer . Definieren wirzur Übung einmal ein Dreieck in der xyEbene mit Hilfe so eines ByteBuffers:

ByteBuffer buffer = ByteBuffer.allocateDirect( 3 * 3 * 4 );buffer.order(ByteOrder.nativeOrder());FloatBuffer vertices = buffer.asFloatBuffer();vertices.put( ‐0.5f );vertices.put( ‐0.5f );vertices.put( 0 ); vertices.put( 0.5f );vertices.put( ‐0.5f );vertices.put( 0 ); vertices.put( 0 );vertices.put( 0.5f );vertices.put( 0 );

Ganz schön viel Code für so ein kleines Dreieck. Als erstes erstellen wir einen direct ByteBuffer der 3 * 3 * 4 Bytesgroß ist. Die Zahl ergibt sich da wir 3 Vertices haben zu je 3 Komponenten (x, y, z) die jeweils 4 Byte Speicherbrauchen (float > 32bit). Anschließend sagen wir dem ByteBuffer, dass er alles in nativer Ordnung speichern soll,d.h. in Big oder LittleEndian. Den fertig initialisierten ByteBuffer wandeln wir dann in einen FloatBuffer um den wirmit der Methode FloatBuffer.put() befüllen können. Jeweils 3 Aufrufe definieren die Koordinaten eines Vertex'unseres Dreiecks. Der erste Vertex liegt links unter dem Ursprung, der zweite Vertex rechts unter dem Ursprungund der dritte Vertex direkt über dem Ursprung. Dazu folgendes Bild:

Damit haben wir OpenGL aber noch immer nicht wirklich etwas verraten. Das machen wir doch gleich undveranlassen das Zeichnen unseres Dreiecks:

gl.glViewport(0, 0, activity.getViewportWidth(), activity.getViewportHeight());gl.glEnableClientState(GL10.GL_VERTEX_ARRAY ); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices);gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

Im ersten Methodenaufruf teilen wir OpenGL mit, welcher Bereich des Bildschirms bezeichnet werden soll. Dieersten beiden Parameter geben dabei die Startkoordinaten des zu bezeichnenden Bereichs an, die nächstenbeiden seine Größe. Beide Angaben werden in Pixeln gemacht, hier sagen wir konkret, dass der ganze Bildschirmausgenutzt werden soll.

Achtung: der Emulator benötigt unbedingt den Aufruf von glViewport. Auf Geräten ist der Viewport bereits auf den

Page 14: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 14/90

gesamten Bildschirm gesetzt, im Emulator hat der Viewport am Anfang die Größe 0,0. Nicht vergessen, da sonstnichts am Bildschirm gezeichnet wird und man stundenlang sucht (ja, ist mir auch schon passiert...)!

Im nächsten Aufruf sagen wir OpenGL, dass wir ihm jetzt gleich Vertex Positionen übergeben werden und er fortanbeim Zeichnen immer den übergebenen FloatBuffer verwenden soll. Im dritten Aufruf teilen wir OpenGL mit, wo erdie Positionsdaten findet. Der erste Parameter gibt dabei die Anzahl der Vertices an, der zweite gibt den Typen derKomponenten jeder Position an, in diesem Fall floats. Der dritte Parameter nennt sich stride und ist für uns ohneBelang, wir setzen ihn einfach auf 0. Der letzte Parameter ist der FloatBuffer, den wir zuvor mit den Positionen der3 Vertices befüllt haben. Als Letztes befehlen wir OpenGL die eben definierten Vertices zu zeichnen. Der ersteParameter gibt dabei an, was wir zeichnen wollen. In diesem Fall: Dreiecke. Der zweite Parameter gibt an, abwelcher Position im FloatBuffer OpenGL beginnen soll, die Positionsdaten zu holen. Der letzte Parameter sagtOpenGL noch, wie viele Vertices wir gezeichnet haben wollen. Zeichnen wir Dreiecke, muss dieser Parameterimmer ein Vielfaches von 3 sein. Und schon haben wir das erste Dreieck mit OpenGL gezeichnet! ZurEntspannung hier der gesamte Code dieses Beispiels:

public class TriangleSample extends GameActivity implements GameListener private FloatBuffer vertices; public void onCreate( Bundle savedInstance ) super.onCreate( savedInstance ); setGameListener( this ); @Override public void setup(GameActivity activity, GL10 gl) ByteBuffer buffer = ByteBuffer.allocateDirect( 3 * 4 * 3 ); buffer.order(ByteOrder.nativeOrder()); vertices = buffer.asFloatBuffer(); vertices.put( ‐0.5f ); vertices.put( ‐0.5f ); vertices.put( 0 ); vertices.put( 0.5f ); vertices.put( ‐0.5f ); vertices.put( 0 ); vertices.put( 0 ); vertices.put( 0.5f ); vertices.put( 0 ); vertices.rewind(); @Override public void mainLoopIteration(GameActivity activity, GL10 gl) gl.glViewport(0, 0, activity.getViewportWidth(), activity.getViewportHeight()); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY ); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices); gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

Alternativ kann man sich den Code etwas schöner formatiert unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TriangleSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TriangleSample.java ] ansehen.

Und hier ein Screenshot unseres Dreiecks

Page 15: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 15/90

Farbspiele

Ein weißes Dreieck ist natürlich etwas langweilig. Um das zu ändern, verwenden wir, bevor wir glDrawArraysaufrufen, den Befehl

glColor4f( float r, float g, float b, float a );

R, g, b stehen für die drei Farbkomponenten, a steht für die Transparenz. Alle Werte sind im Bereich 0 bis 1anzugeben. Setzen wir r und b auf 1 bekommen wir ein schönes pink:

Page 16: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 16/90

Ich habe vorher schon erwähnt, dass ein Vertex nicht nur eine Position hat. Solange wir diese nicht explizitdefinieren, hat jeder Vertex in einem Mesh die Farbe, die wir mit glColor4f angeben. Um jedem Vertex eine eigeneFarbe zu geben, verwenden wir denselben Mechanismus, wie für die Vertex Positionen. Zuerst bauen wir wiedereinen direct FloatBuffer, in den wir für jeden Vertex dir r, g, b und a Werte speichern:

buffer = ByteBuffer.allocateDirect( 3 * 4 * 4 );buffer.order(ByteOrder.nativeOrder());colors = buffer.asFloatBuffer(); colors.put( 1 );colors.put( 0 );colors.put( 0 );colors.put( 1 );colors.put( 0 );colors.put( 1 );colors.put( 0 );colors.put( 1 ); colors.put( 0 );colors.put( 0 );colors.put( 1 );colors.put( 1 ); colors.rewind();

Da wir 3 Vertices haben, brauchen wir einen ByteBuffer, der 3 Farben hält, zu je 4 Komponenten (r, g, b, a) mit je 4byte (floats). Den ByteBuffer schalten wir wieder auf native order und wandeln ihn in einen FloatBuffer um. Nunkönnen wir ihn mit den drei Farben für unsere drei Vertices befüllen, hier rot (1, 0, 0, 1), grün (0, 1, 0, 1) und blau(0, 0, 1, 1). Beim Zeichnen sagen wir OpenGL, dass es unseren FloatBuffer für die Farben der Vertices verwendensoll:

Page 17: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 17/90

gl.glEnableClientState(GL10.GL_COLOR_ARRAY );gl.glColorPointer( 4, GL10.GL_FLOAT, 0, colors );

Zuerst sagen wir OpenGL, dass wir für die Farben der einzelnen Vertices einen FloatBuffer haben(glEnableClientState). Dann geben wir, gleich, wie bei den Vertex Positionen, an, wo dieser FloatBuffer zu findenist (glColorPointer). Der erste Parameter sagt, wie viele Komponenten eine Farbe hat (4 > r, g, b, a), der zweiteParameter gibt an, welchen Typ die Komponenten haben, der dritte Parameter ist wieder der stride und der vierteist unser zuvor befüllter FloatBuffer. Und das war es auch schon wieder. Achtung: hat man den client stateGL10.GL_COLOR_ARRAY aktiviert, wird jeder Aufruf von glColor4f ignoriert. Es muss dann unbedingt einFloatBuffer mit glColorPointer angegeben werden, der zumindest so viele Farben besitzt, wie das Mesh Verticeshat, bzw. so viele, wie man Vertices bei glDrawArrays angibt!

Der gesamte Code zum Zeichnen unseres nun schön eingefärbten Dreiecks sieht so aus:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY ); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices);gl.glEnableClientState(GL10.GL_COLOR_ARRAY );gl.glColorPointer( 4, GL10.GL_FLOAT, 0, colors );gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

Hier zeichnet sich langsam ein Muster ab: für jede Komponente eines Vertex, also z.B. die Position oder die Farbe,geben wir einen Array (GL10.GL_VERTEX_ARRAY, GL10.GL_COLOR_ARRAY) mit glEnableClientState frei undgeben dann mit einer der glXXXPointer Methoden an wo, das entsprechende "Array" (in unserem Fall in Formeines FloatBuffer) zu finden ist. Diese Methode des Zeichnens nennt man in OpenGL Vertex Arrays und ist dieeinzige Methode, mit der man in OpenGL ES 1.0 überhaupt etwas zeichnen kann. Wir werden vielleicht in einemanderen Artikel die so genannten Vertex Buffer Objects näher betrachten, die ab OpenGL ES 1.1 zur Verfügungstehen. Einstweilen bleiben wir aber bei den Vertex Arrays, da sie auf allen AndroidGeräten funktionieren.

Hier noch ein Screenshot unseres farbigen Dreiecks:

Page 18: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 18/90

Den Code für dieses Beispiel könnt ihr euch unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/ColorSample2.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/ColorSample2.java ] ansehen.

Texturen

So richtig peppig wird es, wenn man seinen Dreiecken Texturen verpasst. Dabei tapeziert man auf die Dreieckeeine Bitmap, die man zuvor geladen hat. Bevor wir uns an die Texturierung selbst machen, schauen wir unsschnell an, wie man eine Bitmap überhaupt lädt. Im BeispielProjekt habe ich im assetsVerzeichnis eine PNGDatei namens "droid.png" abgelegt. Dieses laden wir in unserer setup Methode wie folgt:

try Bitmap bitmap = null; bitmap = BitmapFactory.decodeStream( getAssets().open( "droid.png" ) );catch( Exception ex ) // Oh no!

Sehr einfach: wir rufen die statische Methode decodeStream der Klasse BitmapFactory auf und übergeben ihreinen InputStream auf unser Bitmap Asset namens "droid.png". Da die Methode eine IOException wirft, machenwir noch einen trycatchBlock darum. Da wir klug genug waren, das Asset auch wirklich in das entsprechendeVerzeichnis zu packen, sollte es aber keine Exception geben. Normalerweise behandele ich Exceptions beimLaden von Ressourcen mit einem Log Output und einem System.exit(1). Wie ihr das löst, bleibt aber euchüberlassen.

Page 19: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 19/90

Das Texturieren selbst ist wieder relativ einfach. Die Bitmap, die man lädt wird in ein normiertes KoordinatenSystem gelegt:

Den Achsen geben wir zur Vermeidung von Verwechslungen mit dem VertexPositionen KoordinatenSystemneue Namen, s nach Rechts und t nach unten. Egal, welche Abmessungen das Bild hat, Pixel werden immer imBereich [0,0][1,1] angesprochen. Man kann so z.B. leicht eine hochauflösende Textur mit einer niedrigauflösenden Textur austauschen, ohne die TexturKoordinaten des Meshes zu ändern. Was die Bildabmessungenbetrifft, so gibt es eine Limitation auf Android. Es müssen ZweierPotenzen sein, also 1, 2, 4, 8, 32, 64, 128, 256usw. Maximal sollte man nicht mehr als 512x512 Pixel verwenden, die Hardware könnte das nicht mehrunterstützen. Die Bilder müssen dabei nicht quadratisch sein, sondern können z.B. auch die Abmessungen 32x64oder 128x32 haben.

Damit unser Dreieck texturiert wird, müssen wir für jeden Vertex zuerst eine TexturKoordinate angeben. DieAngabe erfolgt dabei im KoordinatenSystem der Textur, also zweidimensional und jeweils zwischen 0 und 1 (mankann auch kleinere und größere Werte angeben, das schauen wir uns aber später an):

Page 20: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 20/90

Hier haben wir unser Dreieck gemapped. Aufmerksame Leser wissen schon, was jetzt kommt: Die Koordinatenspeichern wir wieder in einen FloatBuffer:

buffer = ByteBuffer.allocateDirect( 3 * 2 * 4 );buffer.order(ByteOrder.nativeOrder());texCoords = buffer.asFloatBuffer(); texCoords.put(0);texCoords.put(1); texCoords.put(1);texCoords.put(1); texCoords.put(0.5f);texCoords.put(0);texCoords.rewind();

Die Größe ergibt sich wieder aus den drei Vertices, für die wir jeweils TexturKoordinaten mit zwei Komponentenhaben, die wiederum jeweils 4 Byte groß sind (float). Der Rest sollte selbst erklärend sein.

Bevor wir die TexturKoordinaten als VertexKomponente OpenGL mitteilen, müssen wir uns noch um eineKleinigkeit kümmern. Und zwar um das eigentliche laden der Textur. Wir haben zwar schon die Bitmap aus demAsset geladen, eine Textur haben wir aber noch nicht erstellt. Das machen wir jetzt:

int[] TextureIDs = new int[1];gl.glGenTextures(1, TextureIDs, 0); TextureID = TextureIDs[0];

Mit glGenTexturs weisen wir OpenGL an, uns eine neue Textur zu erstellen. Der erste Parameter gibt dabei an,wie viele Texturen wir erstellen wollen (eine), in den zweiten Parameter speichert OpenGL dann die ID(s) derneuen Textur(en). Der letzte Parameter ist nur ein Offset, ab dem OpenGL in dem übergebenen Array schreibensoll. Die erhaltene TexturID müssen wir uns merken, mit dieser aktivieren wir dann später die Textur.

Als nächstes müssen wir die Bitmap in die Textur laden. Hier hat uns das AndroidTeam einen großen BrockenArbeit abgenommen und stellt uns die Klasse GLUtils zur Verfügung:

Page 21: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 21/90

gl.glBindTexture( GL10.GL_TEXTURE_2D, TexturID );GLUtils.texImage2D( GL10.GL_TEXTURE_2D, 0, bitmap, 0);

Zuerst müssen wir die Textur "binden", damit sie zur aktuell aktiven Textur wird. Dazu übergeben wir als erstenParameter GL10.TEXTURE_2D (dessen Erklärung ich mir spare, das ist einfach immer so :)) und als zweitenParameter die zuvor generierte TexturID. Erst dann können wir Daten in die Textur laden, ihre Konfigurationändern oder sie als Textur für eines unserer Meshes verwenden. Als nächster Rufen wir die statische MethodetexImage2D der Klasse GLUtils auf, die unsere zuvor geladene Bitmap in die Textur lädt. Den ersten Parameterignorieren wir wieder, den zweiten auch (gibt den MipMapLevel an), als dritten Parameter übergeben wir dieBitmap und den letzten Parameter ignorieren wir auch wieder. Damit hat unsere Textur jetzt die Bilddaten, die siehaben soll.

Als letzten Schritt müssen wir die Textur jetzt noch konfigurieren:

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR );gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR );gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE );gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE );

Die ersten beiden Methoden setzen die Filter der aktuell gebundenen Textur, die zum Einsatz kommt, wenn dieTextur am Bildschirm größer als im Original ist, bzw. kleiner als im Original ist. Der letzte Parameter gibt dabei denFilter an. Hier hat man die Wahl zwischen GL10.GL_NEAREST, GL10.GL_LINEAR,GL10.GL_LINEAR_MIPMAP_NEAREST und GL10.GL_LINEAR_MIPMAP_LINEAR. GL10.GL_NEAREST ist derhässlichste, GL10.GL_LINEAR ist ein bilinearer Filter, der ganz gute Ergebnisse bringt und die beiden MipMapFilter ignorieren wir einstweilen wieder.

Die beiden anderen Methoden geben an, was geschehen soll, wenn der User TexturKoordinaten angibt, diekleiner als 0 oder größer als 1 sind. Wir wählen hier GL10.GL_CLAMP_TO_EDGE was zur Folge hat, dass solcheTexturen einfach auf den Bereich geschnitten werden ( kleiner 0 wird 0, größer 1 wird 1 ). Alternativ kann man hierGL10.GL_WRAP angeben. Dies hat zur Folge, dass die Koordinaten modulo 1 genommen werden. Eine 4.5 wirdso zur 0.5 und so weiter. Damit kann man die Textur über ein Dreieck mehrere Male wiederholen. Die Angabe desWrapModus erfolgt dabei für die s und tKomponente einzeln.

Damit haben wir die Textur fertig geladen und konfiguriert. Uns bleibt noch das überzeichnen mit der Textur. Hierder gesamte Code im Überblick:

gl.glEnable( GL10.GL_TEXTURE_2D );gl.glBindTexture( GL10.GL_TEXTURE_2D, TextureID ); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY );gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texCoords );gl.glEnableClientState(GL10.GL_VERTEX_ARRAY ); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices); gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

Zuerst müssen wir OpenGL sagen, dass es ab jetzt alle Meshes mit der aktuell gebundenen Textur texturieren soll,als nächstes binden wir unsere Textur. Dann geben wir an, dass unsere Vertices TexturKoordinaten haben undwir diese übergeben werden, was im nächsten Aufruf mit glTexCoordPointer geschieht. Hier unser texturiertesDreieck:

Page 22: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 22/90

Den Code zum Beispiel findet ihr unter [[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TextureSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TextureSample.java ]

Es sei noch angemerkt dass man die Textur nur einmal erstellt (z.B. in der setupMethode). Ich hatte da schonCode von einigen Leuten gesehen, die die Selbe Textur immer und immer wieder laden. Was für Meshes gilt:einmal machen, solang verwenden, wie nötig, dann die Ressourcen wieder freigeben. Im Fall von Vertex Arraysgibt es nichts zu tun. Im Fall von Texturen müssen wir diese löschen, was sehr einfach geht:

int[] TextureIDs = TextureID ;gl.glDeleteTextures( 1, TextureIDs, 0 );

Wir geben einfach die TexturID an und schon ist die Textur Geschichte. Man sollte eine gelöschte Textur natürlichnach dem Löschen nicht mehr binden.

Und das war's wieder. Eigentlich keine Zauberei, ein wenig Code ist es aber schon. Wir werden darum zweiKlassen bauen, die uns für Meshes und Texturen ein wenig Arbeit abnehmen und den Code schlanker machen.

Mesh & Textur Klasse

Für euer Seelenheil hab ich zwei Klassen entwickelt, die ihr sehr einfach verwenden könnt. Zum einen haben wirda die Mesh Klasse:

public final class Mesh public enum PrimitiveType Points,

Page 23: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 23/90

Lines, Triangles, LineStrip, TriangleStrip, TriangleFan public Mesh( GL10 gl, int numVertices, boolean hasColors, boolean hasTextureCoordinates, boolean hasNormals ) public void render( PrimitiveType type ) public void vertex( float x, float y, float z ) public void color( float r, float g, float b, float a ) public void normal( float x, float y, float z ) public void texCoord( float s, float t ) public void dispose( );

Ihr könnt sie über den Konstruktor einfach instanzieren. Den ersten Parameter erhaltet ihr in derGameListener.setup() bzw. GameListener.render()Methode. Der zweite Parameter gibt an, wie viele Vertices dasMesh insgesamt haben soll. Der dritte Parameter besagt, ob das Mesh auch Farben definiert, der vierte, ob TexturKoordinaten dabei sein sollen und der letzte, ob Normalen vorhanden sind. Moment, Normalen? Die erklärenwir an dieser Stelle nicht. Sie werden für die Beleuchtung von Meshes durch Lichtquellen benötigt. Damit wir inspäteren Teilen einmal darauf eingehen können, habe ich sie gleich mit in den Quellcode eingebaut.

Nachdem ihr das Mesh instanziert habt, könnt ihr es sehr einfach befüllen. Unser Color Sample von oben würdez.B. so ausschauen:

mesh = new Mesh( gl, 3, true, false, false );mesh.color( 1, 0, 0, 1 );mesh.vertex( ‐0.5f, ‐0.5f, 0 );mesh.color( 0, 1, 0, 1 );mesh.vertex( 0.5f, ‐0.5f, 0 );mesh.color( 0, 0, 1, 1 );mesh.vertex( 0, 0.5f, 0);

Als Richtlinie gilt hier: zuerst immer alle Komponenten ungleich der Position für einen Vertex angeben (Color,TexturKoordinaten, Normale) und zum fixieren des Vertex vertex mit der Position des Vertex aufrufen. Natürlichsolltet ihr nicht mehr Vertices definieren, als ihr im Konstruktor angegeben habt. Zum Rendern des Mesh reichtfolgender Aufruf

mesh.render(PrimitiveType.Triangles);

PrimitiveType ist, wie oben zu sehen, ein Enum, welches mehrere Arten von Primitiven definiert. Wir haben bisjetzt nur die Dreiecke besprochen, es sind aber auch andere Primitive möglich. Ihr könnt diese im Netznachschlagen (z.B. Triangle Strip), um einen Einblick zu erlangen.

Ein schönes Feature der Klasse ist, dass ihr das Mesh, nachdem ihr es einmal gerendert habt, wieder neudefinieren könnt. Ihr verwendet dazu einfach wieder die Methoden color, texCoord usw., wie vorher gezeigt.Wichtig dabei ist aber, dass das Mesh mindestens einmal zuvor gerendert wurde, da ansonsten ein interner Zeigernicht zurückgesetzt wird. Ihr könnt euch den Code zur MeshKlasse unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/tools/Mesh.java http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/tools/Mesh.java ] ansehen. Wirklich etwas Neuesmache ich dort nicht. Die Grundlagen dafür habt ihr bereits oben gesehen. Den Quellcode zu einem BeispielProgramm, das die Mesh Klasse verwendet, findet ihr unter [http://code.google.com/p/android

Page 24: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 24/90

gamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/MeshSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/MeshSample.java ]. Durch die Verwendungder Klasse wird der Code um einiges schlanker und verständlicher.

Als kleinen Bonus hab ich die Klasse auch noch etwas aufgedröselt und Vertex Buffer Objects implementiert.Diese werden verwendet, wenn das Gerät diese unterstützt. Sie geben ein wenig mehr Performance. Bei Gerätenmit Android Version <= 1.5 sind sie auch die einzige Lösung das permanente Garbage Collecten, welches mitVertex Arrays auftritt zu beenden. Leider ist da den Entwicklern von Android ein kleiner Bug unterlaufen, der beidirectBuffern auftritt. Das ganze erfolgt transparent, ihr müsst euch also nicht darum kümmern. Seid ihr mit derVerwendung eines Mesh fertig, müsst ihr dieses per Aufruf der Methode Mesh.dispose() freigeben.

Die Textur Klasse ist noch einfacher und im Folgenden zu sehen.

public class Texture public enum TextureFilter Nearest, Linear, MipMap public enum TextureWrap ClampToEdge, Wrap public Texture( GL10 gl, Bitmap image, TextureFilter minFilter, TextureFilter maxFilter, TextureWrap sWrap, TextureWrap tWrap ) public void bind( ) public void dispose( ) public void draw( Bitmap bmp, int x, int y ) public int getHeight() public int getWidth()

Beim Instanzieren geben wir wieder die GL10 Instanz an, ebenso wie beim Mesh. Außerdem übergeben wir dieBitmap, die gewünschten Vergrößerungs und VerkleinerungsFilter sowie die Wrap Modi für die s und tTexturKoordinaten. Diese haben wir ja oben ganz kurz angerissen. Auch MipMapping ist hier schon implementiert,einfach den minFilter auf TextureFilter.MipMap setzen. Des Weiteren gibt es eine Methode bind() die die Texturbindet, wie bei glBindTexture. Die Methode dispose löscht die Textur und gibt alle Ressourcen frei. Die Methodedraw() ist ein sehr nettes Feature. Sie erlaubt es im Nachhinein eine andere Bitmap an eine bestimmte xyPosition in der Textur zu zeichnen. Die Koordinaten werden dabei in Pixeln angegeben, der Ursprung ist dasobere linke Eck, die positive yAchse geht nach unten. Intern bindet die Methode die Textur vor dem zeichnen,man muss hier also auf den Seiteneffekt achten.

Was die Klasse nicht macht, ist das Einschalten von Texturierung über glEnable. Das also nicht vergessen. EinBeispiel für die Verwendung der TexturKlasse in Zusammenhang mit der MeshKlasse findet ihr unter[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TextureMeshSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TextureMeshSample.java ]. Das Mesh dortverwendet Farb und TexturKoordinaten, was einen hübschen Effekt hat :)

Damit haben wir jetzt zwei sehr kleine und feine Klassen, die uns viel Arbeit und Code abnehmen.

Page 25: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 25/90

Projektionen

Ich bin ein wenig fies. Nach dem Kapitel über Vektoren hab ich versprochen, dass das alles an Mathematik war,was wir hier sehen werden. Ich hab gelogen. Wir werden uns jetzt mit Projektionen beschäftigen. Dabei gibt eszwei für uns relevante Arten:

Die Parallelprojektion wird auch orthographische Projektion genannt, die Zentralprojektion kennt man auch alsperspektivische Projektion. Was genau macht eine Projektion? Sie nimmt unsere dreidimensionalen VertexPositionen und transformiert diese in 2DKoordinaten am Bildschirm (vereinfacht ausgedrückt, bis zu denBildschirmkoordinaten gibt es noch ein paar Zwischenschritte, die sparen wir uns aber). Die orthographischeProjektion verwendet man im Allgemeinen, wenn man in 2D arbeiten möchte, wie z.B. in alten NESSpielen. Dieperspektivische Projektion verwendet man für alle Spiele, die einen 3DEindruck verwenden wollen. In der Schulesollten die meisten von euch schon mal FluchtpunktZeichnungen gemacht haben, genau dasselbe Prinzipverwendet auch die perspektivische Projektion, nur mathematisch ausformuliert.

Beginnen wir mit der orthographischen Projektion. Für jede Art von Projektion brauchen wir eine Projektionsfläche,in unserem Fall ist das der Bildschirm. Die orthographische Projektion nimmt einfach jeden Vertex her undignoriert dessen zKoordinate. Die x und yKoordinaten werden mehr oder minder so übernommen, wie sie sind.Geometrisch kann man sich das so vorstellen, dass von jedem Vertex eine Linie ausgeht, die die Projektionsflächeschneidet. Diese Linien sind alle parallel zueinander und normal, d.h. in einem 90 Grad Winkel zurProjektionsfläche. Die Schnittpunkte der Linien mit der Projektionsfläche ergeben die finalen projizierten Punkte.Ein Bild sagt mehr als tausend Worte:

Es ist also vollkommen unerheblich, wie weit ein Punkt von der Projektionsfläche entfernt ist. Die orthographischeProjektion werden wir für all unsere 2DBedürfnisse verwenden. Wir konfigurieren sie so, dass wir direkt inBildschirmkoordinaten arbeiten können. Dazu verwenden wir die Klasse GLU, die eine statische Methode namensglOrtho2D besitzt. Um das gewünschte 2DKoordinatenSystem zu bekommen, rufen wir folgendes auf:

gl.glMatrixMode( GL10.GL_PROJECTION );gl.glLoadidentity();GLU.glOrtho2D( gl, 0, activity.getViewportWidth(), 0, activity.getViewportHeight() )

Zuerst sagen wir OpenGL, dass wir die ProjektionsMatrix ab jetzt bearbeiten wollen. Projektionen sindTransformationen von Vertices und werden in OpenGL über Matrizen abgebildet. Jeder Vertex, den wir anOpenGL schicken, wird mit ein paar Matrizen multipliziert, um seine finale Position am Bildschirm zu errechnen.

ParallelprojektionZentralprojektion

Page 26: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 26/90

Die Projektionsmatrix ist eine dieser Matrizen. Wir brauchen uns aber Gott sei Dank hier nicht mit Matrizen direktherumschlagen. Als nächstes laden wir eine EinheitsMatrix. Man stelle sich hier einfach vor, dass der Inhalt derProjektionsMatrix dadurch gelöscht wird und die Matrix keinen Einfluss auf unsere Vertices hat. Die Multiplikationmit dieser Matrix ergibt denselben Vektor. Abschließend verwenden wir glOrtho2D, welches eine orthographischeProjektionsMatrix lädt. Die Parameter geben dabei an, wie groß die Projektionsfläche ist (stimmt so nicht ganz).Wir geben hier den gesamten Bildschirm an, die Angaben sind in Pixel. Ab nun können wir die Vertex Positionenin Bildschirmkoordinaten angeben, wobei das KoordinatenSystem wie folgt im Portrait und LandscapeModusaussieht:

Zur Veranschaulichung hier noch das Ganze mit einem Mesh, das wir in diesem neuen KoordinatenSystem sodefinieren:

mesh = new Mesh( gl, 3, false, false, false );mesh.vertex( 0, 0, 0 );mesh.vertex( 50, 0, 0 );mesh.vertex( 25, 50, 0 );

Wir erwarten also ein Dreieck, das unten links neben dem Ursprung in Erscheinung tritt. Und das tut es auch(siehe Sample unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/OrthoSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/OrthoSample.java ]

Page 27: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 27/90

Damit könnten wir jetzt schon fast unser erstes kleines 2DSpielchen implementieren. Da wir aber moderneMenschen sind, wollen wir etwas in 3D machen. Dazu benötigen wir die perspektivische Projektion.

Die perspektivische Projektion ist um ein Stückchen schwerer zu durchschauen als die orthographischeProjektion, funktioniert aber nach einem ähnlichen Prinzip. Wieder schicken wir Linien durch alle Vertices, diesmaljedoch nicht normal zur Projektionsfläche, sondern durch einen Punkt vor der Projektionsfläche (auf der anderenSeite sind die Vertices).

Dieser Punkt ist insofern besonders, als dass er der Position des Auges eines Betrachters in unsererdreidimensionalen Welt entspricht.

Page 28: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 28/90

Die perspektivische Projektion wird durch mehrere Parameter definiert. Zum einen durch den so genannten Fieldof View. Dies ist das Sichtfeld das man in der yAchse bis zur Projektionsfläche abdeckt. Als nächstes gibt es dienear und die far Clipping Planes. Die near Clipping Plane ist unserer Projektionsebene, man gibt hier dieEntfernung zum Betrachter an. Die far Clipping Plane ist jene Ebene ab der nichts mehr dargestellt wird. JederVertex der hinter dieser Ebene liegt wird nicht gezeichnet. Als letzten Parameter für eine perspektivischeProjektion braucht man das Verhältnis zwischen Breite und Höhe der Projektionsebene, auch Aspect Ratiogenannt. Diesen errechnen wir aus der ViewportGröße, die in Pixeln angegeben ist. All diese Parametergemeinsam definieren einen Sichtkegel, der vorne durch die near Clipping Plane und hinten durch die FarClipping Plane begrenzt ist. Auch ist er oben und unten, sowie links und rechts begrenzt. Diesen Sichtkegel nenntman View Frustum. Ganz schön viel Information auf einmal, schauen wir uns an, wie einfach das in OpenGL geht:

gl.glMatrixMode( GL10.GL_PROJECTION );gl.glLoadIdentity();float aspectRatio = (float)activity.getViewportWidth() / activity.getViewportHeight();GLU.gluPerspective( gl, 67, aspectRatio, 1, 100 );

Wieder sagen wir OpenGL, dass wir die ProjektionsMatrix ändern wollen und laden dann eine EinheitsMatrix. Alsnächstes berechnen wir den aspectRatio als Viewport Breite durch Viewport Höhe. Dieser Wert ist eineDezimalzahl, daher der Cast auf (float). Zu guter Letzt verwenden wir wieder GLU und dessen MethodegluPerspective. Der erste Parameter ist die GL Instanz, der zweite Parameter das Field of View in Grad, wobei 67Grad ungefähr dem Sichtfeld nach oben und unten eines Menschen entsprechen. Der nächste Parameter gibt dieDistanz zur near Clipping Plane an, hier setzen wir ihn auf 1. Der letzte Parameter gibt die Distanz zur far ClippingPlane an, hier 100, und wir sind auch schon fertig mit der Konfiguration der perspektivischen Projektion.

Jetzt stellt sich uns die Frage, wo in unserem 3D KoordinatenSystem sich der Betrachter befindet. Wir erinnernuns, die positive xAchse zeigt nach rechts, die positive yAchse nach oben und die positive zAchse aus demBildschirm heraus. Die negative zAchse zeigt somit in den Bildschirm hinein. Der Betrachter befindet sich imUrsprung, also an den Koordinaten (0,0,0) und schaut gerade entlang der negativen zAchse. Die near ClippingPlane befindet sich damit an der zKoordinate 1, die far Clipping Plane an der zKoordinate 101. Für unser Meshbedeutet das, dass wir es auf z irgendwo zwischen 1 und 100 ansiedeln müssen. Hier ein Beispiel mit zweiDreiecken, eines auf z=2 und ein zweites auf z=5. Beide Dreiecke haben die Selbe Größe.

mesh = new Mesh( gl, 6, true, false, false ); mesh.color( 0, 1, 0, 1 );mesh.vertex( 0f, ‐0.5f, ‐5 );mesh.color( 0, 1, 0, 1 );mesh.vertex( 1f, ‐0.5f, ‐5 );mesh.color( 0, 1, 0, 1 );mesh.vertex( 0.5f, 0.5f, ‐5 );

Page 29: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 29/90

mesh.color( 1, 0, 0, 1 );mesh.vertex( ‐0.5f, ‐0.5f, ‐2 );mesh.color( 1, 0, 0, 1 );mesh.vertex( 0.5f, ‐0.5f, ‐2 );mesh.color( 1, 0, 0, 1 );mesh.vertex( 0, 0.5f, ‐2);

Das erste Dreieck ist ein wenig nach rechts verschoben und liegt hinter dem zweiten Dreieck. Die Dreieckewerden von OpenGL auch in dieser Reihenfolge gezeichnet, wodurch das hintere grüne Dreieck vom vorderenroten Dreieck überdeckt wird:

Wie zu erwarten, erscheint das grüne Dreieck kleiner als das rote, da es ja auch weiter entfernt ist. Wir habensomit den Schritt in die dritte Dimension geschafft! Den Sample Code findet ihr unter[[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/PerspectiveSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/PerspectiveSample.java ]

Kamera, ZBuffer und wie lösche ich den Schirm

Eine Kamera, die man nicht bewegen kann, ist äußerst langweilig. Zum Glück ist es mit Hilfe der Klasse GLUextrem einfach, das zu ändern. Dazu müssen wir uns aber zuerst vor Augen führen, wie eine Kamera funktioniert.Zum einen hat sie natürlich eine Position in unserer 3DWelt. Auch muss sie eine Richtung besitzen, in die sieschaut. Aus dem VektorKapitel wissen wir, wie wir das abbilden können. Ein Baustein fehlt noch: der so genannteUpVektor. Dieser definiert die yAchse der Kamera, die Richtung definiert die zAchse der Kamera und die xAchse können wir leicht über das so genannte KreuzProdukt aus Up und Richtungsvektor errechnen. Dasbrauchen wir aber alles gar nicht, da uns das die GLUKlasse abnimmt. Zum Verständnis des UpVektors: manstelle sich vor ein Pfeil ragt einem senkrecht aus dem Kopf. Das ist der UpVektor. Neigt man sein Haupt nun nachrechts oder links, ändert sich auch dieser UpVektor und mit ihm der Winkel unter dem man das Bild sieht. Aus

Page 30: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 30/90

mathematischer Sicht sei noch angemerkt, dass dieser UpVektor und der Richtungsvektor Einheitsvektoren sindund normal aufeinander stehen, also im 90GradWinkel.

Damit wir unsere Welt aus der Sicht der Kamera sehen, müssen wir wieder eine Matrix von OpenGL bemühen.Diese nennt sich die ModelViewMatrix. Der ViewTeil bezeichnet dabei den Umstand, dass man in dieser Matrixdie KameraMatrix (die sich aus den oben genannten Eigenschaften der Kamera ergibt) ablegt. Ein Vertex wirdzuerst durch die ModelViewMatrix transformiert (per Multiplikation) und dann mit der ProjektionsMatrixmultipliziert, um seine finale Position zu bestimmen. Schauen wir uns also an, wie wir diese ModelViewMatrix mitGLU so setzen können, dass wir unsere Welt aus der Sicht des Kamera sehen:

gl.glMatrixMode( GL10.GL_MODELVIEW );gl.glLoadIdentity();GLU.glLookAt( gl, positionX, positionY, positionZ, zentrumX, zentrumY, zentrumZ, upX, upY, upZ );

Die ersten beiden Zeilen kennen wir ja schon. In der ersten sagen wir jedoch, dass wir die ModelViewMatrixverändern möchten. In der dritten Zeile definieren wir unsere Kamera aus der GLU dann eine Matrix errechnet unddie ModelViewMatrix auf diese setzt. Der erste Parameter ist wieder die GLInstanz. Die nächsten drei Parametergeben die Koordinaten der Kamera an. zentrumX, zentrumY und zentrumZ geben einen Punkt in der Welt an, aufden die Kamera blicken soll. Nimmt man diesen Punkt und subtrahiert man davon die KameraPosition vektoriell,erhält man die Richtung der Kamera und damit deren zAchse. Die letzten drei Parameter geben den obenbeschriebenen UpVektor an. Dieser muss ein EinheitsVektor sein, also die Länge 1 besitzen, sonst könnenkomische Ergebnisse auftreten.

Ziehen wir unsere Szene mit dem roten und dem grünen Dreieck aus dem letzten Beispiel heran. Wir wollen dieKamera jetzt hinter das grüne Dreieck positionieren (also z<5) und sie in Richtung Ursprung sehen lassen. DieNeigung lassen wir dabei normal, d.h. der UpVektor schaut nach oben (0,1,0):

gl.glMatrixMode( GL10.GL_MODELVIEW ); gl.glLoadIdentity(); GLU.glLookAt( 0, 0, ‐7, 0, 0, 0, 0, 1, 0 );

Das Ergebnis sieht so aus (SampleCode unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/CameraSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/CameraSample.java ] )

Page 31: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 31/90

Da scheint etwas schief gegangen zu sein. Eigentlich dürfte das rote Dreieck ja das grüne nicht überdecken, tut esaber. Das Problem: Im Mesh haben wir das rote Dreieck nach dem grünen definiert und in der Reihenfolgewerden sie auch gezeichnet. Betrachten wir das ganze von vorne, stimmt alles. Von unten passt es aber nichtmehr. Was tun? In Abhängigkeit von der Blickrichtung die Ordnung der Dreiecke ändern? Was macht man dannbei sich überlappenden Dreiecken, wo die Ordnung nicht eindeutig ist?

Für all diese Probleme gibt es eine Lösung mit dem klingenden Namen [[http://de.wikipedia.org/wiki/ZBuffer ZBuffer]. Dieser ist quasi ein Zusatz zum Frame Buffer (wir erinnern uns, dort werden alle Pixel gespeichert) undbesitzt die Selbe Größe. Jedes Pixel eines gezeichneten Dreiecks besitzt neben seiner x und yKoordinate nachder Transformation mit der Projektions und ModelViewMatrix auch eine zKoordinate. In den ZBuffer schreibtOpenGL genau diese zKoordinate und macht noch etwas besonders schlaues: bevor es überhaupt ein Pixelzeichnet, prüft es, ob im ZBuffer bereits ein Pixel existiert, der näher an der Kamera liegt. Ist dies der Fall, brauchtOpenGL den aktuellen Pixel nicht schreiben, da er ja hinter dem aktuellen Pixel liegt. Alles, was wir also tunmüssen, ist diesen ZBuffer einzuschalten und das geht so:

gl.glEnable(GL10.GL_DEPTH_TEST);

Dazu müssen wir aber noch etwas tun. Und zwar den ZBuffer in jedem Frame löschen. Und wenn wir schon dabeisind, dann können wir auch gleich den Frame Buffer mitlöschen. Wie das geht, ist im Folgenden zu sehen.

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

Wer auch noch die Farbe bestimmen will, die im Frame Buffer gelöscht werden soll, kann dies folgendermaßentun:

gl.glClearColor( red, green, blue, alpha );

Page 32: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 32/90

Der Frame und der ZBuffer sollten möglichst in jedem Frame gelöscht werden. Normalerweise mache ich dasimmer ganz am Anfang des Rendering, damit ich das nicht vergesse. Schauen wir uns an in folgendem Bild an,wie unser DreieckSortierproblem jetzt aussieht.

Ausgezeichnet! Und all das mit nur zwei zusätzlichen Befehlen. Mit dem ZBuffer kommen aber auch Probleme:

Beachtet man diese beiden Probleme, steht dem vergnüglichen Gebrauch des ZBuffers nichts im Weg!

Licht und Schatten

3D alleine macht noch kein 3DGefühl. Das menschliche Auge verwendet nicht nur das stereoskopische Sehenzum Abschätzen von Tiefe, sondern auch andere Hinweise und hier vor allem Licht und Schatten.

OpenGL bietet hier einiges an Möglichkeiten, zumindest was Licht betrifft. Schattenwurf, wie wir es kennen, istnicht direkt in OpenGL inkludiert, kann aber ebenfalls simuliert werden. Mit OpenGL ES ist dies jedoch zurechenaufwändig, wir werden uns daher nur um das Licht kümmern. Schatten bekommen wir in der Sparversion:

Wenn zwei Dreiecke in derselben Ebene liegen und überlappen, kommt es zum so genannten ZFighting .Dies tritt vor allem auf, wenn man beim Rendern mit orthographischer Projektion vergisst, den ZBuffer mitglDisable(GL10.GL_DEPTH_TEST) auszuschalten. Wir müssen immer daran denken, das vor dem Zeichnenvon 2DElementen zu machen!Das Problem der Sortierung wird bei transparenten Dreiecken, also bei solchen, durch die der Hintergrundetwas zu sehen ist, nicht gelöst. Man stelle sich ein Dreieck vor, das hinter einem anderen in der Szene liegt.Das verdeckende Dreieck ist transparent und wird vor dem hinteren Dreieck gerendert. Ergebnis: das hintereDreieck kann nicht durchscheinen, da seine Pixel gar nicht erst in den Frame Buffer geschrieben werden. Im ZBuffer befinden sich ja schon die Werte für das vordere Dreieck, das näher an der Kamera ist. Dieses Problemlöst man im Allgemeinen, indem man zuerst alle nichttransparenten Objekte zeichnet, dann alle transparentenObjekte über die Distanz zur Kamera sortiert und in der sortierten Reihenfolge rendert.

Page 33: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 33/90

vom Licht abgewendete Seiten sind dunkler.

Um OpenGL dazu zu bewegen, Licht zu simulieren, müssen wir dieses einfach anknipsen:

gl.glEnable(GL10.GL_LIGHTING);

Das Ausschalten funktioniert analog:

gl.glDisable(GL10.GL_LIGHTING);

Für unsere 2DElemente werden wir kein Licht brauchen. Wir wollen aber die vollen Farben haben. Darummüssen wir vor dem Zeichnen der 2DElements das Licht auch ausschalten.

OpenGL kann verschiedene Lichtarten und Lichtquellen simulieren. Wo liegt hier der Unterschied? Als Lichtartengelten ambientes Licht, diffuses Licht, spekulares Licht und emissives Licht (streng genommen keine Lichtart).Ambientes Licht kommt aus allen Richtungen und hat keine bestimmte Quelle. Es handelt sich um jene Photonen,die schon tausende Male von einem Objekt reflektiert wurden und so für einen "Grundlichtpegel" sorgen. DiffusesLicht geht von einer bestimmten Lichtquelle aus. Es wird in alle möglichen Richtungen reflektiert. Das ist so, weildie meisten Objekte, die Licht reflektieren, feine Unebenheiten aufweisen. Spekulares Licht hingegen wird scharfreflektiert, z.B. auf einem Spiegel und bilden an einem bestimmten Punkt am Objekt ein so genanntes Highlight,einen überbeleuchteten Punkt. Emissives Licht ist Licht, das vom bestrahlten Objekt selbst ausgeht. Das folgendeBild zeigt alle vier Typen: ambient, diffuse, spekular und emissiv.

Wir werden uns nur mit ambientem und diffusem Licht in OpenGL beschäftigen. Spekulares Licht benötigt ein sehrfein aufgelöstes Mesh und damit viele Dreiecke, um richtig zur Geltung zu kommen. Emissives Licht können wirauch über die Farbe des Meshes simulieren, was in der Regel einfacher ist.

Als Lichtquellen gelten Punktlichtquellen, wie etwa eine Lampe, deren Strahlen radial ausstrahlen, direktionaleLichtquellen, wie etwa die Sonne, deren Strahlen aufgrund der Entfernung alle so gut wie parallel bei unsauftreffen und Spotlichtquellen, wie ein gerichteter Scheinwerfer, der einen Lichtkegel bildet. Punktlichtquellenund Spotlichtquellen besitzen eine Position im Raum. Eine direktionale Lichtquelle wird in OpenGL als unendlichweit entfernt angenommen und hat deswegen nur eine Richtung. Wir werden uns nur mit Punktlichtquellen unddirektionalen Lichtquellen beschäftigen. Für Spotlichtquellen gilt wieder ähnliches, wie für spekulares Licht, siebenötigen hoch aufgelöste Meshes, um zur Geltung zu kommen. Jeder Lichttyp imitiert ambientes, diffuses undspekulares Licht. Wenn wir eine Lichtquelle definieren, müssen wir für jeden Lichttypen die Farbe angeben, diedie Lichtquelle für diesen Typen imitiert. OpenGL ES kann insgesamt 8 Lichtquellen zugleich simulieren. DieseLichtquellen werden von 0 bis 7 durchnummeriert und werden mit den Konstanten GL10.GL_LIGHT0 bisGL10.GL_LIGHT7 identifiziert. Schauen wir uns zuerst an, wie man die Farben der Lichttypen eines Lichtesdefiniert:

float lightColor[] = 1, 1, 1, 1.0;float ambientLightColor[] = 0.2f, 0.2f, 0.2f, 1.0 ;gl.glLightfv(GL.GL_LIGHT0, GL10.GL_AMBIENT, lightColor, 0 );gl.glLightfv(GL.GL_LIGHT0, GL10.GL_DIFFUSE, lightColor, 0 );gl.glLightfv(GL.GL_LIGHT0, GL10.GL_SPECULAR, lightColor, 0 );

In der ersten Zeile basteln wir einen Array mit weißer Lichtfarbe. Für die ambiente Komponente definieren wir ein

Page 34: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 34/90

dunkles Grau in Zeile 2. Zeilen 3 bis 5 setzt dann die Farbe für jeden Lichttypen der Lichtquelle 0. So einfach gehtdas. Natürlich kann man für jeden Lichttyp eine verschiedene Farbe angeben, hier kann man experimentieren.

Ob die Lichtquelle ein direktionales Licht oder ein Punktlicht ist, definiert man ebenfalls über die MethodeglLightfv. Anstatt als zweiten Parameter den Lichttyp anzugeben (z.B: GL10.GL_AMBIENT) verwendet man aberdie Konstante GL10.GL_POSITION. Auch übergeben wir wieder einen floatArray mit vier Elementen. Ist das letzteElement gleich 0, bedeutet das für OpenGL, dass wir ein direktionales Licht haben wollen. Die drei erstenElemente geben dann die negative Richtung des Lichts an. Diese Richtung muss ein Einheitsvektor sein! Und eskann als die Position einer Lichtquelle gelten. Die Richtung ist dann der Vektor von der Lichtquelle zum Ursprung.Sehr verwirrend. Ist das vierte Element gleich 1, heißt das, dass wir ein Punktlicht wollen. Die ersten drei Elementeim Array entsprechen dann der Position des Lichts in der Welt.

Ein direktionales Licht von links würde dem zur Folge so definiert:

float[] direction = 1, 0, 0, 0 ;gl.glLightfv(GL.GL_LIGHT0, GL10.GL_POSITION, direction, 0 );

Wie man eine Punktlichtquelle direkt über dem Ursprung angibt, ist im nächsten CodeStück zu sehen.

float[] position = 0, 10, 0, 1 ;gl.glLightfv(GL.GL_LIGHT0, GL10.GL_POSITION, position, 0 );

Wie Licht von einem Objekt reflektiert wird, hängt nicht nur vom Lichttyp und der Lichtquelle ab, auch das Materialdes Objektes spielt dabei eine Rolle. OpenGL ES hat einen relativ guten Mechanismus, um das Material einesObjekts zu definieren. So gut dieser ist so, so langsam ist er leider auch. Wieder müssen wir für jeden Lichttyp eineFarbe definieren. Dies würden wir mit der Methode glMaterialfv machen. Diese ist aber, wie gesagt, extremlangsam. Wir nehmen hier eine Abkürzung und verwenden eine spezielle Methode von OpenGL ES.

gl.glEnable(GL_COLOR_MATERIAL);

Dies weist OpenGL ES an, dass es anstatt eines definierten Materials einfach die Farbe des Vertex hernehmensoll und dieses für die ambiente und diffuse Komponente verwenden soll. In der Regel kommt man damit, vorallem auf kleinen Bildschirmen, locker durch.

Wer sich noch an das Kapitel Mesh und TexturKlasse erinnern kann, denkt vielleicht jetzt an die Normalen, diewir pro Vertex gleich wie Farbe oder TexturKoordinaten angeben können. Diese brauchen wir auch unbedingt,wenn wir OpenGLs Beleuchtungsmodell verwenden wollen. Was ist also so eine VertexNormale? Dazu einkleines Bild.

Page 35: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 35/90

An jeder Ecke des Würfels sitzen 3 Vertices. Wenn wir darüber nachdenken, wird schnell klar warum: jede Seite,die auf diese Ecke trifft, hat ein Dreieck an dieser Stelle. Da wir es nicht besser wissen, würden wir für den Würfelinsgesamt 6 * 2 = 12 Dreiecke haben und damit 6 * 2 * 3 = 36 Vertices. Jeder Vertex hat nun eine Normale. DieseNormale ist normal zur Ebene, in der das Dreieck liegt und ragt aus der Vorderseite des Dreiecks. OpenGLverwendet diese Normale, um den Winkel des Vertices zur Lichtquelle zu berechnen. Darum müssen wir inMeshes unbedingt angeben, was wir beleuchten wollen. Im Code zur MeshKlasse könnt ihr euch anschauen, wieman die Normale eines Vertex OpenGL übergibt. Der Mechanismus ist 1:1 derselbe, wie bei Farben und TexturKoordinaten, darum gehe ich hier nicht noch mal gesondert drauf ein. Zur Übung versuchen wir einfach schnellein Mesh zu machen, das den drei in der obigen Zeichnung eingezeichneten Dreiecken entspricht. Wir werden esso definieren, dass das dunkelste Dreieck in der xyEbene liegt.

mesh = new Mesh( gl, 9, false, false, true );mesh.normal( 0, 0, 1 );mesh.vertex( 1, 0, 0 );mesh.normal( 0, 0, 1 );mesh.vertex( 1, 1, 0 );mesh.normal( 0, 0, 1 );mesh.vertex( 0, 1, 0 );mesh.normal( 1, 0, 0 );mesh.vertex( 1, 0, 0 );mesh.normal( 1, 0, 0 );mesh.vertex( 1, 0, ‐1);mesh.normal( 1, 0, 0 );mesh.vertex( 1, 1, 0 );mesh.normal( 0, 1, 0 );mesh.vertex( 1, 1, 0 );mesh.normal( 0, 1, 0 );mesh.vertex( 1, 1, ‐1 );mesh.normal( 0, 1, 0 );mesh.vertex( 0, 1, 0 );

Page 36: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 36/90

Pfuh, ganz schön viel Code für drei Dreiecke. Wir werden da später Abhilfe schaffen. Beim Rendern müssen wirjetzt ein paar Dinge erledigen. Zum einen die Beleuchtung einschalten, danach die Lichtquelle definieren. Danndie Lichtquelle einschalten und vor dem Rendern des Mesh noch ColorMaterial aktivieren. Als Lichtquellenehmen wir eine weiße Direktionale, die von Rechts oben kommt (1, 1, 0):

gl.glEnable( GL10.GL_LIGHTING );float[] lightColor = 1, 1, 1, 1 ;float[] ambientLightColor = 0.2f, 0.2f, 0.2f, 1 ;gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLightColor );gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_DIFFUSE, lightColor );gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_SPECULAR, lightColor );float[] direction = ‐1 / (float)Math.sqrt(2), ‐1 / (float)Math.sqrt(2), 0, 0 ;gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_POSITION, direction );gl.glEnable( GL10.GL_LIGHT0 );gl.glEnable( GL10.GL_COLOR_MATERIAL );mesh.render(PrimitiveType.Triangles);

Wieder ein HöllenAufwand. Wir haben auch ein paar Faux Pas geschossen. Zum einen würden wir so im MainLoop permanent zwei neue floatArrays instanzieren. Das würde irgendwann den Garbage Collector verstimmen,der sich dann ein paar hundert Millisekunden Auszeit nimmt, um aufzuräumen. Wir werden im SampleCode diebeiden Arrays zu Klassen Member unserer Sample Activity machen und somit nur einmal instanzieren. Zweitensmüssen wir eine Lichtquelle nicht immer neu definieren. Wenn sich diese über den Verlauf nicht ändert, reicht esderen LichttypFarben nur einmal anzugeben. Die Position/Richtung der Lichtquelle müssen wir aber immer nachdem Aufruf von GLU.glLookAt machen, da die Position sonst in einem anderen KoordinatenSystem definiert wird.Verwirrend. Wer sich übrigens fragt warum wir die directionElemente durch Math.sqrt(2) dividieren: Hiernormalisieren wir den Richtungsvektor! ( (1, 1, 0)' = [1 / |(1, 1, 0)|, 1 / |(1, 1, 0), 0 / |(1, 1, 0)] ).

Um die Situation im Bild oben nachzustellen werden wir auch die Kamera Position und Richtung entsprechendsetzen. Den genauen Code könnt ihr hier sehen [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/LightSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/LightSample.java ]. Das Ergebnis sieht soaus:

Page 37: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 37/90

Eine Punktlichtquelle würde komplett analog dazu definiert und eingesetzt werden, mit dem Unterschied im viertenElement im Positions Array. Und wieder ein Geheimnis von OpenGL gelüftet!

Transformationen

Jetzt wird's noch mal kurz mathematisch. Vielleicht hat sich der eine oder andere bereits gefragt, wie man denn einMesh an verschiedenen Positionen mehrere Male zeichnen kann. Schließlich sieht man das ja auch in anderenSpielen, z.B. in einem Echtzeitstrategiespiel, wo derselbe Einheitentyp mehrere Male gezeichnet wird, nur anverschiedenen Positionen und in unterschiedlicher Ausrichtung. In OpenGL verwendet man dazu wieder Matrizen,genauer, die bereits erwähnte ModelViewMatrix. Hier erklärt sich auch der erste Teil des Namens der Matrix:Model steht für die Möglichkeit ein Model (Mesh) in der Welt zu verschieben, zu rotieren und zu skalieren (größerund kleiner machen).

Unter einer Transformation versteht man die Verschiebung (Translation), Skalierung und Rotation von Vertices inder Welt. Dabei kann man mehrere solcher Transformationen über die ModelViewMatrix kombinieren, z.B. zuerstskalieren, dann rotieren und zum Schluss verschieben. Mathematisch gesehen entspricht das der Multiplikationvon Matrizen. Für jede Transformation wird eine Matrix erstellt, diese werden dann in der gewünschtenReihenfolge der Transformationen miteinander multipliziert. Wieder müssen wir uns zum Glück nicht direkt mitMatrizen herumschlagen, OpenGL bietet uns verschiedene Methoden die das erstellen und multiplizieren derMatrizen für uns erledigt.

Fangen wir mit der Translation an. Diese wird über einen TranslationsVektor angegeben der zu allen Vertices, dieman rendert hinzuaddiert wird.

Page 38: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 38/90

In OpenGL ES erreichen wir das, indem wir folgende Methode verwenden:

gl.glTranslatef( x, y, z );

Wie unschwer zu erkennen, handelt es sich bei den drei Parametern um den TranslationsVektor. Diese Methodeerstellt intern eine TranslationsMatrix und multipliziert die aktuell aktive Matrix damit, z.B. die ModelViewMatrixdie wir über glMatrixMode ausgewählt haben.

Die Skalierung multipliziert jede Komponente der Vertex Position mit einem Skalierungsfaktor.

OpenGL stellt dafür folgende Methode zur Verfügung:

gl.glScalef( scaleX, scaleY, scaleZ );

Für jede der drei Achsen gibt es einen eigenen Skalierungsfaktor. Wieder wird intern eine Matrix erstellt, mit denWerten für die Skalierung befüllt und dann mit der aktuell aktiven Matrix multipliziert.

Die Rotation ist ein wenig schwerer zu verstehen. Gedreht wird immer um den Ursprung. Gleichzeitig müssen wireine Achse angeben, (die implizit durch den Ursprung geht) um die sich die Vertices drehen sollen.

Page 39: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 39/90

Die OpenGL Methode dafür:

gl.glRotatef( angle, axisX, axisY, axisZ );

Angle gibt den Winkel in Grad an, axisX bis axisZ ist die Rotationsachse, um die gedreht werden soll. Im obigenBeispiel ist diese (0, 0, 1). Wie bei vielen anderen Dingen, muss die Rotationsachse ein Einheitsvektor sein.

Diese drei Transformationen können wir beliebig miteinander kombinieren, z.B. verschieben, rotieren, skalierenusw. Als aktive Matrix wählen wir für diese Transformationen immer die ModelViewMatrix über glMatrixMode.Schauen wir uns einmal an was die Kombination von Translation und Rotation bewirkt:

Zuerst verschieben wir das Dreieck ein wenig nach rechts, dann rotieren wir um die positive zAchse. Wenn wirdas ganze umdrehen, sieht das Ergebnis so aus:

Page 40: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 40/90

Eine komplett andere Wirkung. Wir müssen bei der Anwendung von Transformationen immer auf die Reihenfolgeschauen. Das erste Beispiel würden wir mit OpenGL so realisieren:

gl.glRotatef( 45, 0, 0, 1 );gl.glTranslatef( 2, 0, 0 );

Hm, sollte das nicht umgekehrt sein? Wir wollen ja zuerst verschieben und dann rotieren. OpenGL ist da andererAnsicht, die letzte Transformation, die wir über die Transformations Methoden angeben, ist immer die erste, dieauf die Vertices wirkt. Dies resultiert aus der Art, wie OpenGL Matrizen multipliziert und soll uns hier nicht weiterkümmern. Wir müssen uns nur den Umstand merken, dass wir Transformationen immer in der umgekehrtenReihenfolge ausführen müssen.

Was ich bis jetzt verschwiegen habe, ist der Zusammenhang zwischen Transformationen und der Kamera. Wirbefüllen in 3D ja die ModelViewMatrix per GLU.gluLookAt bereits mit der KameraMatrix. Zeichnen wir nunmehrere Objekte mit jeweils eigenen Transformationen, müssten wir die KameraMatrix vor dem Zeichnen einesObjektes jedes Mal neu setzen, da wir ja die ModelViewMatrix beim vorhergehenden Objekt überschriebenhaben. Um diese recht kostspielige Operation zu vermeiden, gibt es zwei Befehle:

gl.glPushMatrix();gl.glPopMatrix();

In Wirklichkeit gibt es für jede Matrix in OpenGL (Projektion, ModelView) nicht nur eine Matrix, sondern einenStack an Matrizen. Mit den oben vorgestellten Methoden manipulieren wir immer die Spitze des Stack. Die beidenMethoden glPushMatrix() und glPopMatrix() erlauben es uns, die über glMatrixMode aktuell selektierte Matrix aufden Stack zu legen, bzw. die zuletzt auf den Stack gelegte Matrix wieder zur aktuellen Matrix zu machen. Beimpushen der Matrix wird eine Kopie angelegt, die aktuelle Matrix bleibt dieselbe.

In Spielen geht man in der Regel so vor: jedes Objekt in der Spielwelt hat eine Orientierung (also eine Richtung, indie es schaut) und eine Position. Die Meshes für die Objekte sind immer um den Ursprung definiert. Zeichnet mannun die Objekte, lädt man zu allererst die KameraMatrix in die ModelViewMatrix. Beim Zeichnen jedes Objektespushen wir die ModelViewMatrix, damit wir eine Kopie der KameraMatrix auf dem Stack haben. Dannmultiplizieren wir die Transformationen auf die ModelViewMatrix und zeichnen das Mesh des Objekts. DasObjekt wird damit richtig transformiert. Dann popen wir die ModelViewMatrix wieder vom Stack. Auf diese Weisebeinhaltet dieser wieder nur die KameraMatrix. Diesen Prozess wiederholen wir für alle Objekte, die wir zeichnen.So werden wir das dann auch in unserem Space InvadersKlon machen. Ein Sample, das ein wenig vorgreift,(verwendet den Obj MeshLoader) findet ihr unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/MultipleObjectsSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/MultipleObjectsSample.java ]. Das Samplezeigt auch, wie man einen ApplikationFullscreen macht und die Orientierung fixiert. Das Ganze ist imuntenstehenden Bild zu sehen.

Page 41: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 41/90

Damit haben wir den letzten großen Brocken, was OpenGL betrifft, abgearbeitet. Hier gilt das Gleiche, wie für Licht:experimentieren, experimentieren, experimentieren. Um Transformationen zu verstehen, muss man sie in Aktionsehen. Als kleine Aufgabe könnt ihr ja eines der Samples hernehmen und ein um die yAchse rotierendes Dreieckproduzieren. Dazu müsst ihr nur in jedem Frame glRotatef mit dem aktuellen Winkel aufrufen. Den Winkel mussman natürlich in jedem Frame erhöhen und, wenn er größer als 360 ist, wieder auf 0 zurücksetzen.

Text zeichnen

Für die Anzeige von Scores und Ähnlichem brauchen wir eine Möglichkeit, Text zu zeichnen. Dies ist in OpenGLvon Haus aus nicht integriert, schließlich kann OpenGL ohne unser zutun ja wirklich nur Dreiecke zeichnen. Dieherkömmliche Herangehensweise für die Implementierung von Text in OpenGL funktioniert aber relativ einfach.Das Betriebssystem bietet in der Regel Möglichkeiten, um an die Bitmaps für einzelne Characters, sprich Zeichen,einer bestimmten Schriftart zu kommen. Alles, was wir machen müssen, ist diese Bitmaps in eine Textur zuzeichnen und uns zu merken, wo in der Textur wir die Bitmap für einen Character finden. Wollen wir Text zeichnen,müssen wir lediglich ein Mesh erstellen, die für jeden Character im String zwei Dreiecke besitzt, die ein Viereckbilden. Das Viereck muss genauso groß sein, wie die Bitmap für den Character. Danach mappen wir diese beidenDreiecke so mit der CharacterTextur, das diese genau den Ausschnitt der Textur verwenden, wo die Bitmap desCharacters hingezeichnet wurde. Übrigens nennt man die Bitmap für so einen Character auch Glyph. Die Texturist demzufolge ein so genannter GlyphCache. Um diese ganze mühselige Arbeit ein wenig zu vereinfachen, habeich eine Klasse Font sowie eine Klasse Text geschrieben, die uns diese ganze Arbeit abnimmt. Die Klasse Fonthat dabei nur ein paar relevante Funktionen:

public class Font public Font(GL10 gl, String fontName, int size, FontStyle style) public Font(GL10 gl, AssetManager assets, String file, int size, FontStyle style) public Text newText( GL10 gl ) public void dispose( )

Die ersten beiden Methoden sind die Konstruktoren der Klasse. Der erste Konstruktor instanziert einen Font überdie Angabe seines Namens. Damit kann man Fonts, die im System installiert sind, instanzieren. Der dritteParameter gibt die Größe des Fonts in Punkten an, der vierte den Stil des Fonts, also z.B. italic oder bold usw. Der

Page 42: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 42/90

zweite Konstruktor erlaubt das Laden eines TrueType Fonts aus einer AssetDatei. Dazu geben wir denAssetManager an, den wir von unserer Activity erhalten, sowie den Dateinamen des Font Assets. Die restlichenParameter entsprechen dem ersten Konstruktor. Die dritte Methode instanziert eine Instanz der Klasse Text. Diesespeichert die Dreiecke auf den von uns gewünschten Text. Die letzte Methode gibt den Font und seineRessourcen (Glyphcache Textur) wieder frei.

Die Klasse Text hat einige Methoden zum formatieren von Text, die wir aber für unseren Space InvadersKlonnicht brauchen. Hier die wichtigsten Methoden.

public class Text public void setText( String text ); public void render( );

Die erste Methode setzt den Text, den wir zeichnen wollen. Intern werden dabei die entsprechenden Dreieckeerstellt, die auf die Glyphcache Textur des Fonts, von dem die Instanz Text kommt, mappen. Die Methode renderzeichnet den Text dann, beginnend im Ursprung. Wir können später einfach Transformationen verwenden, um denText am Bildschirm zu verschieben. Wichtig ist auch, dass wir beim Zeichnen eine orthographische Projektionverwenden, die ein 2DPixelkoordinatensystem verwendet. Die Klasse Text kann auch mehrzeiligen Text rendern,diesen linksbündig, zentriert und rechtsbündig ausrichten oder ähnliches. Ein wenig damit herumspielen und manhat den Dreh heraus.

Damit auch nur wirklich die Pixel des Textes gerendert werden, müssen wir auch Blending einschalten. Blendingsorgt für Transparenz, jeder Pixel in einer Textur mit einem Alphawert kleiner als 1 wird durchsichtig. Dasselbe giltauch für Polygone, deren Vertices Farbwerte mit Alphawerten kleiner als 1 haben. Blending schalten wir inOpenGL so ein:

gl.glEnable( GL10.GL_BLEND );gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA );

Der erste Aufruf schaltet Blending ein, der zweite legt fest, wie geblendet wird. Blending ist ein sehr komplexerThemenkreis, deshalb werde ich Blendfunctions hier nicht besprechen. Die oben angegebene Blendfunctionreicht für 90% aller Bedürfnisse aus. Sie besagt, dass die Pixel des gerade gezeichneten Dreiecks mit den Pixelnim Framebuffer geblendet werden sollen. Diese Blendfunction brauchen wir für unseren Text, aber auch für dasAnzeigen von anderen durchscheinenden Objekten, wie z.B. Explosionen. Zum Ausschalten von Blending reichtein Aufruf.

gl.glDisable( GL10.GL_BLEND );

Ein komplettes Sample zum Zeichnen von Text unter orthographischer Projektion, findet ihr unter[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TextSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/TextSample.java ].

Page 43: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 43/90

Meshes laden

Nachdem wir den Schritt in die dritte Dimension gewagt haben, wäre es natürlich nicht schlecht, eine Möglichkeitzu besitzen, Meshes aus anderen Programmen zu laden. Eines der einfachsten Mesh Formate ist das WavefrontOBJ Format . Ich habe mir die Freiheit genommen, dafür einen einfachen Lader zu schreiben. Dieser kann mitObjDateien umgehen, die nur Dreiecke beinhalten. Baut ihr also eure eigenen Meshes in Wings3D oderBlender , könnt ihr eure Meshes von dort aus in eine ObjDatei exportieren und mit dem Lader laden. Der Laderbesitzt nur eine statische Methode:

Mesh MeshLoader.loadObj( GL10 gl, InputStream in )

Zum Laden übergeben wir also nur einen InputStream auf ein ObjAsset und erhalten eine Mesh zurück, die fixund fertige ist. Wenn das Mesh Normalen oder TexturKoordinaten hat, werden diese natürlich mitgeladen undkönnen dann mit einer Lichtquelle bzw. Textur verwendet werden. Ich habe in Wings3D ein kleines Raumschiffgebaut und mit Gimp dafür eine Textur erstellt. Die ObjDatei und die Textur verwende ich in einem weiterenSample, das ihr unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/ObjSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/ObjSample.java ] findet. Es ist im Grunde dasLight Sample mit dem Unterschied, dass ich eine Textur und das Mesh aus der ObjDatei lade. Außerdem habeich mir erlaubt, hier die Aufgabe aus dem TransformationsKapitels umzusetzen. Das Schiff dreht sich hübsch. Hiernoch ein Screenshot.

Page 44: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 44/90

Jetzt haben wir aber wirklich alles besprochen, was es zu besprechen gibt. Auf zum Sound!

Soundeffekte und Musik werden uns in diesem Kapitel beschäftigen. Für beides stellt uns Android zwei handlicheKlassen zur Verfügung: SoundPool für Soundeffekte und MediaPlayer für das abspielen von Musik. Fangen wir mitSoundPool an.

Wie wir uns erinnern sind Soundeffekte AudioDateien die wir aufgrund ihrer kleinen Größe vollständig in denSpeicher laden. Auch kann ein und derselbe Soundeffekt mehrere Male gleichzeitig abzuspielen sein. Genaudiese Aufgaben erledigt für uns der SoundPool . Schauen wir uns an wie, man ihn instanziert.

SoundPool soundPool = new SoundPool( 5, AudioManager.STREAM_MUSIC, 0);

Sehr einfach. Der erste Parameter gibt an, wie viele Soundeffekte der Soundpool maximal gleichzeitig abspielenkann. Hier geht es wirklich nur um das abspielen, laden können wir so viele, wie wir Speicher haben. Der zweiteParameter gibt an, um welchen Stream es sich handelt. Auf Android gibt es verschiedene Kanäle für Audio, z.B.den KlingeltonStream oder eben den hier gewählten MusikStream. Diesen wählen wir im Fall von Soundeffektenimmer. Der letzte Parameter hat zurzeit noch keine Funktion und soll laut Dokumentation auf 0 gesetzt werden,was wir auch tun.

Einen Soundeffekt zu laden, geht auch sehr einfach. Wir gehen davon aus, dass wir eine AudioDatei namens

SoundPool und MediaPlayer

Page 45: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 45/90

"shot.wav" in unserem Asset Verzeichnis haben. Es wird wie folgt geladen:

AssetFileDescriptor descriptor = getAssets().openFd( "shot.wav" );int soundID = soundPool.load( descriptor, 1 );

Als erstes benötigen wir einen AssetFileDescriptor, den wir uns für die AudioDatei in der ersten Zeile holen.Diesen übergeben wir dann in der zweiten Zeile an die Methode SoundPool.load die uns dann die AudioDatei inden Speicher lädt. Als Rückgabewert erhalten wir, für die gerade geladene Datei, eine ID, die wir später angebenmüssen, wenn wir diesen Soundeffekt abspielen wollen. Eine kleine Warnung: Es dauert eine Zeit, bis derSoundPool alle Soundeffekte geladen hat. Das macht er in einem separaten Thread, d.h. in unserem Spiel werdenwir davon laufzeittechnisch nichts merken. Was wir aber merken, ist, dass in den ersten paar Sekunden nach demLaden das Abspielen keinen Effekt hat. Hierfür gibt es leider noch keine Lösung, da ein Spiel aber meistens ineinem Menü beginnt und dort keine Soundeffekte verwendet werden, ist das normalerweise kein Problem.

Das Abspielen des eben geladenen SoundEffekts ist dann ebenso simpel:

int volume = (AudioManager)activity.getSystemService(Context.AUDIO_SERVICE).getStreamVolume( AudioManager.STREAM_MUSIC );soundPool.play(soundID, volume, volume, 1, 0, 1);

Als erstes müssen wir herausfinden, wie laut die Medienlautstärke aktuell ist. Diese Information bekommen wir vonder Methode getStreamVolume der Klasse AudioManager, deren Instanz wir wiederum über getSystemServiceerhalten. Als Stream geben wir wieder den Musik Stream an, da wir ja mit diesem Arbeiten. Den zurückgegebenenWert, merken wir uns und verwenden ihn in der zweiten Zeile. Hier weisen wir den SoundPool an, den zuvorgeladenen Soundeffekt abzuspielen. Dazu geben wir als ersten Parameter die ID an, die wir zuvor erhaltenhaben, dann die Lautstärke des linken und rechten Kanals. Der vierte Parameter gibt die Priorität an, mit der derSoundeffekt abgespielt werden soll. Der Wert 1 erweist uns hier gute Dienste. Der fünfte Parameter gibt an, ob derSoundeffekt geloopt, also mehrere male hintereinander abgespielt werden soll. Wie geben hier 0 an, da wir dasnicht wollen. Der letzte Parameter gibt an, mit welcher Geschwindigkeit der Soundeffekt abgespielt werden soll. 1bedeutet in normaler Geschwindigkeit, 2 würde doppelte Geschwindigkeit bedeuten und so weiter. Damit wissenwir jetzt, wie wir Soundeffekte abspielen können.

Ein Sample, welches beim berühren des Bildschirms ein Schießgeräusch macht, findet ihr unter[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/SoundSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/SoundSample.java ].

Für das Abspielen von Musik verwenden wir den MediaPlayer . Der gibt sich von der Dokumentation her rechtkompliziert, ist er aber im Grunde nicht. Schauen wir uns zuerst an, wie wir ihn Instanzieren:

MediaPlayer mediaPlayer = new MediaPlayer( );

Das war wieder einfach. Als nächstes müssen wir dem MediaPlayer sagen, was er abspielen soll. Dafür brauchenwir wieder einen AssetFileDescriptor, wie schon bei den Soundeffekten:

AssetFileDescriptor descriptor = getAssets().openFd( "music.mp3" );mediaPlayer.setDataSource( descriptor.getFileDescriptor() );mediaPlayer.prepare();mediaPlayer.start();

Page 46: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 46/90

Wir holen uns also den FileDescriptor auf unseren Musik Asset. In der nächsten Zeile setzen wir den MediaPlayerdann von dieser Datei in Kenntnis. In der dritten Zeile geben wir dem MediaPlayer Zeit, sich auf das Abspielenvorzubereiten. Ohne den Aufruf von MediaPlayer.prepare spielt der MediaPlayer nichts! Schließlich starten wir dasPlayback der Musik.

Der MediaPlayer bietet alle möglichen Methoden zum pausieren, zurückspulen und so weiter. Für unsere Zweckereicht das einmalige starten aber vollkommen aus. Was wir beim MediaPlayer noch beachten müssen, ist, dass wirihn beim Pausieren der Applikation wieder freigeben müssen. Ansonsten spielt er einfach weiter. Wirüberschreiben dazu die onPause Methode unserer GameActivity:

@Overrideprotected void onPause( ) super.onPause(); mediaPlayer.release();

Nicht auf den Aufruf von super.onPause() vergessen! Ein Sample, das eine kleine Eigenkomposition spielt, findetihr unter [http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/MusicSample.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/samples/MusicSample.java ].

Das war ein erfrischend kurzes und einfaches Kapitel. Wir haben jetzt alle nötigen Tools zusammengetragen, diewir für die Entwicklung eines kleinen Spiels brauchen. All die hier besprochenen Samples und Klassen findet ihrim Projekt. Ich empfehle euch damit herumzuspielen, da man nur so ein Gefühl für die Dinge erhält. Auch ist dasLesen der hier verlinkten Dokumentationen keine schlechte Idee. The more you know...

Auf zu Space Invaders!

Wer Space Invaders nicht kennt soll sich zuerst einmal selbst Ohrfeigen. Neben Asteroids war Space Invaders daserste Shot em' up, das kommerziell äußerst erfolgreich war. Ausgezeichnet hat es die, für damalige Verhältnisse,große Menge an Objekten, die gleichzeitig am Bildschirm dargestellt wurden. Das Spielprinzip ist dabei extremsimpel. Als Kommandeur eines kleinen Raumschiffes, gilt es, außerirdische Raumschiffe davon abzuhalten, dieErde zu überrennen. Das schafft man, indem man auf die Raumschiffe schießt, die dann über den Jordan gehen.Einen Klon des Originals kann man unter [http://www.spaceinvaders.de/ http://www.spaceinvaders.de/ ] spielen,was ich hiermit jedem empfehle. Hier noch ein kleiner Screenshot:

Space Invaders

Page 47: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 47/90

Bevor wir uns an die Umsetzung des Spiels machen, wollen wir seine einzelnen Teile analysieren, damit wir einegenaue Vorstellung davon haben, was wir überhaupt alles implementieren müssen.

Space Invaders hat ein auf den Bildschirm begrenztes Spielfeld. Es gibt vier Arten von Invaders, die drei, die obenim Screenshot sichtbar sind, sowie ein Ufo, das von Zeit zu Zeit am oberen Bildschirmrand vorbeifliegt. Die Invadersind dabei in einem Netz in gleichen Abständen angeordnet. Jede Reihe besteht aus elf Invadern, insgesamt gibtes fünf Reihen. Die Invader fahren von links nach rechts, danach eine Reihe weit nach unten, dann von rechtsnach links. Das ganze wiederholt sich, die Invader kommen dabei immer näher an das Schiff heran. Gleichzeitigschießen die Invader zufallsbasiert hin und wieder. Am unteren Bildschirmrand befinden sich Schildblöcke, dieSchüsse der Invader, sowie des Schiffes, abfangen. Die Blöcke werden durch Schüsse zerstört. Sie bilden eineBarriere zwischen dem Schiff und den Invadern, die im Spielverlauf verschwindet. Sobald ein Invader auf Höheder Blöcke ist, verschwinden die bis dahin verbleibenden Blöcke komplett. Kollidiert ein Invader mit dem Schiff,verliert dieses eines von seinen insgesamt drei Leben. Das Schiff selbst kann immer nur einen Schuss abfeuern.Der nächste Schuss kann erst abgefeuert werden, wenn der erste verschwunden ist. Dies ist der Fall, wenn einInvader getroffen wird oder der Schuss das Spielfeld verlässt. Hat der Spieler alle Invader auf dem Bildschirmvernichtet, kommt eine neue Welle an Invadern, die schneller sind als die vorhergehenden. Die Blöcke werdenrestauriert und das ganze beginnt von vorne, bis der Spieler alle Leben verloren hat. Der Abschuss eines Invadersbringt Punkte. Die Anzahl der Punkte ist dabei abhängig vom Typen des Invaders. Wird das Schiff von einemSchuss getroffen, explodiert es, ebenso wie bei einem getroffenen Invader. Es kann dann für einige Sekunde nichtkontrolliert werden. An der Position, an der das Schiff zerstört wurde, respawned es wenig später. Die Kontrolledes Schiffes erlaubt das Steuern nach links und rechts, sowie das Abfeuern eines Schusses, wenn noch keinSchuss des Schiffes im Spielfeld ist.

Aus all dem Gesagten lassen sich die Elemente für das Spiel relativ einfach ableiten:

Analyse des Originals

SchiffInvaderExplosionBlock

Page 48: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 48/90

Wir werden uns als nächstes anschauen, wie wir unseren Klon strukturieren. Die hier besprochenen Elementewerden in unseren Klon einfließen, ein paar Dinge werden wir adaptieren (müssen).

Unser Space Invader Klon soll im Grunde seines Herzens nach wie vor ein 2DSpiel bleiben. Wir werden ausdiesem Grund das Spielfeld selbst in unserem dreidimensionalen Raum in die xzEbene verlegen. Im Originalwurde das Spielfeld vom Bildschirm selbst begrenzt, da wir uns in einem dreidimensionalen Raum befinden,machen wir diese Begrenzung künstlich. Die untere Grenze des Spielfeldes stellt die XAchse dar (z=0). Auf dieserwerden wir später das Schiff bewegen. Den oberen Rand des Spielfeldes setzen wir auf den z=15. Links undrechts begrenzen wir das Spielfeld auf x=13 bzw. x=13. Unser Spielfeld ist damit (13+13)*15 Einheiten groß undliegt in der xzEbene, wobei all unsere z Werte <= 0 und >= 15 sein werden, unsere x Werte >= 13 und <= 13.

Man beachte, wie gesagt, dass das Spielfeld im negativen zBereich liegt. Ein Feld hat dabei die Abmessungen2x2. Die yAchse spielt in diesem Spiel keine Rolle, alle yWerte werden gleich 0 sein.

Neben den SpielfeldDimensionen müssen wir uns auch Gedanken über die Größen der einzelnen Spielelementemachen. Der Einfachheit halber, werden wir deren Radius auf 0.5 und ihren Durchmesser damit auf 1 festlegen.Dies entspricht weitestgehend dem Original, in dem Invaders und Schiff ungefähr gleich groß sind. Auch dieinitiale Positionierung der Invader und Blöcke müssen wir uns überlegen. Im Original stehen die Invader amoberen Ende des Spielfeldes mittig. Zwischen den Invadern ist immer ein kleiner Freiraum. Wir werden dasnachbilden. Aus Performancegründen müssen wir die Anzahl der Invader etwas herunterschrauben. Anstatt fünfReihen zu je elf Invadern, werden wir vier Reihen zu je acht Invadern haben. Die Invader haben einenDurchmesser von 1, also werden wir sie im Abstand von zwei Einheiten neben und untereinander positionieren.Die Invader der obersten Reihe haben also zWerte von 15, die der nächsten Reihe 13 usw. Der Invader ganzlinks einer Reihe sitzt auf x=7, der nächste auf 5 usw. Das Ganze sieht zu Beginn des Spiels so aus:

Schuss

Das Spielfeld

Page 49: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 49/90

Auch bei den Blöcken werden wir ein wenig vom Original abweichen. In diesem Bestand ein Block aus mehrerenSubblöcken. Diesen Umstand werden wir übernehmen. Aus Performancegründen werden wir die Subblöcke abervereinfachen. Ein Block bildet aus fünf Subblöcken eine UForm. Jeder Subblock hat dabei die Größe 1x1. Anstattvier Blöcke, wie im Original, werden wir nur drei Blöcke haben. Diese verteilen wir gleichmäßig über das Spielfeld.Das Zentrum des ersten Blocks befindet sich dabei an Position x=10,z=2.5, der nächste an x=0,z=2.5 und derdritte an x=10,z=2.5. Die jeweils fünf Subblöcke bauen wir um die Zentren. Das Ganze sieht dann wie folgt aus:

Das Schiff wird sich an der unteren gelben Linie des Spielfelds nach links und rechts bewegen können, dieInvader werden von oben nach unten fliegen und dabei immer an der linken bzw. rechten Spielfeldlinie in dieandere Richtung steuern beginnen. Damit haben wir unser Spielfeld definiert und können uns jetzt der Simulation

Page 50: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 50/90

widmen.

Kernstück jedes Spiels, ist die Simulation der Spielewelt selbst. Diese sollte so unabhängig, wie möglich von allenanderen Modulen sein, wie zum Beispiel dem Grafikmodul. Ziel ist es, die oben genannten Aspekte des Spiels zusimulieren, d.h. die Invader, das Schiff, die Blöcke, Schüsse und Explosionen. Wir werden jedes dieser Elementein einer eigenen Klasse abbilden. Als große Klammer erstellen wir auch noch eine Simulationsklasse, die all dieElemente beherbergt und für den eigentlichen Spielablauf sorgt. Das bedeutet, dass die Simulationsklasse dafürsorgt, dass sich die Invader bewegen, Schüsse ihr Ziel treffen und so weiter. Die Simulation wird dabei imKoordinatenSystem ablaufen, das im letzten Kapitel beschrieben wurde, also in der xyzEbene. Wir werden hierjede Klasse der Simulation im Detail besprechen. Die Simulation werden wir später dann in unserem Main Loopverwenden und ausführen. Hier ergibt sich ein HenneEi Problem: was beschreib ich zuerst. Meiner Meinung nachist die Struktur des Programms zu diesem Zeitpunkt unerheblich, das Verstehen der Simulation, ist erstmal derwichtigste Aspekt. Darum stürzen wir uns gleich einmal auf die Klassen der Simulation.

Randnotizen: Wir brauchen eine Klasse, die es uns erlaubt, vektoriell zu arbeiten. Zu diesem Zweck hab ich eineäußerst simple VektorKlasse implementiert. Diese besitzt Methoden, die sich mit den mathematischenAusdrücken im Mathematik Kapitel decken. Die Klasse könnt ihr euch vorab unter[http://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/spaceinvaders/simulation/Vector.javahttp://code.google.com/p/androidgamedev/source/browse/trunk/src/com/badlogic/gamedev/spaceinvaders/simulation/Vector.java ] anschauen.Am wichtigsten dabei wird für uns die Methode zum Messen der Distanz zwischen zwei Punkten (hier fälschlichauch Vektoren genannt) sein. Des Weiteren werden wir von herkömmlichen Best Practices derSoftwareentwicklung ein wenig abweichen. Anstatt Getter und Setter Methoden für jede Klasse zu erstellen,machen wir sämtliche Attribute public. Alle Methoden unserer Klassen haben einen rein funktionalen Charakterund kapseln Arbeitsgänge. Hintergrund dafür, ist der Performancezuwachs, den wir dadurch gewinnen. Die DalvikVirtual Machine ist zwar bereits relativ gut, Methodenaufrufe kosten aber doch Zeit, die wir, speziell in Spielen,nicht haben. Getter und SetterMethoden sind in diesem Rahmen meist Overkill und sollten vermieden werden. Inunserem simplen Space InvadersKlon mag dies noch keine große Auswirkung auf die Performance haben,gestaltet man aber komplexere Spiele, kann es zu Laufzeiteinbußen kommen. Wir sind uns also des schlechtenStils vollkommen bewusst, nehmen ihn aber aus Gründen der Performance in Kauf.

Fangen wir mit der Klasse für Blöcke an.

BlockKlasse

Beginnen wir gleich mit ein wenig Code. Hier die BlockKlasse:

public class Block public final static float BLOCK_RADIUS = 0.5f; public Vector position = new Vector( ); public Block(Vector position) this.position.set( position );

Eine Instanz dieser Klasse beschreibt einen der (Sub)Blöcke, wie im SpielfeldKapitel beschrieben. Dieser besitzteine Position, die wir als Attribut speichern (position). Zusätzlich haben wir eine statisches und finales Attribut, dasden Radius eines Blocks definiert, in diesem Fall 0.5f Einheiten. Diese Art von Konstanten wird uns in den

Die Simulation

Page 51: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 51/90

Simulationsklassen öfter begegnen. Sie legen die oben definierten Maße und Einheiten fest, die wir zumAusführen der Simulation benötigen. Der Konstruktor ist keine Zauberei, es wird lediglich die Position des Blocksgesetzt. Mehr macht die Klasse nicht. Die Blöcke bewegen sich nicht, daher bedarf es keines Updates derPosition. Auch Schüsse gibt der Block keine ab. Die Klasse dient lediglich zum Speichern der Position einesBlockes. Auf zur nächsten Klasse.

ExplosionKlasse

Mit der ExplosionKlasse modellieren wir Explosionen... Eine Explosion besitzt eine Position im Raum, sowie eineLebenszeit. Diese misst, wie lange die Explosion bereits gedauert hat. Nach einer bestimmten Zeitspanne soll dieExplosion schließlich wieder verschwinden. Hier begegnet uns der zweite Mechanismus, den man in der Regelneben Konstanten, wie dem Radius, in Simulationsklassen findet: die update Methode. In jeder Iteration des MainLoop stoßen wir die Simulation selbst mit dem Aufruf ihrer update Methode an. Der Methode übergibt man dieZeitspanne, die man simulieren möchte, normalerweise die Delta Time, die wir ja schon brav in der GameActivitymessen. Die Simulation wiederum ruft von jedem Element dessen update Methode auf. Die Elemente sorgendann dafür, dass sie ihren Status entsprechend der vergangenen Zeit anpassen. Das kann das zeitbasierteändern von Positionen sein, die Reaktion auf Spielereignisse, wie Schüsse, und so weiter. Im Fall unsererExplosion ändert sich nur deren Lebensdauer. Diese erhöhen wir einfach bei jedem Aufruf um die übergebeneDelta Time. Hier der Code zur Klasse:

public class Explosion public static final float EXPLOSION_LIVE_TIME = 1; public float aliveTime = 0; public final Vector position = new Vector( );

public Explosion( Vector position ) this.position.set( position ); public void update( float delta ) aliveTime += delta;

Keine Überraschungen. Die maximale Lebensdauer einer Explosion definieren wir wieder über die Konstante derKlasse und setzen diese auf 1, für eine Sekunde. Außerdem besitzt jede Instanz der Klasse einen Member zurSpeicherung ihrer bereits abgelaufenen Lebensdauer, sowie ihrer Position. Zweitere wird einmal im Konstruktorgesetzt. Zum Konstruktor gesellt sich eine weitere Methode, die bereits besprochene updateMethode. Diesebekommt die Delta Time übergeben, die sie auf das Attribut Lebensdauer aufaddiert. Dieses Attribut werden wirspäter in der Simulation dazu verwenden, zu prüfen, ob die Explosion beendet ist oder nicht. Auch diese Klasse istwieder sehr einfach, wenden wir uns also einer etwas komplizierteren Klasse zu.

ShotKlasse

Wie der Name besagt, simuliert diese Klasse einen Schuss in unserem Spiel. Ein Schuss definiert sich wiederüber eine Position im Raum. Außerdem bewegt sich ein Schuss, d.h. wir brauchen wieder eine updateMethode,die die Position des Schusses, in Abhängigkeit von der vergangenen Zeit (Delta Time), ändert. Der Schuss mussauch eine Richtung besitzen, in die er fliegt. Diese ist abhängig davon, ob er vom Schiff oder von einem Invaderstammt. Im ersten Fall bewegt sich der Schuss immer weiter in den negativen Bereich (z wird kleiner), im zweitenFall bewegt sich der Schuss in den positiven Bereich (z wird größer). Auch wollen wir, dass der Schuss weiß ob erdas Spielfeld verlassen hat. All dies implementieren wir wie folgt:

Page 52: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 52/90

public class Shot public static float SHOT_VELOCITY = 10; public final Vector position = new Vector(); public boolean isInvaderShot; public boolean hasLeftField = false;

public Shot( Vector position, boolean isInvaderShot ) this.position.set( position ); this.isInvaderShot = isInvaderShot;

public void update(float delta) if( isInvaderShot ) position.z += SHOT_VELOCITY * delta; else position.z ‐= SHOT_VELOCITY * delta; if( position.z > Simulation.PLAYFIELD_MAX_Z ) hasLeftField = true; if( position.z < Simulation.PLAYFIELD_MIN_Z ) hasLeftField = true;

Gehen wir zuerst die Attribute durch. Die Konstante SHOT_VELOCITY definiert die Geschwindigkeit. Und zwar inEinheiten pro Sekunde, mit denen ein Schuss fliegt. Übersetzt bedeutet der Wert: in einer Sekunde fliegt derSchuss zehn Einheiten weit. Außerdem besitzt die Klasse ein Attribut für die Position, eine Markierung, ob derSchuss vom Schiff stammt oder von einem Invader, sowie die Information, ob der Schuss das Spielfeld verlassenhat. Der Konstruktor bietet wieder keine Überraschungen und setzt lediglich zwei Attribute. Schauen wir uns alsodie updateMethode genauer an.

Als erstes ändern wir die Position des Schusses. Schüsse fliegen immer entlang der zAchse, daher müssen wirauch nicht diese Koordinate der Position ändern. Die Änderungen erfolgt über das subtrahieren/addieren derGeschwindigkeit multipliziert mit der vergangenen Zeit. Die Geschwindigkeit haben wir als Konstante definiert(SHOT_VELOCITY) die Zeit bekommen wir als Parameter, sie entsprich der Delta Time. Ein Schuss bewegt sichalso pro Frame um SHOT_VELOCITY * Delta Time entlang der zAchse. Die Richtung ist abhängig vom Typen desSchusses, also, ob er von einem Invader kommt oder vom Schiff.

In der updateMethode prüfen wir auch, ob der Schuss das Spielfeld verlassen hat. Nachdem er sich nur entlangder zAchse bewegt und wir davon ausgehen können, dass er von einem Schiff/Invader abgefeuert wurde undsomit gültige x/yKoordinaten hat, prüfen wir auch nur, ob der das Spielfeld in der zAchse verlassen hat. Dazubietet die Klasse Simulation zwei statische Attribute, die die maximale und minimale zKoordinate des Spielfeldesangeben. Ergibt die Prüfung, dass der Schuss nicht mehr im Spielfeld ist, setzen wir das Attribut hasLeftField auftrue. Die Simulation wird diesen Wert später dazu verwenden, um zu bewerten, ob der Schuss aus der Simulationentfernt werden kann oder nicht. Es verhält sich hier also genauso, wie bei der Lebensdauer der Explosion

Das wichtigste an dieser Klasse, ist das zeitbasierte aktualisieren der Position. Dieses Prinzip müsst ihrverinnerlichen, es wird uns noch ein paar Mal begegnen.

ShipKlasse

Und das Muster setzt sich fort. Auch unser Schiff braucht natürlich eine Position. Ebenso wie in der Klasse Blöcke,

Page 53: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 53/90

hat es auch einen Radius und eine Höchstgeschwindigkeit. Zusätzlich kann sich ein Schiff auch in Luft auflösen,sprich explodieren. Dies müssen wir irgendwie vermerken, da das Schiff in dieser Zeit nicht beschossen werdenkann. Außerdem hat ein Schiff eine Anzahl an Leben (drei als Standard). Schauen wir uns an, wie wir dasimplementieren.

public class Ship public static final float SHIP_RADIUS = 1; public static final float SHIP_VELOCITY = 20; public final Vector position = new Vector( ); public int lives = 3; public boolean isExploding = false; public float explodeTime = 0; public void update( float delta ) if( isExploding ) explodeTime += delta; if( explodeTime > Explosion.EXPLOSION_LIVE_TIME ) isExploding = false; explodeTime = 0;

Wieder begegnen uns zwei Konstanten, der Schiffradius sowie die maximale Schiffgeschwindigkeit. Die Positionmerken wir uns in Form eines Vektors. Ein weiteres Attribut hält fest, wie viele Leben das Schiff noch besitzt. DasBoolean isExploding speichert, ob das Schiff gerade explodiert, das Attribut explodeTime vermerkt, wie lange dieExplosion schon dauert, analog zur aliveTime der Explosion. Konstruktor haben wir keinen, da die Position bereitsauf (0,0,0) initialisiert wird (Konstruktor des Vektors). Schauen wir uns die updateMethode an.

Wieder bekommen wir als Parameter die Delta Time. Explodiert das Schiff, rechnen wir diese einfach auf dasAttribut explodeTime auf. Ist das Schiff lange genug explodiert, setzen wir isExploding, sowie das AttributexplodeTime wieder zurück. Das Schiff befindet sich danach wieder im normalen Zustand.

Aufmerksame Leser werden bemerken, dass das Schiff nicht bewegt wird. Das machen wir dann später in derSimulation auf Basis der Benutzereingabe. Auch das Abziehen von Leben, im Fall einer Kollision mit einemSchuss oder Invader, wird in der Simulation erledigt.

InvaderKlasse

Jetzt kommt die erste etwas komplexere Simulationsklasse. Der Invader hat wie alles andere natürlich erstmaleine Position. Auch Konstanten für Radius und Geschwindigkeit gibt es wieder. Das schwierige am Invader ist seinkomplexes Bewegungsmuster. Rekapitulieren wir diese schnell: zu Beginn bewegt sich ein Invader nach links.Nach einer bestimmten Distanz bewegt er sich um eine Einheit nach unten (positiv z), um danach nach rechtseinzuschlagen. Nach einer bestimmten Strecke nach rechts, fährt er wieder nach unten und dann nach links, dasganze wiederholt sich. Der Invader hat also drei Zustände: fahr nach links, nach unten und nach rechts. InAbhängigkeit des Zustands verändert er seine Position entlang der x bzw. zAchse. Die Distanzen, die ein Invadernach links und rechts zurücklegen muss, damit er in den nächsten Zustand wechseln kann, können wir schön ausder Beschreibung des Spielfeldes ablesen. Schauen wir uns noch mal das Bild dazu an:

Page 54: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 54/90

Anfangs legt der Invader sechs Einheiten nach links zurück. Danach eine Einheit nach unten, dann 13 Einheitennach rechts, eine Einheit nach unten, 13 Einheiten nach links ad infinitum. Dies zu implementieren scheintanfänglich etwas komplex, ist aber bei genauerer Betrachtung relativ einfach. Wir merken uns für den aktuellenStatus (links, rechts, runter), wie weit der Invader schon gewandert ist. Hat er die maximale Distanz für denZustand erreicht (13 oder 1), wechseln wir in den nächsten Zustand. Den Zustand selbst, müssen wir uns natürlichauch merken.

Die Bewegung des Invaders erfolgt natürlich wieder zeitbasiert über die Multiplikation der Geschwindigkeit mit derDelta Time. Das Ergebnis rechnen wir dann entsprechend dem Status auf die Position auf, entweder auf die xKoordinate (links, rechts) oder die zKoordinate (runter). Hier der gesamte Code:

public class Invader public static float INVADER_RADIUS = 0.75f; public static float INVADER_VELOCITY = 1; public static int INVADER_POINTS = 40; public final static int STATE_MOVE_LEFT = 0; public final static int STATE_MOVE_DOWN = 1; public final static int STATE_MOVE_RIGHT = 2; public final Vector position = new Vector(); public int state = STATE_MOVE_LEFT; public boolean wasLastStateLeft = true; public float movedDistance = Simulation.PLAYFIELD_MAX_X / 2; public Invader( Vector position ) this.position.set( position ); public void update(float delta, float speedMultiplier) movedDistance += delta * INVADER_VELOCITY * speedMultiplier;

Page 55: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 55/90

if( state == STATE_MOVE_LEFT ) position.x ‐= delta * INVADER_VELOCITY * speedMultiplier; if( movedDistance > Simulation.PLAYFIELD_MAX_X ) state = STATE_MOVE_DOWN; movedDistance = 0; wasLastStateLeft = true; if( state == STATE_MOVE_RIGHT ) position.x += delta * INVADER_VELOCITY * speedMultiplier; if( movedDistance > Simulation.PLAYFIELD_MAX_X ) state = STATE_MOVE_DOWN; movedDistance = 0; wasLastStateLeft = false; if( state == STATE_MOVE_DOWN ) position.z += delta * INVADER_VELOCITY * speedMultiplier; if( movedDistance > 1 ) if( wasLastStateLeft ) state = STATE_MOVE_RIGHT; else state = STATE_MOVE_LEFT; movedDistance = 0;

Die Definition der Konstanten für den Radius und die Geschwindigkeit sollten keine große Überraschung sein. Dienächsten drei Konstanten stehen für die drei Status, in der sich ein Invader befinden kann. Natürlich haben wirauch wieder ein Attribut für die Position. Ein weiteres Attribut hält den aktuellen Status des Invaders, den setzenwir zu Beginn auf STATE_MOVE_LEFT. Aus organisatorischen Gründen merken wir uns auch, ob der letzteZustand nach links oder nach rechts geführt hat. Das letzte Attribut speichert die gefahrene Distanz für denaktuellen Zustand, den initialisieren wir auf die Hälfte des maximalen xWertes der Simulation (siehe Grafik). DenKonstruktor kennen wir in der Form auch schon. Schauen wir uns also die updateMethode an.

Was als erstes auffällt, ist ein zweiter Parameter namens speedMultiplier. Unser Klon soll ja auf Dauer etwasfordernder werden. Dazu lassen wir einfach die Invader schneller werden. Nach jeder Welle erhöhen wir diesenSpeedmultiplier etwas, was wiederum Auswirkungen auf die Geschwindigkeit der Invader hat. Dazu mehr in derBeschreibung der SimulationKlasse. Wir merken uns nur, dass wir diesen Wert bei der Zeitbasierten Bewegungaufmultiplizieren müssen.

Als erstes addieren wir in dieser Methode die im letzten Frame zurückgelegt Strecke auf movedDistance. Keinegroße Sache, wir berechnen einfach den zeitbasierten Weg (mal Speedmultiplier). Als nächstes prüfen wir, inwelchem Zustand wir uns befinden.

Sind wir im Zustand STATE_MOVE_LEFT, bewegen wir unseren Invader zeitbasiert ein Stück nach links(Geschwindigkeit * Delta Time * Speedmultiplier = Im Frame zurückgelegte Strecke). Danach wird kontrolliert, obwir die maximale Distanz für diesen Zustand zurückgelegt haben. Ist dies der Fall, wechseln wir den Status in

Page 56: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 56/90

STATE_MOVE_DOWN und setzen movedDistance auf 0. Auch merken wir uns, dass der letzte horizontale Statusnach links ging.

Dasselbe Spiel spielen wir, wenn wir uns im Zustand STATE_MOVE_RIGHT befinden. Anstatt nach links,bewegen wir uns nach rechts. Sollte die maximale Distanz für den Zustand überschritten sein, setzen wir denStatus wieder auf STATE_MOVE_DOWN, movedDistance auf 0 und merken uns, dass wir nach rechts gefahrensind.

Die Handhabung des Zustands STATE_MOVE_DOWN läuft ein wenig anders ab. Zuerst bewegen wir uns einmalzeitbasiert nach unten. Haben wir die maximale Distanz für den Status überschritten (>1), wechseln wir in einender horizontalen Zustände. Fuhren wir zuvor nach links, müssen wir jetzt nach rechts fahren und umgedreht.Natürlich setzen wir auch movedDistance wieder zurück.

Pfuh, ganz schönes Stück Arbeit, so ein Invader. Damit haben wir aber die letzte Klasse fertig besprochen undkönnen uns der eigentlichen Simulationsklasse widmen.

SimulationKlasse

Die Aufgabe der Klasse, ist das Zusammenspiel der verschiedenen Spielelemente zu regeln. Zum einen sorgt dieKlasse dafür, dass alle Elemente, die eine updateMethode besitzen, auch aktualisiert werden. Zum anderen prüftsie verschiedene Ereignisse, wie die Kollision von Schüssen und führt entsprechende Reaktionen aus.Beispielsweise das verschwinden lassen von Invadern, das Erzeugen von Explosionen und so weiter. In unseremFall bietet die Klasse auch drei Methoden, die von außen angesteuert werden. Diese sind für die Bewegung desSchiffes sowie das Feuern eines Schusses verantwortlich. Wir werden die Klasse in kleinen Stückchen sezieren,um ihre Funktionsweise zu verstehen. Beginnen wir mit den Attributen:

public class Simulation public final static float PLAYFIELD_MIN_X = ‐14; public final static float PLAYFIELD_MAX_X = 14; public final static float PLAYFIELD_MIN_Z = ‐15; public final static float PLAYFIELD_MAX_Z = 2; public ArrayList<Invader> invaders = new ArrayList<Invader>(); public ArrayList<Block> blocks = new ArrayList<Block>( ); public ArrayList<Shot> shots = new ArrayList<Shot>( ); public ArrayList<Explosion> explosions = new ArrayList<Explosion>( );

public Ship ship; public Shot shipShot = null;

public SimulationListener listener; public float multiplier = 1; public int score; public int wave = 1; private ArrayList<Shot> removedShots = new ArrayList<Shot>(); private ArrayList<Explosion> removedExplosions = new ArrayList<Explosion>( );

... to be continued ...

Die ersten vier Attribute sind wieder Konstanten für unser Spielfeld. Die ersten beiden geben die Begrenzungenauf der xAchse an, die anderen beiden die Begrenzungen auf der zAchse.

Es folgen die Listen für die verschiedenen Spielelemente. Wieder keine große Überraschung, wir verwenden

Page 57: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 57/90

einfach eine ArrayLists für Invader, Blöcke, Schüsse und Explosionen.

Auch das Schiff müssen wir speichern, genauso wie den aktuellen Schuss des Schiffs. Dieser kommt auch in dieshotsListe, wird aber separat noch mal gespeichert, damit wir wissen, ob es ein Schiffschuss gibt oder nicht.

Die Simulation erlaubt auch das Einhängen eines Listeners. Der Sinn dahinter: Außenstehende Klassenbekommen so essentielle Ereignisse in der Simulation mit, wie Explosionen oder das Abfeuern von Schüssen. Wirwerden später einen solchen Listener einhängen, der die entsprechenden Soundeffekte, für bestimmteEreignisse, abspielt.

Der Multiplikator ist uns im Invader schon begegnet, als Parameter für die updateMethode. Zu Beginn derSimulation setzen wir diesen auf 1. Dies bedeutet, dass Invader mit der GeschwindigkeitInvader.INVADER_VELOCITY fliegen werden. Später werden wir diesen Multiplier erhöhen, damit die Invaderschneller werden.

Natürlich speichern wir auch den aktuellen Punktestand, ohne diesen wäre das Spiel nur halb so lustig. DesWeiteren speichern wir noch, wie viele Wellen an Invadern bereits aufgetreten sind. Reine Statistik, ohne großeFunktion.

Die letzten beiden ArrayLists sind UtilityAttribute, die wir zum Löschen von Schüssen und Explosionen benötigen.Wir wollen diese nicht immer neu instanzieren, da sonst der Garbage Collector permanent anspringt.

Instanzieren wir eine Simulation, so wollen wir ein fix und fertiges Spielfeld darin vorfinden. Dort sollen alleElemente so positioniert sein, wie im Abschnitt Spielfeld dargelegt. Das Befüllen der Simulation werden wir in eineeigene Methode namens populate packen. Schauen wir uns Konstruktor und populate an:

... continued ...public Simulation( ) populate( ); private void populate( ) ship = new Ship();

for( int row = 0; row < 4; row++ ) for( int column = 0; column < 8; column++ ) Invader invader = new Invader( new Vector( ‐PLAYFIELD_MAX_X / 2 + column * 2f, 0, PLAYFIELD_MIN_Z + row * 2f )); invaders.add( invader ); for( int shield = 0; shield < 3; shield++ ) blocks.add( new Block( new Vector( ‐10 + shield * 10 ‐1, 0, ‐2) ) ); blocks.add( new Block( new Vector( ‐10 + shield * 10 ‐1, 0, ‐3) ) ); blocks.add( new Block( new Vector( ‐10 + shield * 10 + 0, 0, ‐3) ) ); blocks.add( new Block( new Vector( ‐10 + shield * 10 + 1, 0, ‐3) ) ); blocks.add( new Block( new Vector( ‐10 + shield * 10 + 1, 0, ‐2 ) ) ); ... to be continued ...

Im Konstruktor rufen wir lediglich populate auf, das übernimmt das Befüllen der Simulation. In der Methode

Page 58: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 58/90

populate beginnen wir damit, das Schiff zu instanzieren. Als nächstes platzieren wir die Invader auf dem Spielfeld,wie vorher schon beschrieben. Die erste Schleife geht dabei über die vier Reihen, die nächste über die jeweilsacht Invader pro Reihe. Die neu erstellten Invader geben wir in unser Attribut invaders, damit wir sie auch späternoch verfügbar haben. Danach erstellen wir die 3 * 5 Blöcke, ebenfalls so positioniert, wie im Abschnitt Spielfeldbeschrieben. Als Übung könnt ihr euch ja die Positionen, so wie sie sich aus dem Code ergeben, errechnen undmit der Beschreibung im Spielfeldabschnitt vergleichen.

Die Hauptarbeit der Simulation, ist das zeitbasierte aktualisieren aller Spielelemente. Dazu haben wir, wie gehabt,eine updateMethode. Von außen wird dieser die Delta Time mitgeteilt. Sehen wir uns die Methode an:

... continued ...public void update( float delta ) ship.update( delta ); updateInvaders( delta ); updateShots( delta ); updateExplosions(delta); checkShipCollision( ); checkInvaderCollision( ); checkBlockCollision( ); checkNextLevel( ); ... to be continued ...

Schön aufgeräumt, mit Methodenaufrufen, gehen wir die einzelnen Elemente durch. Als erstes aktualisieren wirdas Schiff. Wir erinnern uns daran, dass beim Update, im Fall einer Explosion, deren Zeit gemessen wird und einentsprechender Vermerk gesetzt wird. Anschließend bringen wir die Invader, die Schüsse und die Explosionen aufden augenblicklichen Stand. Wir sehen uns die drei Methoden gleich im Detail an. Als nächstes schauen wir, obes Kollisionen zwischen dem Schiff und Schüssen bzw. Invadern gab (checkShipCollision). Dasselbe tun wir dannauch für die Invader (checkInvaderCollision) und für die Blöcke (checkBlockCollision). Zum Schluss prüfen wir, oballe Invader der aktuellen Welle zerstört wurden. Ist dies der Fall, befüllen wir das Spielfeld mit neuen Invadern(checkNextLevel) und damit mit der nächsten Welle.

Das ist die große Klammer, die in jeder Iteration einen Schritt in der Simulation ausführt. Wie wollen uns jetzt mitden in update aufgerufenen Methoden auseinandersetzen. Gehen wir sie der Reihe nach durch:

... continued ...private void updateInvaders( float delta ) for( int i = 0; i < invaders.size(); i++ ) Invader invader = invaders.get(i); invader.update( delta, multiplier ); ... to be continued ...

Wie gehen einfach durch alle Invader durch und rufen deren updateMethode auf, mit der aktuellen Delta Time,sowie dem Wert des Multiplikators (der die Geschwindigkeit je nach Nummer der aktuellen Welle etwas erhöht).Wir verwenden keine Iteratoren in der Schleife, da diese unter Android bzw. Dalvik Objekte instanzieren, diewiederum den Garbage Collector anwerfen würden. Wie bereits erwähnt, sollten wir das vermeiden.

Als nächstes schauen wir uns updateShots an:

Page 59: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 59/90

... continued ...private void updateShots( float delta ) removedShots.clear(); for( int i = 0; i < shots.size(); i++ ) Shot shot = shots.get(i); shot.update(delta); if( shot.hasLeftField ) removedShots.add(shot); for( int i = 0; i < removedShots.size(); i++ ) shots.remove( removedShots.get(i) ); if( shipShot != null && shipShot.hasLeftField ) shipShot = null;

if( Math.random() < 0.01 * multiplier && invaders.size() > 0 ) int index = (int)(Math.random() * (invaders.size() ‐ 1)); Shot shot = new Shot( invaders.get(index).position, true ); shots.add( shot ); if( listener != null ) listener.shot(); ... to be continued ...

Hier passiert schon ein wenig mehr. Zuerst rufen wir die updateMethode von jedem Schuss auf. Wir erinnern uns,diese prüft, ob der Schuss außerhalb des Spielfelds ist und bewegt den Schuss. In der Schleife schauen wir nachdem Update, ob der Schuss das Spielfeld verlassen hat, indem wir das entsprechende Attribut prüfen, und gebenihn in diesem Fall in die Liste der zu löschenden Schüsse.

Die nächste Schleife löscht alle Schüsse, die wir gerade in die Liste removedShots gegeben haben aus der Listeshots. Damit verschwinden sie komplett aus dem Spiel. Da wir nicht über Iteratoren arbeiten, müssen wir dies,etwas umständlich, mit der removedShots Liste machen.

Als nächstes schauen wir, ob auch ein Schuss vom Schiff auf dem Spielfeld ist und ob dieser das Spielfeldverlassen hat. Ist dies der Fall, setzen wir shipShot auf null, womit wir später wissen, dass kein Schuss desSchiffes mehr am Spielfeld ist. Das entfernen des Schusses passierte schon zuvor in der Schleife, da derSchiffschuss ja auch in der shotsListe ist.

Der nächste Teil ist ein wenig schwerer zu verstehen. Die Invader sollen ja ebenfalls hin und wieder schießen.Dieses „hin und wieder“ ist zufallsbasiert. Dazu holen wir uns über Math.random() eine Zahl zwischen 0 und 1. Istdie Zahl kleiner als 0.01 * Faktor und gibt es mehr als einen Invader, erzeugen wir einen neuen Schuss. D.h. dieWahrscheinlichkeit, dass in einem Simulationsupdate ein Schuss von einem Invader abgegeben wird, beträgt einProzent.

Im Körper der ifAbfrage, suchen wir uns dann ebenfalls zufallsbasiert einen der Invader aus und erzeugen andessen Position einen neuen Schuss, den wir in die shotsListe einfügen. Außerdem rufen wir hier zum ersten Maleinen eventuellen Listener auf, der so weiß, dass ein Schuss abgegeben wurde. Damit hätten wir schon mal einenessentiellen Mechanismus, nämlich das Schießen der Invader abgehakt. Die Schüsse, die wir hier neuhinzufügen, werden im nächsten Update über die vorhergehende Schleife wieder verarbeitet.

Als nächstes schauen wir uns an, was wir mit den Explosionen machen:

Page 60: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 60/90

... continued ...public void updateExplosions( float delta ) removedExplosions.clear(); for( int i = 0; i < explosions.size(); i++ ) Explosion explosion = explosions.get(i); explosion.update( delta ); if( explosion.aliveTime > Explosion.EXPLOSION_LIVE_TIME ) removedExplosions.add( explosion );

for( int i = 0; i < removedExplosions.size(); i++ ) explosions.remove( explosions.get(i) ); ... to be continued ...

Wie schon bei den Invadern, gehen wir über alle Explosionen am Spielfeld und aktualisieren sie. Waren sie langegenug am Leben, geben wir sie in die removedExplosionListe, über die wir sie in der nächsten Schleife dannendgültig vom Spielfeld entfernen.

Damit haben wir alle UpdateMethoden abgehandelt. Es verbleiben noch die KollisionsRoutinen. Starten wir mitcheckInvaderCollision:

... continued ...private void checkInvaderCollision() if( shipShot == null ) return; for( int j = 0; j < invaders.size(); j++ ) Invader invader = invaders.get(j); if( invader.position.distance(shipShot.position) < Invader.INVADER_RADIUS ) shots.remove( shipShot ); shipShot = null; invaders.remove(invader); explosions.add( new Explosion( invader.position ) ); if( listener != null ) listener.explosion(); score += Invader.INVADER_POINTS; break; ... to be continued ...

Unsere Aufgabe hier, ist es, einen eventuell vorhandenen Schuss des Schiffes mit allen Invadern zu prüfen.Befindet sich der Schuss innerhalb des Radius eines Invaders, so geht dieser in einer Explosion hoch.

Als erstes schauen wir, ob es überhaupt einen Schiffschuss gibt. Ist dies nicht der Fall, verabschieden wir unsgleich wieder. Danach gehen wir jeden Invader durch. Ist die Distanz zwischen Invader und Schiffschuss kleinerals der Radius eines Invader, löschen wir den Schiffschuss aus shots und setzen shipShot auf null. Danach

Page 61: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 61/90

löschen wir auch den getroffenen Invader und erzeugen eine neue Explosion an der Position des toten Invaders.Ist ein Listener gesetzt, rufen wir diesen auf und teilen ihm mit, dass eine Explosion stattgefunden hat. Zu guterLetzt erhöhen wir den Punktestand und brechen die Schleife ab.

Es sei hier angemerkt, dass wir einen kleinen Shortcut genommen haben. Nachdem wir die Invader von hintennach vorne in die invadersListe eingefügt haben, kommen wir mit dieser Methode aus. Wären die Invader nichtnach Tiefe sortiert, müssten wir die Distanz zu jedem Invader prüfen und uns den merken, der am nächstengetroffenen ist. Das bleibt uns aber erspart, da wir ja schlau sind.

Das Schiff müssen wir sowohl auf Kollisionen mit Schüssen, wie auch mit Invadern selbst prüfen:

... continued ...private void checkShipCollision() removedShots.clear(); if( !ship.isExploding ) for( int i = 0; i < shots.size(); i++ ) Shot shot = shots.get(i); if( !shot.isInvaderShot ) continue; if( ship.position.distance(shot.position) < Ship.SHIP_RADIUS ) removedShots.add( shot ); shot.hasLeftField = true; ship.lives‐‐; ship.isExploding = true; explosions.add( new Explosion( ship.position ) ); if( listener != null ) listener.explosion(); break; for( int i = 0; i < removedShots.size(); i++ ) shots.remove( removedShots.get(i) ); for( int i = 0; i < invaders.size(); i++ ) Invader invader = invaders.get(i); if( invader.position.distance(ship.position) < Ship.SHIP_RADIUS ) ship.lives‐‐; invaders.remove(invader); ship.isExploding = true; explosions.add( new Explosion( invader.position ) ); explosions.add( new Explosion( ship.position ) ); if( listener != null ) listener.explosion(); break;

Page 62: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 62/90

... to be continued ...

Zuerst prüfen wir, ob ein Invaderschuss das Schiff getroffen hat. Das Ganze tun wir aber nur, wenn das Schiff nichtexplodiert. Wurde das Schiff getroffen, löschen wir den Schuss, vermindern die Leben des Schiffes um eins underzeugen eine Explosion. Außerdem sagen wir dem Schiff, dass es explodieren soll. Dies machen wir, indem wirdas entsprechende Attribut setzen. Der Listener wird aufgerufen und die Schleife abgebrochen. Mehr als ein Malkann das Schiff nicht getroffen werden.

Als nächstes prüfen wir, ob ein Invader mit dem Schiff kollidiert ist. Hier machen wir das Gleiche, wie bei denShots, Distanz kontrollieren, Leben abziehen, Explosionen erzeugen (für Invader und Schiff) und Invader löschen.Zum Schluss rufen wir wieder den Listener auf und brechen die Schleife ab. Eigentlich alles nicht so schlimm.

Jetzt müssen wir noch die Blöcke auf Kollisionen mit Schüssen überprüfen:

... continued ...private void checkBlockCollision( ) removedShots.clear(); for( int i = 0; i < shots.size(); i++ ) Shot shot = shots.get(i); for( int j = 0; j < blocks.size(); j++ ) Block block = blocks.get(j); if( block.position.distance(shot.position) < Block.BLOCK_RADIUS ) removedShots.add( shot ); shot.hasLeftField = true; blocks.remove(block); break; for( int i = 0; i < removedShots.size(); i++ ) shots.remove( removedShots.get(i) );... to be continued ...

Selbes Spiel wie immer. Wir prüfen jeden Schuss mit jedem Block. Wurde ein Block getroffen, löschen wir sowohlBlock, als auch Schuss. Explosionen gibt es in diesem Fall keine.

Schauen wir uns an, wie wir die nächste Welle lostreten:

... continued ...private void checkNextLevel( ) if( invaders.size() == 0 && ship.lives > 0 ) blocks.clear(); shots.clear();

Page 63: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 63/90

shipShot = null; Vector shipPosition = ship.position; int lives = ship.lives; populate(); ship.lives = lives; ship.position.set(shipPosition); multiplier += 0.1f; wave++; ... to be continued ...

Eine erfrischend kurze Methode. Sind alle Invader explodiert und hat das Schiff noch mindestens ein Leben,befüllen wir die Simulation neu. Dazu löschen wir alle vorhandenen Blöcke und Schüsse und setzen shipShot aufnull. Dann merken wir uns die aktuelle Schiffsposition. Diese wird ja im folgenden Aufruf der Methode populate auf(0,0,0) gesetzt, selbiges machen wir für die Anzahl der Leben. Nach Aufruf der populateMethode ist dieSimulation mit neuen Invadern und Blöcken befüllt und besitzt eine neue Instanz der Klasse Ship. Dieser setzenwir die letzte Position und speichern die Anzahl der Leben. Danach gehen wir den Multiplier an und erhöhendiesen um 0.1. Dies entspricht einer Erhöhung der Invader Geschwindigkeit um zehn Prozent. Abschließenderhöhen wir noch den Wave Counter, womit die Simulation für eine neue Runde bereit ist.

Als letztes Puzzlestück müssen wir uns noch anschauen, wie das Schiff bewegt wird und wir einen Schussabfeuern können. Beginnen wir mit der Bewegung nach links:

... continued ...public void moveShipLeft(float delta, float scale) if( ship.isExploding ) return; ship.position.x ‐= delta * Ship.SHIP_VELOCITY * scale; if( ship.position.x < PLAYFIELD_MIN_X ) ship.position.x = PLAYFIELD_MIN_X; ... to be continued ...

Als Parameter erhalten wir die Delta Time, sowie einen Faktor scale. Was es mit dem zweiten auf sich hat,erfahren wir gleich. Zuerst prüfen wir aber, ob das Schiff explodiert. Ist das der Fall, brauchen wir gar nichtsmachen, da sich ein explodiertes Schiffe nicht mehr bewegt.

Ist das Schiff nicht zerstört worden, wird es nach links verschoben. Dazu multiplizieren wir wie gewohnt die DeltaTime mit der Schiffsgeschwindigkeit. Zusätzlich multiplizieren wir auch noch den Faktor scale dazu. Dieser kommtvon außen und bewegt sich im Bereich (0,1). Aufmerksame Leser werden erahnen, woher der Wert kommt: derAccelerometer gibt uns diesen. Mehr dazu später im Kapitel GameLoop, wo wir die Verarbeitung derAccelerometerdaten zur Steuerung des Schiffes besprechen.

Als letztes wird kontrolliert, ob das Schiff das Spielfeld verlassen hat. Ist dies der Fall, setzen wir es auf den letztenerlaubten Punkt auf der xAchse zurück.

Für die Bewegung nach rechts gibt es eine analoge Methode, die ich hier nicht anführe, da sie das Gleiche, nur indie andere Richtung macht.

Zu guter Letzt die Methode zum Abfeuern eines Schusses:

Page 64: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 64/90

... continued ...public void shot() if( shipShot == null && !ship.isExploding ) shipShot = new Shot( ship.position, false ); shots.add( shipShot ); if( listener != null ) listener.shot();

Zuerst wird geprüft, ob es bereits einen Schuss auf das Schiff gibt, bzw. ob das Schiff explodiert ist. Ist dies nichtder Fall, erstellen wir einen neuen Schuss an der aktuellen Schiffsposition, geben ihn in die Liste shots undsignalisieren dem Listener, dass ein Schuss abgefeuert wurde. Das war es auch schon!

Damit haben wir alle Klassen der Simulation besprochen. Abgesehen von der Eingabe der Accelerometerdatenund TouchEvents, haben wir hiermit eine voll funktionstüchtige Spielewelt, die unabhängig von jeglicher anderenKomponente funktioniert. Unser Ziel ist also erreicht. Was noch bleibt, ist das Schreiben eines Renderers, derunsere Simulation zeichnet, sowie des GameLoops und des Start und Game OverScreens. Widmen wir unszuerst dem Renderer, die größte Klasse nach der Simulation.

Nachdem wir die Simulation so hübsch gekapselt haben, wollen wir das Ganze jetzt auch auf den Bildschirmzaubern. Dazu brauchen wir für jedes Element im Spiel verschiedene Ressourcen, d.h. Meshes und Texturen.Bevor wir uns Code anschauen, widmen wir uns kurz der Erstellung der Ressourcen.

Am Spielfeld gibt es vier verschiedene, sichtbare Elemente: das Schiff, die Invader, Schüsse und Blöcke. Für allevier brauchen wir ein Mesh und eventuell auch Texturen. Ich habe für den Space InvadersKlon Wings3Dverwendet, ein Polygonmodeller zum Nulltarif. Für die Erstellung der Texturen habe ich Gimp verwendet, eineOpenSource Grafikapplikation. Der Einfachheit halber gibt es nur ein einziges Modell für Invader, eine fliegendeUntertasse. Für das Schiff gibt es auch ein kleines Mesh. Die Blöcke habe ich als abgeflachte Würfel modelliertund die Schüsse sind ebenfalls kleine Würfel. Hier ein paar Screenshots der Modelle in Wings3D, sowie dieverwendeten Texturen:

Das Rendering

Page 65: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 65/90

Page 66: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 66/90

Datei:shipTextur.pngDatei:invaderTextur.png

Wie zu erkennen ist, sind die einzelnen Meshes schon richtig skaliert. Ein Feld des Gitters hat die Abmessung 1x1.Texturen gibt es nur für Invader und das Schiff, Blöcke und Schüsse färben wir später über glColor4f ein. AlleMeshes besitzen Normalen zur Lichtberechnung, die wir natürlich auch verwenden wollen.

Zusätzlich zu den Spielobjekten brauchen wir auch noch einen hübschen Hintergrund als Grundlage derDarstellung. Dazu hat mein schwedischer Freund Zire (auch bekannt als Killergoat from hell) ein hübsches Bildseines Spieles Gods and Idols beigesteuert, ein Dankeschön an dieser Stelle.

Page 67: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 67/90

Für hübsche Explosionen habe ich einen frei im Netz erhältlichen Generator verwendet. Das Ergebnis sieht soaus:

Man sieht mehrere Animationsphasen der Explosion. Wir werden später sehen, wie wir diese auf ein Meshbekommen und die Explosionen damit zeichnen.

Als Font zur Darstellung der Punkte, der Leben und der aktuellen Welle, habe ich mir aus dem Internet einen freiverwendbaren Font namens "Cosmic Alien" besorgt.

Wie die Meshes und das Texturieren im Detail funktionieren, kann ich hier leider nicht erklären, das würde denRahmen des Tutorials mehr als sprengen. Im Netz gibt es dazu aber mehr als genug Material, ich verweise dengeneigten Leser daher an dieser Stelle auf Google.

Mit all den Ressourcen bewaffnet, wenden wir uns jetzt dem Renderer zu.

Renderer Klasse

Ziel der Klasse ist es, erstens alle benötigten Ressourcen zu laden und zu verwalten und zweitens, die Simulationmit diesen Ressourcen zu rendern. Darunter fällt das Zeichnen des Hintergrunds, der Statistiken, sowie derInvader, des Schiffes, der Schüsse, der Explosionen und der Blöcke. Nach getaner Arbeit soll der Renderer auchwieder all die Ressourcen freigeben können.

Unsere Spielwelt werden wir in 3D zeichnen, mit einer direktionalen Lichtquelle, die alle Elemente beleuchtet. DieKamera soll dabei immer etwas über dem Schiff schweben und leicht nach unten auf das Spielfeld blicken.Außerdem soll sie sich mit dem Schiff mitbewegen. Die Invader sollen sich drehen, das Schiff soll sich je nachAusrichtung des AndroidGeräts neigen. Als kleine Motivation hier ein Bild der fertigen Szene:

Page 68: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 68/90

Schauen wir uns zuerst die Attribute der Klasse an:

public class Renderer Mesh shipMesh; Textur shipTextur; Mesh invaderMesh; Textur invaderTextur; Mesh blockMesh; Mesh shotMesh; Mesh backgroundMesh; Textur backgroundTextur; Mesh explosionMesh; Textur explosionTextur; Font font; Text text; float invaderAngle = 0; int lastScore = 0; int lastLives = 0; int lastWave = 0;... to be continued ...

Für das Schiff und die Invader haben wir jeweils ein Mesh, sowie eine Textur. Blöcke und Schüsse besitzen nurein Mesh, die färben wir später händisch ein. Für den Hintergrund haben wir ebenfalls ein Mesh, sowie eineTextur, selbiges gilt für Explosionen. Da wir auch ein paar Statistiken anzeigen wollen, besitzt der Renderer aucheinen Font, sowie eine TextKlasse. Der Member invaderAngle wird zur Speicherung des aktuellen Drehwinkelsder Invader benötigt. Die restlichen drei Attribute merken sich die letzten Werte für Leben, Welle und Punktestand.Die verwenden wir später, um Änderungen dieser Werte zu registrieren. Nur bei Änderungen bauen wir die TextKlasse neu, da dies dem Garbage Collector besser passt.

Bevor wir irgendetwas zeichnen, müssen wir zuerst einmal all unsere Ressourcen laden. Das Ganze machen wirrelativ überraschungslos im Konstruktor:

... continued ...

Page 69: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 69/90

public Renderer( GL10 gl, GameActivity activity ) try shipMesh = MeshLoader.loadObj(gl, activity.getAssets().open( "ship.obj" ) ); invaderMesh = MeshLoader.loadObj( gl, activity.getAssets().open( "invader.obj" ) ); blockMesh = MeshLoader.loadObj( gl, activity.getAssets().open( "block.obj" ) ); shotMesh = MeshLoader.loadObj( gl, activity.getAssets().open( "shot.obj" ) ); backgroundMesh = new Mesh( gl, 4, false, true, false ); backgroundMesh.texCoord(0, 0); backgroundMesh.vertex(‐1, 1, 0 ); backgroundMesh.texCoord(1, 0); backgroundMesh.vertex(1, 1, 0 ); backgroundMesh.texCoord(1, 1); backgroundMesh.vertex(1, ‐1, 0 ); backgroundMesh.texCoord(0, 1); backgroundMesh.vertex(‐1, ‐1, 0 ); explosionMesh = new Mesh( gl, 4 * 16, false, true, false ); for( int row = 0; row < 4; row++ ) for( int column = 0; column < 4; column++ ) explosionMesh.texCoord( 0.25f + column * 0.25f, 0 + row * 0.25f ); explosionMesh.vertex( 1, 1, 0 ); explosionMesh.texCoord( 0 + column * 0.25f, 0 + row * 0.25f ); explosionMesh.vertex( ‐1, 1, 0 ); explosionMesh.texCoord( 0f + column * 0.25f, 0.25f + row * 0.25f ); explosionMesh.vertex( ‐1, ‐1, 0 ); explosionMesh.texCoord( 0.25f + column * 0.25f, 0.25f + row * 0.25f ); explosionMesh.vertex( 1, ‐1, 0 ); catch( Exception ex ) Log.d( "Space Invaders", "couldn't load meshes" ); throw new RuntimeException( ex ); try Bitmap bitmap = BitmapFactory.decodeStream( activity.getAssets().open( "ship.png" ) ); shipTextur = new Textur( gl, bitmap, TexturFilter.MipMap, TexturFilter.Nearest, TexturWrap.ClampToEdge, TexturWrap.ClampToEdge ); bitmap.recycle();

bitmap = BitmapFactory.decodeStream( activity.getAssets().open( "invader.png" )); invaderTextur = new Textur( gl, bitmap, TexturFilter.MipMap, TexturFilter.Nearest, TexturWrap.ClampToEdge, TexturWrap.ClampToEdge ); bitmap.recycle();

bitmap = BitmapFactory.decodeStream( activity.getAssets().open( "planet.jpg" ) ); backgroundTextur = new Textur( gl, bitmap, TexturFilter.Nearest, TexturFilter.Nearest, TexturWrap.ClampToEdge, TexturWrap.ClampToEdge ); bitmap.recycle(); bitmap = BitmapFactory.decodeStream( activity.getAssets().open( "explode.png" ) ); explosionTextur = new Textur( gl, bitmap, TexturFilter.MipMap, TexturFilter.Nearest, TexturWrap.ClampToEdge, TexturWrap.ClampToEdge );

Page 70: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 70/90

bitmap.recycle(); catch( Exception ex ) Log.d( "Space Invaders", "couldn't load Texturs" ); throw new RuntimeException( ex ); font = new Font( gl, activity.getAssets(), "font.ttf", 16, FontStyle.Plain ); text = font.newText( gl ); float[] lightColor = 1, 1, 1, 1 ; float[] ambientLightColor = 0.0f, 0.0f, 0.0f, 1 ; gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLightColor, 0 ); gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_DIFFUSE, lightColor, 0 ); gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_SPECULAR, lightColor, 0 );... to be continued ...

Viel Code zum Laden. Gehen wir es der Reihe nach durch. Zuerst laden wir die Meshes für das Schiff, Invader,Blöcke und Schüsse. Diese liegen, wie gehabt, als Assets vor, dementsprechend laden wir sie auch. Als nächstesbasteln wir uns ein Mesh für das Hintergrundbild. Dieses ist ein Rechteck, bestehend aus zwei Dreiecken, die wirauf die ganze HintergrundTextur mappen. Die linke obere Ecke des Rechtecks liegt bei (1, 1, 0), die untere Eckebei (1, 1, 0). Wir erinnern uns, dass, wenn wir keine Projektionsmatrix setzen, der sichtbare Teil desKoordinatensystems am Bildschirm dieselben Abmessungen hat. Später müssen wir also lediglich dieProjektionsmatrix auf identity setzen und die beiden Dreiecke zeichnen, schon haben wir das Hintergrundbild überden ganzen Bildschirm gespannt.

Als nächstes bauen wir das Mesh für die Explosionen. Hier wird es wieder ein wenig schwieriger. In derExplosionsTextur befinden sich 4 * 4 = 16 Animationsstufen der Explosion. Für jede dieser Animationsstufenbauen wir im Mesh ein eigenes Rechteck aus zwei Dreiecken, beginnend bei der obersten linken Animationsstufe.Ein solches Rechteck hat die Abmessungen 1x1, was ungefähr den Abmessungen der Invader bzw. des Schiffesin der xyAchse entspricht. Insgesamt haben wir in dem Mesh also 16 Rechtecke in Sequenz der Animation derExplosion. Diesen Umstand nutzen wir dann später beim Zeichnen der Explosionen aus. Die Technik nennt manauch Sprite Rendering und sie funktioniert ähnlich, wie in einem Trickfilm. Mehr dazu später.

Nachdem wir jetzt alle Meshes geladen bzw. erstellt haben, können wir uns den Texturen widmen. Auch dieseladen wir wieder unspektakulär aus den Assets. Man beachte hier die Angabe von Mipmapping beim Laden. Diesführt zu einem großen PerformanceGewinn und sollte so gut wie immer verwendet werden.

Abschließend laden wir noch den Font, den wir verwenden wollen und erstellen eine neue Instanz der KlasseText, die wir später zum Rendern der Statistiken verwenden werden.

Licht brauchen wir auch, darum setzen wir die LichttypFarben für die Lichtquelle 0 schon einmal im Konstruktor.Wie im LichtKapitel beschrieben, müssen wir diese Werte nicht jedes Mal neu setzen, OpenGL merkt sich das füruns.

Nachdem jetzt alles geladen ist, können wir uns gleich das Rendering selbst anschauen. Dazu besitzt derRenderer die Methode render, die eine GL10 Instanz, die GameActivity und die Instanz der Simulationentgegennimmt. Die Simulation brauchen wir natürlich, um zu wissen, wo welches Objekt gezeichnet werden soll:

... continued ...public void render( GL10 gl, GameActivity activity, Simulation simulation ) gl.glClear( GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT );

Page 71: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 71/90

gl.glViewport( 0, 0, activity.getViewportWidth(), activity.getViewportHeight() ); gl.glEnable( GL10.GL_TEXTUR_2D ); renderBackground( gl ); gl.glEnable( GL10.GL_DEPTH_TEST ); gl.glEnable( GL10.GL_CULL_FACE ); setProjectionAndCamera( gl, simulation.ship, activity ); setLighting( gl ); renderShip( gl, simulation.ship, activity ); renderInvaders( gl, simulation.invaders );

gl.glDisable( GL10.GL_TEXTUR_2D ); renderBlocks( gl, simulation.blocks );

gl.glDisable( GL10.GL_LIGHTING ); renderShots( gl, simulation.shots );

gl.glEnable( GL10.GL_TEXTUR_2D ); renderExplosions( gl, simulation.explosions );

gl.glDisable( GL10.GL_CULL_FACE ); gl.glDisable( GL10.GL_DEPTH_TEST );

set2DProjection(gl, activity); gl.glEnable( GL10.GL_BLEND ); gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA ); gl.glTranslatef( 0, activity.getViewportHeight(), 0 ); if( simulation.ship.lives != lastLives || simulation.score != lastScore || simulation.wave != lastWave ) text.setText( "lives: " + simulation.ship.lives + " wave: " + simulation.wave + " score: " + simulation.score ); lastLives = simulation.ship.lives; lastScore = simulation.score; lastWave = simulation.wave; text.render(); gl.glDisable( GL10.GL_BLEND); gl.glDisable( GL10.GL_TEXTUR_2D ); invaderAngle+=activity.getDeltaTime() * 90; if( invaderAngle > 360 ) invaderAngle ‐= 360; ... to be continued ...

Wir beginnen damit, den Framebuffer und den ZBuffer zu löschen. Danach setzen wir, wie gehabt, den Viewport.Als nächstes wollen wir den Hintergrund zeichnen. Dazu müssen wir zuerst Texturing einschalten und springendann in eine Methode, die das Rendering selbst übernimmt. Diese werden wir uns später anschauen.

Nachdem der Hintergrund kein 3DObjekt ist und immer ganz gezeichnet werden soll, haben wir bis zu diesemZeitpunkt den ZBuffer noch nicht eingeschaltet. Das machen wir jetzt, da wir die 3DObjekte, wie Invader und dasSchiff, zeichnen wollen. Auch schalten wir so genanntes Backface Culling dazu. Dieses sorgt dafür, dass nur

Page 72: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 72/90

jene Dreiecke gezeichnet werden, die in Richtung des Betrachters "blicken".

Bevor wir unsere 3DObjekte zeichnen, müssen wir noch die Kamera einrichten und die Lichtquelle setzen. Dasübernehmen die Methoden setProjectionAndCamera und setLighting für uns, die wir uns später im Detailanschauen werden.

Wir haben jetzt also den ZBuffer aktiviert, Backface Culling eingeschaltet, die Projektions und KameraMatrixgesetzt, sowie das Licht eingeschalten. Auch Texturing ist noch in Betrieb. Jetzt sind wir bereit, die 3DObjekte zuzeichnen. Das Zeichnen des Schiffes und der Invader übernehmen die Methoden renderShip und renderInvader.Als nächstes schalten wir Texturing aus, da wir die Blöcke und die Schüsse zeichnen. Diese besitzen ja keineTexturen. Hier helfen uns die Methoden renderBlocks und renderShots aus. Bevor wir die Schüsse zeichnen,schalten wir das Licht wieder aus, diese sollen ja keinen Schatten haben. Als letzte 3DObjekte zeichnen wir dieExplosionen. Für diese müssen wir Texturing wieder einschalten. Wir zeichnen sie deshalb als letztes in derReihenfolge, da diese transparent sind. Wie wir uns erinnern, müssen wir transparente Objekte immer zuletztzeichnen, da es sonst zu Problemen mit dem ZBuffer kommt. Auch hier haben wir wieder eine handliche Methodenamens renderExplosions, die uns die Arbeit abnimmt.

Alle 3DObjekte sind gezeichnet, daher können wir Backface Culling und den ZBuffer wieder ausschalten. Wirwollen nun die Statistiken zeichnen, die ja 2DElemente sind. Dazu setzen wir eine entsprechendeProjektionsmatrix, die für ein 2DKoordinatenSystem sorgt. Das macht die Methode set2DProjection. Als nächstesschalten wir Blending ein, da unser Text transparente Stellen besitzt und setzen eine Transformation, damit derText in der oberen linken Ecke des Bildschirms gezeichnet wird. Bevor wir den Text zeichnen, prüfen wir noch, obsich die Statistikwerte, im Vergleich zu den zuletzt gespeicherten Werten, geändert haben. Ist dies der Fall, sagenwir der TextInstanz, dass sie einen neuen String darstellen soll und merken uns die neuen Werte. Die MethodeText.setText baut intern auf Basis des übergebenen Strings neue Dreiecke zusammen, was eine etwaskostspielige Operation ist, wenn man es jedes Frame macht. Auch wäre der Garbage Collector wenig erfreut,wenn wir in jedem Frame einen neuen String erzeugen. Darum machen wir das hier etwas umständlich. Der Textist also gesetzt und wir können ihn einfach zeichnen. Danach schalten wir Blending wieder ab.

Zu guter Letzt erhöhen wir das Attribut invaderAngle noch zeitbasiert. Diesen verwenden wir als Rotationswinkelum die yAchse für die Invader, die sich damit drehen. In einer Sekunde drehen sie sich dabei um 90 Grad, wofürdie Multiplikation der Delta Time mit 90 sorgt.

Machen wir uns noch schnell bewusst, welchen Status OpenGL nach verlassen der render Methode hat. ZBuffer,Beleuchtung, Texturing und Blending sind ausgeschalten. Die ProjektionsMatrix ist auf eine 2DProjektiongesetzt, die ModelViewMatrix auf die Transformation zur Verschiebung in die obere linke Ecke. OpenGL behältdiesen Status bis zum nächsten RenderAufruf. Es ist immer wichtig, sich über diesen Status im Klaren zu sein undihn so sauber wie möglich zu halten, es können sonst unerklärbare Probleme auftreten, wie das Fehlen vonTexturen, die eigentlich nur daraus resultieren, dass das Texturing nicht eingeschalten ist. Selbiges gilt auch fürdie Matrizen, die dafür sorgen können, dass Objekte aus dem Blickfeld verschoben werden, nur weil manvergessen hat, zuerst eine identityMatrix zu laden und damit den alten Inhalt zu löschen.

Schauen wir uns jetzt noch die in der renderMethode verwendeten HelferMethoden an. Wir machen das in derReihenfolge des Auftretens in render:

... continued ...private void renderBackground( GL10 gl ) gl.glMatrixMode( GL10.GL_PROJECTION ); gl.glLoadIdentity(); gl.glMatrixMode( GL10.GL_MODELVIEW ); gl.glLoadIdentity(); backgroundTextur.bind(); backgroundMesh.render(PrimitiveType.TriangleFan); ... to be continued ...

Page 73: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 73/90

Beim Rendering des Hintergrunds setzen wir zuerst Projektions und ModelViewMatrix auf Identity. Damitschaffen wir ein 2DKoordinatensystem, dessen sichtbarer Teil in xy bei (1,1) bis (1,1) geht. Unser Mesh für denHintergrund hat genau dieselben Abmessungen. Wir brauchen daher nur mehr die HintergrundTextur zu bindenund das Mesh zu zeichnen, schon haben wir den ganzen Bildschirm mit dem Hintergrundbild gefüllt. Einenkleinen Fehler hat das ganze: Das Hintergrundbild hat die Abmessungen 512x512 Pixel. Der Bildschirm hat diesehundertprozentig nicht, das Bild wird also gestaucht. Als Übung könnt ihr ja versuchen, dieses Problem zu lösen.

... continued ...private void setProjectionAndCamera( GL10 gl, Ship ship, GameActivity activity ) gl.glMatrixMode( GL10.GL_PROJECTION ); gl.glLoadIdentity(); float aspectRatio = (float)activity.getViewportWidth() / activity.getViewportHeight(); GLU.gluPerspective( gl, 67, aspectRatio, 1, 1000 );

gl.glMatrixMode( GL10.GL_MODELVIEW ); gl.glLoadIdentity(); GLU.gluLookAt( gl, ship.position.x, 6, 2, ship.position.x, 0, ‐4, 0, 1, 0 );... to be continued ...

Auch hier passiert nichts Neues. Wie im Kapitel zu Projektionen beschrieben, setzen wir zuerst eineperspektivische ProjektionsMatrix. Die ModelViewMatrix setzen wir über GLU.gluLookAt auf eine KameraMatrix. Die Kamera befindet sich dabei etwas oberhalb des Schiffes und schaut schräg nach unten auf dasSpielfeld. Interessant dabei ist, dass die xPosition der Kamera von der xPosition des Schiffes abhängt. Mitdiesem Kniff lassen wir die Kamera dem Schiff folgen.

... continued ...float[] direction = 1, 0.5f, 0, 0 ; private void setLighting( GL10 gl ) gl.glEnable( GL10.GL_LIGHTING ); gl.glEnable( GL10.GL_LIGHT0 ); gl.glLightfv( GL10.GL_LIGHT0, GL10.GL_POSITION, direction, 0 ); gl.glEnable( GL10.GL_COLOR_MATERIAL ); ... to be continued ...

Auch das Setzen des Lichtes birgt keine Neuerungen. Zuerst schalten wir das Licht ein, dann, im Speziellen, dasLicht mit Nummer 0. Als nächstes setzen wir die Richtung des Lichtes, das ein direktionales ist. Das Array, das dieRichtung hält, instanzieren wir dabei nicht jedes Mal neu, wir denken an den Garbage Collector. Das Licht kommtin diesem Fall von rechts oben. Abschließend schalten wir Color Materials noch ein und sind fertig.

... continued ...private void renderShip( GL10 gl, Ship ship, GameActivity activity ) if( ship.isExploding ) return;

shipTextur.bind(); gl.glPushMatrix(); gl.glTranslatef( ship.position.x, ship.position.y, ship.position.z ); gl.glRotatef( 45 * (‐activity.getAccelerationOnYAxis() / 5), 0, 0, 1 ); gl.glRotatef( 180, 0, 1, 0 );

Page 74: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 74/90

shipMesh.render(PrimitiveType.Triangles); gl.glPopMatrix();... to be continued ...

Explodiert das Schiff gerade, brauchen wir es natürlich nicht zeichnen. Das Zeichnen der entsprechendenExplosion regeln wir dann in renderExplosions. Explodiert das Schiff nicht, binden wir zuerst die Textur für dasSchiff. Danach folgt, was immer folgen sollte, wir pushen die ModelViewMatrix (die wir zuvor insetProjectionAndCamera aktiviert haben). Die nächsten drei Aufrufe verschieben und rotieren das Schiff. Wirerinnern uns wieder, daas wir die Reihenfolge umkehren müssen. Beginnen wir also bei der letztenTransformation. Diese rotiert das Schiff um 180 Grad um die yAchse. Da ich das Schiff in Wings3D so gemachthabe, dass es entlang der positiven zAchse schaut, müssen wir es hier in die entgegengesetzte Richtungrotieren. Als nächstes sorgen wir mit einer weiteren Rotation für einen schönen Anblick. Davon abhängig, wie dasGerät entlang seiner yAchse geneigt ist, rotieren wir das Schiff um die zAchse. Hält man das Geräte imLandscapeModus und neigt es wie ein Lenkrad nach links und rechts, schlägt der Accelerometer entlang der yAchse im Bereich (10,10) aus. Diesen Werte negieren wir (wegen der Rotationsrichtung) und dividieren ihn durch5, womit wir in einen Wertebereich von (2,2) kommen. Diese Werte multiplizieren wir dann noch mit 45 Grad,womit wir auf den wirklichen Rotationswinkel des Schiffes um die zAchse kommen. Dieser Winkel liegt somit imBereich (45*2, 45*2). Diese Maximalwerte werden erreicht, wenn man das Gerät im PortraitModus hält. Als letzteTransformation verschieben wir das rotierte Schiff noch an seine Position im Spielfeld. Es folgt das Rendern desMeshes und das popen der ModelViewMatrix. Die hat danach wieder nur die KameraMatrix gesetzt und wirkönnen die nächsten Objekte zeichnen.

... continued ...

private void renderInvaders( GL10 gl, ArrayList<Invader> invaders ) invaderTextur.bind(); for( int i = 0; i < invaders.size(); i++ ) Invader invader = invaders.get(i); gl.glPushMatrix(); gl.glTranslatef( invader.position.x, invader.position.y, invader.position.z ); gl.glRotatef( invaderAngle, 0, 1, 0 ); invaderMesh.render(PrimitiveType.Triangles); gl.glPopMatrix(); ... to be continued ...

Beim Zeichnen der Invader gehen wir ähnlich vor. Zuerst setzen wir die InvaderTextur. Danach gehen wir über allInvader in der Simulation (die wir ja als Parameter in der Renderer.render Methode bekommen haben). Für jedenInvader pushen wir zuerst wieder die ModelViewMatrix, transformieren den Invader an seinen richtigen Platz undzeichnen das InvaderMesh. Danach popen wir wieder die ModelViewMatrix. Wie ersichtlich, rotieren wir jedenInvader auch um die yAchse. Den Winkel erhöhen wir in der Renderer.renderMethode zeitbasiert.

... continued ...private void renderBlocks( GL10 gl, ArrayList<Block> blocks ) gl.glEnable( GL10.GL_BLEND ); gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA ); gl.glColor4f( 0.2f, 0.2f, 1, 0.7f ); for( int i = 0; i < blocks.size(); i++ ) Block block = blocks.get(i);

Page 75: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 75/90

gl.glPushMatrix(); gl.glTranslatef( block.position.x, block.position.y, block.position.z ); blockMesh.render(PrimitiveType.Triangles); gl.glPopMatrix(); gl.glColor4f( 1, 1, 1, 1 ); gl.glDisable( GL10.GL_BLEND ); ... to be continued ...

Wie auf dem Bild oben ersichtlich, scheinen die Blöcke leicht durch. Deswegen aktivieren wir zuerst Blending.Auch Textur haben wir für die Blöcke keine, Texturing ist zu diesem Zeitpunkt schon deaktiviert. Wir färben dieBlöcke daher händisch mit glColor4f mit einem hübschen Blau ein. Danach gehen wir wieder über alle Blöcke,pushen die ModelViewMatrix, transformieren den Block, rendern das BlockMesh und popen die ModelViewMatrix. Abschließend setzen wir die Zeichenfarbe wieder auf weiß und schalten Blending aus.

... continued ...private void renderShots( GL10 gl, ArrayList<Shot> shots ) gl.glColor4f( 1, 1, 0, 1 ); for( int i = 0; i < shots.size(); i++ ) Shot shot = shots.get(i); gl.glPushMatrix(); gl.glTranslatef( shot.position.x, shot.position.y, shot.position.z ); shotMesh.render(PrimitiveType.Triangles); gl.glPopMatrix(); gl.glColor4f( 1, 1, 1, 1 );... to be continued ...

Auch die Schüsse sind schnell gezeichnet. Farbe setzen, über alle Schüsse gehen, push, transform, render, popund die Farbe wieder zurücksetzen. Langsam wird es langweilig.

private void renderExplosions(GL10 gl, ArrayList<Explosion> explosions) gl.glEnable( GL10.GL_BLEND ); gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA ); explosionTextur.bind(); for( int i = 0; i < explosions.size(); i++ ) Explosion explosion = explosions.get(i); gl.glPushMatrix(); gl.glTranslatef( explosion.position.x, explosion.position.y, explosion.position.z ); explosionMesh.render(PrimitiveType.TriangleFan, (int)((explosion.aliveTime / Explosion.EXPLOSION_LIVE_TIME) * 15) * 4, 4); gl.glPopMatrix(); gl.glDisable( GL10.GL_BLEND );

Für die Explosionen brauchen wir wieder Blending, das wir gleich einmal einschalten. Dann binden wir dieExplosionsTextur und gehen über jede Explosion. Hier wird es interessant. Wie gehabt, pushen wir zuerst undtransformieren dann. Beim Rendern des Meshes wenden wir aber einen kleinen Trick an. Abhängig davon, wie

Page 76: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 76/90

lange die Explosion schon am Leben ist, zeichnen wir nur eines der Rechtecke im ExplosionsMesh. Wir erinnernuns, dass wir dort ja für die 16 Animationsphasen jeweils ein Rechteck definiert haben. Die MeshKlasse bietetneben der einfachen Mesh.render(PrimitiveType type)Methode auch noch eine zweite, die es erlaubt, einenOffset in das Mesh anzugeben, sowie die Anzahl der Vertices, die ab diesem Offset verwendet werden sollen. Wirerrechnen uns einfach den Offset des passenden Rechtecks und verwenden dessen vier Vertices zum Zeichnender aktuellen Explosion. Das Offset ergibt sich aus der Lebensdauer der Explosion, geteilt durch die maximaleLebensdauer, was einen Wert zwischen 0 und 1 ergibt. Diesen Wert multiplizieren wir dann mit 15 und casten inauf einen int. Nach dieser einfachen Formel lässt sie die Animationsstufe sehr einfach berechnen. Ist dieExplosion z.B. seit einer Sekunde am Leben, wählen wir Rechteck Nummer (int)(1.0 / 2 * 15) = 7. Diesmultiplizieren wir noch mit vier, da wir das Offset in Vertices angeben müssen. Und schon haben wir unsereExplosionen animiert! Natürlich popen wir dann wieder die ModelViewMatrix und schalten Blending wieder aus.

Damit haben wir eigentlich das ganze Rendering der Simulation besprochen. Wir brauchen nur noch eineMethode, die die ganzen Meshes und Texturen aufräumt:

... continued ...public void dispose( ) shipTextur.dispose(); invaderTextur.dispose(); backgroundTextur.dispose(); explosionTextur.dispose(); font.dispose(); text.dispose(); explosionMesh.dispose(); shipMesh.dispose(); invaderMesh.dispose(); shotMesh.dispose(); blockMesh.dispose(); backgroundMesh.dispose();

Wir geben einfach alle geladenen Texturen und Meshes wieder frei, keine große Sache.

Der Renderer in Kombination mit einer SimulationsInstanz wäre jetzt schon vollständig einsetzbar. Wir müsstendas Ganze nur in eine GameActivity packen, die Simulation in jedem Frame aktualisieren und den Rendereranwerfen. Bevor wir dass aber tun, wollen wir uns noch kurz um den Sound kümmern. Soll ja schließlich auchkrachen.

Dieses Kapitel wird wieder erfrischend einfach. Ähnlich dem Renderer brauchen wir eine Klasse, die uns dasLaden der Soundeffekt und Musik schön kapselt. Auch muss sie uns die Möglichkeit bieten ondemand einenSoundeffekt abzuspielen, bzw. die Musik zu stoppen. Wir basteln dazu eine Klasse namens SoundManager, diewir ins gleich im Detail anschauen werden.

Die Soundeffekte für den Space InvadersKlon habe ich mit einem netten Tool namens sfxr gemacht. Dieseserlaubt das einfache erstellen von 8Bitartigen Soundeffekten mit einem einzigen Mausklick. Die so entstandenenEffekte habe ich ein wenig mit Audacity nachbearbeitet. Es gibt einen Effekt für die Explosionen und einen Effektfür die Schüsse.

Die Musik habe ich vor Urzeiten daheim mit Cubase eingespielt. Ist ein unaufregender Rock Track.

SoundManager Klasse

Wie im Renderer müssen wir zuerst einmal alles laden. Das machen wir wieder im Konstruktor:

Sound und Musik

Page 77: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 77/90

public class SoundManager SoundPool soundPool; AudioManager audioManager; MediaPlayer mediaPlayer; int shotID; int explosionID; public SoundManager( GameActivity activity ) soundPool = new SoundPool( 10, AudioManager.STREAM_MUSIC, 0); audioManager = (AudioManager)activity.getSystemService(Context.AUDIO_SERVICE); activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); try AssetFileDescriptor descriptor = activity.getAssets().openFd( "shot.wav" ); shotID = soundPool.load( descriptor, 1 ); descriptor = activity.getAssets().openFd( "explosion.wav" ); explosionID = soundPool.load( descriptor, 1 ); catch( Exception ex ) Log.d( "Sound Sample", "couldn't load sound 'shot.wav'" ); throw new RuntimeException( ex ); mediaPlayer = new MediaPlayer(); try AssetFileDescriptor descriptor = activity.getAssets().openFd( "8.12.mp3" ); mediaPlayer.setDataSource( descriptor.getFileDescriptor() ); mediaPlayer.prepare(); mediaPlayer.setLooping(true); mediaPlayer.start(); catch( Exception ex ) ex.printStackTrace(); Log.d( "Sound Sample", "couldn't load music 'music.mp3'" ); throw new RuntimeException( ex ); ... to be continued ...

Als Attribute halten wir uns je einen SoundPool, einen AudioManager einen MediaPlayer, sowie die späterinitialisierten IDs der beiden Soundeffekte. Im Konstruktor bauen wir zuerst einen SoundPool, der zehn Effektegleichzeitig spielen können soll. Als nächstes holen wir uns den AudioManager, den brauchen wir später noch.Dann sagen wir Android, dass bei Betätigung der VolumeTasten die Medienlautstärke geändert werden soll. Dasist wichtig, sonst wird nur die Klingeltonlautstärke über die Tasten geregelt.

Als nächstes laden wir die beiden Soundeffekte in den SoundPool. Diese liegen als Assets vor. Die beidenerhaltenen Ids, speichern wir in den entsprechenden Attributen.

Nach dem Instanzieren des MediaPlayers lassen wir diesen die MusikDatei abspielen und zwar geloopt.

Natürlich gibt es noch ein paar andere Methoden:

Page 78: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 78/90

... continued ...public void playShotSound( ) int volume = audioManager.getStreamVolume( AudioManager.STREAM_MUSIC ); soundPool.play(shotID, volume, volume, 1, 0, 1); public void playExplosionSound( ) int volume = audioManager.getStreamVolume( AudioManager.STREAM_MUSIC ); soundPool.play(explosionID, volume, volume, 1, 0, 1);... to be continued ...

Zwei Methoden werden es uns später erlauben, die Effekte für Schüsse und Explosionen abzuspielen. Auch hiersollten wir schon alles kennen. Wir holen uns jeweils die aktuelle Medienlautstärke und spielen dann den Effektmit der entsprechenden ID ab.

Zu guter Letzt müssen wir natürlich wieder aufräumen:

public void dispose( ) soundPool.release(); mediaPlayer.release();

Wir geben sowohl den SoundPool, als auch den MediaPlayer frei. Letzteres ist wichtig, da sonst die Musik auchnach dem Schließen der Applikation weiterläuft. Wir dürfen also später nicht vergessen, dieSoundManager.dispose Methode aufzurufen.

Endlich können wir uns dem letzten Baustein unseres Spiels widmen, der Activity und den so genannten Screens.Ein Screen stellt einen Zustand im Spiel dar, in unserem Fall ist das der StartBildschirm, der das Spiellogo zeigt,außerdem eine Aufforderung, den Bildschirm zu berühren, den eigentlichen SpielBildschirm, der die Simulationlaufen lässt und rendert und den GameOverBildschirm, der den erreichten Punktestand anzeigt. Die Aufteilung inScreens erlaubt es uns, die verschiedenen Zustände des Spiels in der eigentlichen Activity zu verwalten. Aufeinen Screen folgt ein anderer. Ein neuer Screen wird aktiviert, sobald der aktuelle beendet ist. Ich habe dazufolgendes Interface definiert:

public interface GameScreen public void update( GameActivity activity ); public void render( GL10 gl, GameActivity activity ); public boolean isDone( ); public void dispose( );

Die update Methode soll den Zustand des Screens aktualisieren. Darunter fällt z.B. das Laufen lassen derSimulation. Die Methode render sollte selbsterklärend sein. Die Methode isDone erlaubt es uns, den Screen zufragen, ob er fertig ist und der nächste Screen angezeigt werden kann. Die Methode dispose werden wir aufrufen,

SpaceInvaders Activity & Screens

Page 79: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 79/90

wenn wir auf den nächsten Screen schalten, damit die Ressourcen des alten wieder freigegeben werden(SoundManager, Renderer).

Wie bereits erwähnt, besitzt unser Space InvadersKlon drei Screens, die jeweils von GameScreen ableiten. Wirwerden diese zuerst besprechen und dann, als letzten Punkt, die Activity betrachten, die all dies steuert.

StartScreen Klasse

Der StartBildschirm soll zum einen unser Hintergrundbild darstellen, sowie ein Logo und den Hinweis, dass derBenutzer den Schirm berühren soll. Der Screen wird beendet, sobald der Benutzer den Screen berührt hat. DasGanze ist nicht allzu schwer zu implementieren, schauen wir es uns an:

public class StartScreen implements GameScreen Mesh backgroundMesh; Textur backgroundTextur; Mesh titleMesh; Textur titleTextur; boolean isDone = false; SoundManager soundManager; Font font; Text text; String pressText = "Touch Screen to Start!";... to be continued ...

Erst mal leiten wir von GameScreen ab. Als nächstes definieren wir uns ein paar Attribute. Dazu zählen die Texturund das Mesh für den Hintergrund, bzw. für das Logo. Ein Boolean namens isDone speichert, ob der Screen fertigist. Einen SoundManager brauchen wir auch, da wir Hintergrundmusik abspielen wollen. Auch eine Instanz vonFont und Text brauchen wir zur TouchAufforderung. Das letzte Attribut ist im Endeffekt nur eine Konstante, die denAufforderungstext hält.

public StartScreen( GL10 gl, GameActivity activity ) backgroundMesh = new Mesh( gl, 4, false, true, false ); backgroundMesh.texCoord(0, 0); backgroundMesh.vertex(‐1, 1, 0 ); backgroundMesh.texCoord(1, 0); backgroundMesh.vertex(1, 1, 0 ); backgroundMesh.texCoord(1, 1); backgroundMesh.vertex(1, ‐1, 0 ); backgroundMesh.texCoord(0, 1); backgroundMesh.vertex(‐1, ‐1, 0 ); titleMesh = new Mesh( gl, 4, false, true, false ); titleMesh.texCoord(0, 0); titleMesh.vertex(‐256, 256, 0); titleMesh.texCoord(1, 0); titleMesh.vertex(256, 256, 0); titleMesh.texCoord(1, 0.5f); titleMesh.vertex(256, 0, 0); titleMesh.texCoord(0, 0.5f); titleMesh.vertex(‐256, 0, 0);

try

Page 80: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 80/90

Bitmap bitmap = BitmapFactory.decodeStream( activity.getAssets().open( "planet.jpg" ) ); backgroundTextur = new Textur( gl, bitmap, TexturFilter.MipMap, TexturFilter.Nearest, TexturWrap.ClampToEdge, TexturWrap.ClampToEdge ); bitmap.recycle();

bitmap = BitmapFactory.decodeStream( activity.getAssets().open( "title.png" ) ); titleTextur = new Textur( gl, bitmap, TexturFilter.Nearest, TexturFilter.Nearest, TexturWrap.ClampToEdge, TexturWrap.ClampToEdge ); bitmap.recycle(); catch( Exception ex ) Log.d( "Space Invaders", "couldn't load Texturs" ); throw new RuntimeException( ex ); soundManager = new SoundManager(activity); font = new Font( gl, activity.getAssets(), "font.ttf", activity.getViewportWidth() > 480?32:16, FontStyle.Plain ); text = font.newText( gl ); text.setText( pressText );... to be continued ...

Wie gewohnt, laden wir hier alle unsere Ressourcen. Zuerst bauen wir das HintergrundMesh, so wie imRenderer. Als nächstes bauen wir das Mesh für das Logo. Dieses definieren wir um den Ursprung inPixelkoordinaten. Es hat die Abmessungen 512x256 und gemapped auf den oberen Teil folgender Textur:

Page 81: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 81/90

Als nächstes laden wir die Texturen für Hintergrund und Logo. Danach instanzieren wir einen neuenSoundManager. Abschließend erstellen wir uns noch einen Font und eine TextInstanz, der wir denAufforderungstext setzen. Interessant hierbei ist, dass ich die Größe des Fonts abhängig von der Viewport Breitemache. Android unterstützt ja mittlerweile mehrere Bildschirmauflösungen. Ein 16PixelFont sieht auf 800x480 zuklein aus, deswegen machen wir im Fall einer hohen Bildschirmauflösung den Font doppelt so groß.

... continued ...@Overridepublic boolean isDone() return isDone;... to be continued ...

Die erste Interface Methode ist relativ einfach, sie liefert nur den Inhalt der isDoneVariablen zurück undsignalisiert so nach außen, ob der Screen fertig ist oder nicht.

... continued ...@Overridepublic void update(GameActivity activity) if( activity.isTouched() ) isDone = true;... to be continued ...

Auch update hält sich simpel. Wir erfragen lediglich von der GameActivity, ob der Screen aktuell berührt wird undsetzen isDone entsprechend. Damit haben wir auch schon die Logik dieses Screens komplett implementiert. Fehltnoch das Rendern:

... continued ...@Overridepublic void render(GL10 gl, GameActivity activity) gl.glViewport( 0, 0, activity.getViewportWidth(), activity.getViewportHeight() ); gl.glClear( GL10.GL_COLOR_BUFFER_BIT ); gl.glEnable( GL10.GL_TEXTUR_2D ); gl.glMatrixMode( GL10.GL_PROJECTION ); gl.glLoadIdentity(); gl.glMatrixMode( GL10.GL_MODELVIEW ); gl.glLoadIdentity(); gl.glEnable( GL10.GL_BLEND ); gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA ); backgroundTextur.bind(); backgroundMesh.render(PrimitiveType.TriangleFan );

gl.glMatrixMode( GL10.GL_PROJECTION ); GLU.gluOrtho2D( gl, 0, activity.getViewportWidth(), 0, activity.getViewportHeight() ); gl.glMatrixMode( GL10.GL_MODELVIEW ); gl.glLoadIdentity();

Page 82: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 82/90

gl.glLoadIdentity(); gl.glTranslatef( activity.getViewportWidth() / 2, activity.getViewportHeight() ‐ 256, 0 ); titleTextur.bind(); titleMesh.render(PrimitiveType.TriangleFan);

gl.glLoadIdentity(); gl.glTranslatef( activity.getViewportWidth() / 2 ‐ font.getStringWidth( pressText ) / 2, 100, 0 ); text.render(); gl.glDisable( GL10.GL_TEXTUR_2D ); gl.glDisable( GL10.GL_BLEND );... to be continued ...

Wir starten wie immer mit dem Setzen des Viewport und dem Löschen des Framebuffers. Als nächstes setzen wirdie Projektions und ModelViewMatrix für das Zeichnen des Hintergrunds. Dasselbe haben wir ja schon imRenderer gemacht. Blending und Texturing schalten wir auch ein. Es folgt das Rendern des Hintergrunds. Danachsetzen wir eine orthographische Projektion, damit wir uns wieder im Bildschirmkoordinatensystem befinden. DasLogo zeichnen wir zentriert am oberen Ende des Bildschirms. Den Text zeichnen wir zentriert 100 Pixel über demunteren Bildschirmrand. Abschließend schalten wir Texturing und Blending wieder aus und sind fertig.

Eine Methode fehlt uns noch:

... continued ...public void dispose() backgroundTextur.dispose(); titleTextur.dispose(); soundManager.dispose(); font.dispose(); text.dispose(); backgroundMesh.dispose(); titleMesh.dispose();

Wie nicht anders zu erwarten, geben wir hier wieder alle Ressourcen frei. Augenmerk soll auch auf das „dispose“des SoundManagers gelegt werden. Damit drehen wir auch wieder die Musik ab, die sonst weiterlaufen würde!

GameOverScreenKlasse

Die GameOverScreen Klasse ist nahezu identisch mit der StartScreen Klasse. Einziger Unterschied ist, dass wiranstatt des Logos in großen Lettern Game Over anzeigen und anstatt der Aufforderung zum Berühren desBildschirms die erreichte Punktezahl anzeigen, die wir über den Konstruktor hereinbekommen. Logik undRendering sind identisch mit der StartScreen Klasse. Ich erspare mir hier also die Auflistung des Codes.

GameLoopKlasse

Der interessanteste Screen, ist der GameLoopScreen. Hier instanzieren wir den Renderer und die Simulation undprozessieren den Input des Accelerometers, um das Schiff zu bewegen. Durch die schöne Kapselung ist dieseKlasse extrem klein:

public class GameLoop implements GameScreen, SimulationListener

Page 83: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 83/90

public Simulation simulation; Renderer renderer; SoundManager soundManager;... to be continued ...

Wir haben nur drei Attribute: die Simulation, den Renderer und einen SoundManager. Sehr hübsch!

... continued ...public GameLoop( GL10 gl, GameActivity activity ) simulation = new Simulation(); simulation.listener = this; renderer = new Renderer( gl, activity ); soundManager = new SoundManager( activity );

public GameLoop(GL10 gl, GameActivity activity, Simulation simulation) this.simulation = simulation; this.simulation.listener = this; renderer = new Renderer( gl, activity ); soundManager = new SoundManager( activity );... to be continued ...

Konstruktoren gibt es zwei an der Zahl. Der erste instanziert seine Simulation selbst, der zweite nimmt eineSimulation von außen entgegen. Den Zweiten werden wir später dazu verwenden, ein pausiertes Spielfortzusetzen. Nach dem Aufruf des Konstruktors haben wir einen voll geladenen Renderer, eine Simulation undeinen SoundManager, der bereits die Hintergrundmusik abspielt.

.... continued ...@Overridepublic void update(GameActivity activity) processInput( activity ); simulation.update( activity.getDeltaTime() );... to be continued ...

Die updateMethode des Screens ist wieder denkbar einfach. Zuerst rufen wir die Methode processInput auf, diedie Accelerometereingabe in Schiffbewegungen umsetzt. Danach aktualisieren wir einfach die Simulation mit deraktuellen Delta Time.

... continued ...private void processInput( GameActivity activity ) if( activity.getAccelerationOnYAxis() < 0 ) simulation.moveShipLeft( activity.getDeltaTime(), Math.abs(activity.getAccelerationOnYAxis()) / 10 ); else simulation.moveShipRight( activity.getDeltaTime(), Math.abs(activity.getAccelerationOnYAxis()) / 10 ); if( activity.isTouched() ) simulation.shot();

Page 84: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 84/90

... to be continued ...

In dieser Methode nehmen wir die Benutzereingabe entgegen. Wir erinnern uns noch an die MethodenmoveShipLeft/moveShipRight der Simulation. Diese befeuern wir jetzt in Abhängigkeit vom Ausschlag desAccelerometers auf der yAchse des Gerätes. Wie wir aus dem Rendering Kapitel wissen, liegt dieser im Bereich(10,10). Durch die Division mit 10 normieren wir diesen auf (1,1). Dieser Faktor bestimmt dann, wie viel von derSchiffsgeschwindigkeit Ship.SHIP_VELOCITY wirklich herangezogen wird, um das Schiff zu bewegen. Je mehrder Benutzer das Gerät neigt, desto schneller fährt das Schiff nach links und rechts. Das ist alles, was wirbrauchen, um das Schiff zu bewegen. Sehr einfach, dank unserer Kapselung.

Auch Schüsse wollen wir abgeben. Dazu fragen wir die GameActivity, ob der Screen berührt wird und rufen indiesem Fall Simulation.shot auf, die für uns das Erstellen eines Schusses, sowie das Prüfen, ob schon ein Schussdes Schiffes im Spielfeld ist, übernimmt. Mehr Code brauchen wir für die Verarbeitung der Benutzereingabe nicht!

... continued ...public boolean isDone( ) return simulation.ship.lives == 0; ... to be continued ...

Der Screen ist fertig, sobald das Schiff kein Leben mehr hat.

... continued ...@Overridepublic void render(GL10 gl, GameActivity activity) renderer.render( gl, activity, simulation);... to be continued ...

Das Rendering übernimmt für uns die Renderer Klasse, der wir einfach die Simulation übergeben.

... continued ...@Overridepublic void dispose( ) renderer.dispose(); soundManager.dispose();... to be continued ...

Und auch das Aufräumen gestaltet sich wieder sehr einfach.

Aufmerksamen Lesern ist vielleicht aufgefallen, dass die Klasse das Interface SimulationListener implementiert.Außerdem setzen wir in den Konstruktoren der Simulation die Instanz der Klasse als Listener. Das nutzen wir aus,um die Soundeffekte abzuspielen!

... continued ...@Overridepublic void explosion()

Page 85: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 85/90

soundManager.playExplosionSound();

@Overridepublic void shot() soundManager.playShotSound();

Jedes Mal, wenn in der Simulation ein Schuss fällt oder eine Explosion erzeugt wird, rufen wir ja den Listener auf.In diesem Fall nutzen wir das um dem SoundManager zu sagen, dass er den entsprechenden Effekt abspielensoll.

Und das war das gesamte Spiel. Es fehlt nur noch die Activity, dann sind wir komplett fertig!

SpaceInvaders Activity

Zum Abschluss müssen wir unsere drei Screens noch irgendwie koordinieren. Auch müssen wir den Activity LifeCycle implementieren. Das machen wir in einer Activity namens SpaceInvaders. Die Activity startet mit demStartScreen und zeigt diesen so lange an, bis dessen isDoneMethode true zurückgibt. Zu diesem Zeitpunktschalten wir auf den GameLoop um und das Spiel startet. Das Ganze machen wir dann auch für den GameLoopbzw. den GameOverScreen. Die Activity muss aber natürlich auch auf onPause und onResume regieren. Wirwollen ja, dass das Spiel fortgesetzt werden kann, wenn es durch einen Anruf oder einem Druck auf die HomeTaste unterbrochen wird. Hier hilft uns die schöne Kapselung der Simulation. Schauen wir uns also an, was dieActivity alles im Code macht:

public class SpaceInvaders extends GameActivity implements GameListener GameScreen screen; Simulation simulation = null; public void onCreate( Bundle bundle ) setRequestedOrientation(0); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN ); super.onCreate( bundle ); setGameListener( this );

if( bundle != null && bundle.containsKey( "simulation" ) ) simulation = (Simulation)bundle.getSerializable( "simulation" );

Log.d( "Space Invaders", "created, simulation: " + (simulation != null) ); ... to be continued ...

Die Activity leitet natürlich von GameActivity ab. Des Weiteren implementiert sie das GameListener Interface. AlsMember hat sie einen GameScreen und außerdem eine Simulation. Im Konstruktor setzen wir das Fenster derActivity zuerst in den LandscapeMode, schalten die Titelleiste ab und gehen Fullscreen. Danach rufen wironCreate der Vaterklasse GameActivity auf, die ja die OpenGLInitialisierung für uns übernimmt und setzen unsselbst als GameListener.

Page 86: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 86/90

Abhängig vom LifeCycle kann der Parameter bundle von uns zuvor gesetzte Instanzen verschiedener Klassenbesitzen. Für das implementieren der ResumeFähigkeit werden wir später in dieses Bundle in die Simulationschreiben, wenn wir uns im GameLoop befinden. Im Konstruktor lesen wir lediglich das Bundle aus undüberprüfen, ob eine Simulation darin enthalten ist. Ist das so, speichern wir sie im Attribut simulation der Activity.

... continued ...@Overridepublic void onSaveInstanceState( Bundle outState ) super.onSaveInstanceState( outState ); if( screen instanceof GameLoop ) outState.putSerializable( "simulation", ((GameLoop)screen).simulation ); Log.d( "Space Invaders", "saved game state" );... to be continued ...

Diese Methode wird vom Android OS aufgerufen, bevor die Activity über den Jordan geht. Sie erlaubt es uns,Zustände temporär zu speichern und diese dann später in onCreate beim Neuerstellen der Activity zu lesen. Ichhabe mir die Freiheit genommen, alle Klassen der Simulation das Serializable Interface implementieren zu lassen.Damit brauchen wir hier lediglich überprüfen, ob gerade der GameLoop aktiviert ist und uns von diesem dieSimulation holen, die wir dann in das Bundle unter dem Schlüssel simulation ablegen. Damit hätten wir einen Teildes LifeCycles fertig.

... continued ...@Overridepublic void onPause( ) super.onPause(); if( screen != null ) screen.dispose(); if( screen instanceof GameLoop ) simulation = ((GameLoop)screen).simulation; Log.d( "Space Invaders", "paused" ); @Overridepublic void onResume( ) super.onResume(); Log.d( "Space Invaders", "resumed" ); ... to be continued ...

Als nächstes müssen wir onPause und onResume implementieren. In beiden rufen wir zuerst die entsprechendeMethode der Superklasse auf, das ist wichtig und darf auf keinen Fall vergessen werden. In onPause geben wirauch den aktuell aktiven Screen frei, wenn dieser bereits gesetzt ist. Dann speichern wir die Simulation desGameLoop in unserem SimulationsObjekt, falls der GameLoop aktiv ist. Wir speichern die Simulation hier, da dieGeschichte mit dem Bundle nicht immer anschlägt und die Activity nicht neu erstellt wird. Als nächstes fehlen nochdie beiden Methoden des GameListener Interface:

... continued ...@Overridepublic void setup(GameActivity activity, GL10 gl)

Page 87: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 87/90

if( simulation != null ) screen = new GameLoop( gl, activity, simulation ); simulation = null; Log.d( "Space Invaders", "resuming previous game" ); else screen = new StartScreen(gl, activity); Log.d( "Space Invaders", "starting a new game" ); ... to be continued ...

Diese Methode wird aufgerufen, wenn die GLSurfaceView erstellt wurde. Dementsprechend initialisieren wir hierden Screen, den wir anfangs verwenden wollen. Welchen Screen wir verwenden, machen wir davon abhängig, obdas Attribut simulation gesetzt ist oder nicht. Für den Fall, dass es gesetzt wurde (über das Bundle in onCreateoder in onPause), erstellen wir einen neuen GameLoop, der mit dieser Simulation arbeitet. Andernfalls erstellenwir einen StartScreen, der den User auffordert, den Bildschirm zu berühren. Die Simulation überlebt dasPausieren der Applikation ohne Probleme, lediglich der Renderer muss neu erstellt werden, da beim Pausierensämtliche Ressourcen, wie Texturs und Meshes, von OpenGL verworfen werden. Kommen wir zum letztenCodeteil in diesem Tutorial:

... continued ...long start = System.nanoTime();int frames = 0; @Overridepublic void mainLoopIteration(GameActivity activity, GL10 gl) screen.update( activity ); screen.render( gl, activity); if( screen.isDone() ) screen.dispose(); Log.d( "Space Invaders", "switching screen: " + screen ); if( screen instanceof StartScreen ) screen = new GameLoop( gl, activity ); else if( screen instanceof GameLoop ) screen = new GameOverScreen( gl, activity,((GameLoop)screen).simulation.score ); else if( screen instanceof GameOverScreen ) screen = new StartScreen( gl, activity ); Log.d("Space Invaders", "switched to screen: " + screen );

frames++; if( System.nanoTime() ‐ start > 1000000000 ) Log.d( "Space Invaders", "fps: " + frames ); frames = 0; start = System.nanoTime();

Page 88: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 88/90

Zuerst wird der aktuelle Screen aktualisiert und gerendert. Als nächstes Fragen wir, ob der Screen fertig ist, damitwir den nächsten Screen aktivieren können. Ist der Screen fertig, geben wir zuerst all seine Ressourcen frei. Alsnächstes prüfen wir, welcher Screen aktiv war. Im Falle des StartScreens, erstellen wir einen neuen GameLoop,im Falle des GameLoop, erzeugen wir einen neuen GameOverScreen und im Falle des GameOverScreenstarten wir den StartScreen neu. Abschließend erhöhen wir ein Attribut der Klasse FrameCounter namens framesund prüfen, ob eine Sekunde seit der letzten ZeitMessung vergangen ist. Ist dies der Fall, geben wir die Anzahlder Frames aus und setzen Startzeit und FrameCounter auf neue Werte. Damit können wir unsere Frames proSekunde messen und ausgeben. Als wirklich allerletzten Punkt müssen wir uns noch anschauen, wie wir dieActivity im AndroidManifest.xml definieren:

<activity android:name=".spaceinvaders.SpaceInvaders" android:label="Space Invaders" android:launchMode="singleTask"> <intent‐filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent‐filter></activity>

Hier sollte es keine großen Überraschungen geben. Am wichtigsten ist der launchMode, den wir auf singleTasksetzen. Dies muss für die GLSurfaceView so sein, da es sonst zu ein paar kleineren Problemen kommen kann.

Wir sind somit fertig und haben unser erstes kleines 3DAndroidSpiel geschrieben, komplett mit Sound und allem,was dazugehört.

Performance ist ein großer Punkt in Android. Da wir mit einer VM arbeiten, die den Code nur interpretiert, alsonicht direkt als CPUAnweisungen ausführt, müssen wir auf ein paar Dinge achten. Hier eine kleine Auswahl derwichtigsten Tipps:

Performance Tipps

Garbage Collection: der Garbage Collector in Androids Dalvik VM ist ein Biest. Er sorgt dafür, dass nicht mehrbenötigte Objekte ihren Speicherbereich freigeben. Dieses Freigeben dauert zwischen 100 und 300Millisekunden, was in einem Spiel fatal ist. Bemerkbar macht sich das durch ein unschönes Stocken desSpielablaufs. Es gilt also zu verhindern, dauernd neue Objekte anzulegen. Am besten instanziert man bereitsvorab alles, was, man braucht und verwertet nicht mehr benötigte Objekte wieder. Damit kann man demGarbage Collector ein Schnippchen schlagen. In unserem Klon haben wir dieses Ziel weitestgehend erreicht.Lediglich das Instanzieren von Schüssen und Explosionen widerspricht dem ein wenig und führt nach längererLaufzeit zu einem kleinen Aussetzer.Floating Point vs. Fixed Point: bisher erhältliche AndroidGeräte besitzen keine Floating Point Unit (imGegensatz zum iPhone). Bei der Verwendung von Fließkommazahlen werden diese in Software emuliert, waseiniges an Performance kosten kann. In den frühen Neunzigern hatten auch viele Desktop PCs noch keineFPU, was Spieleentwickler meist dazu zwang, auf so genannte FixedPoint Arithmetik umzusteigen. Dasselbekönnte man auch auf Android tun, ich rate aber davon ab, zumindest, wenn man nicht die NDK verwendet unddas ganze in C schreibt. Das Implementieren von FixedPoint Arithmetik in Java unter Dalvik ist nur minimalschneller als die Verwendung von Fließkommazahlen. Da der Code unleserlicher wird, rate ich nur inäußersten Notfällen zur Umstellung auf FixedPoint.OpenGL Lighting: Wer das Spiel auf G1 oder Hero Hardware ausprobiert hat, wird feststellen, dass es beivoller Invaderzahl ca. 30fps schafft, bei wenigen Invadern auf bis zu 60fps kommt. Grund dafür ist, dass wir dieBeleuchtung eingeschalten haben. In frühen AndroidGeräten wird die Lichtberechnung meist von der CPUausgeführt und ist dementsprechend langsam. Schaltet man diese im Space Invaders Klon ab, bekommt mankonstante 60fps. Hier muss man entscheiden, ob die verlorene Performance die ansehnlichere Grafik Wert ist.OpenGL MipMapping: Zeichnet man Objekte mit Texturen, muss die GPU die verwendeten Teile der Texturenauslesen. Je größer dabei die Textur, desto mehr Leseoperationen braucht die Texturierung. Mit Hilfe vonMipMapping kann man dies um einiges Verbessern. Beim MipMapping werden von der originalen Textur

Page 89: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 89/90

Natürlich konnte ich ihm Rahmen dieses Tutorials nicht alle Aspekte der Spieleentwicklung und Optimierungbeschreiben. Spieleentwicklung ist ein LearningByDoingProzess. Das Lesen von viel Quellcode wirdniemandem erspart bleiben, der wirklich gute Spiele schreiben will. Im Netz gibt es zu allen möglichen ThemenUnmengen an Material, das sich so auch auf Android anwenden lässt. Abschließend möchte ich daher nocheinige Quellen auflisten, die sich der geneigte Leser zu Gemüte führen kann.

Allgemeine Spieleentwicklung

OpenGL ES

Bei Fragen, Anregungen, Korrekturen und Drohungen einfach eine EMail an badlogicgames at gmail dot comschreiben. Ich bin sehr antwortfreudig.

Macht's gut!

Kategorien: Entwicklung

kleinere Versionen angefertigt. Die GPU prüft bei eingeschaltetem MipMapping dann, wie groß das texturierteDreieck am Bildschirm ist und wählt eine Textur, die eine entsprechende Textur hat. Ein kleines Objekt bedientsich dabei kleinerer Texturen, was wiederum dazu führt, dass weniger Pixel der Textur gelesen werdenmüssen. Ich empfehle daher standardmäßig MipMapping für den Minificationsfilter der Texturen zu verwenden.Die TexturKlasse bietet die entsprechende Funktionalität.

Abschließende Worte

http://www.gamedev.net/ Seite mit unzähligen Artikeln zum Thema Spieleentwicklung und all ihrerSubbereiche.http://www.gamasutra.com/ Industrie orientierte Seite, ebenfalls mit sehr vielen Artikelnhttp://flipcode.com/archives/ Archiv der dereinst ausgezeichneten Flipcode Seite. Unmengen an nützlichenArtikeln zu allen Themenbereichen.http://www.rbgrn.net/content/54gettingstartedAndroidgamedevelopment Seite von Robert Green, Inhabervon Battery Powered Games und Macher von Light Racer und Wixxel. Hat einen sehr interessantenEntwicklerblog mit vielen Tipps und Tricks.

http://www.khronos.org/opengles/sdk/1.1/docs/man/ OpenGL ES 1.1 Manual Pages. Referenz für alleMethoden der Klassen GL10 und GL11http://www.khronos.org/opengles/sdk/1.1/docs/man/ OpenGL ES 1.0 Specification. Die Spezifikation vonOpenGL ES 1.0.http://iphonedevelopment.blogspot.com/2009/05/openglesfromgrounduptableof.html OpenGL ES fromthe ground up. OpenGL ES Tutorial fürs iPhone, kann aber leicht auf Android umgelegt werden.http://wiki.delphigl.com/index.php/Tutorial Delphi GL Tutorial Wiki. Große Sammlung an deutschen Tutorialsfür OpenGL. Das meiste davon lässt sich auch mit OpenGL ES umsetzen.

Page 90: Spieleentwicklung 101 - Android Wiki - AndroidPIT

2/26/2015 Spieleentwicklung 101 Android Wiki AndroidPIT

http://www.androidpit.de/de/android/wiki/view/Spieleentwicklung_101#toc61 90/90

Die Themen auf AndroidPIT

MAGAZIN – Aktuelle News rund um Android

APPS – Alle Apps für Dein Smartphone

HARDWARE News und Spezifikationen

FORUM – Sei Teil der größten AndroidCommunity Europas

AndroidPIT International

Folge uns:

Home Developer Werben Testbericht anfordern Jobs Staff Über uns Impressum AGB

Hilfe

androidpit.de Deutsch