Entwicklung eines 2D Tiled Map LibGDX Game Framework - DHBW KARLSRUHE TINF12B5

 
WEITER LESEN
Entwicklung eines 2D Tiled Map LibGDX Game Framework - DHBW KARLSRUHE TINF12B5
DHBW K ARLSRUHE

                        TINF12B5

                       S TUDIENARBEIT

Entwicklung eines 2D Tiled Map LibGDX
          Game Framework

 Author:                                               Betreuer:

 Armin Benz & Mike Schwörer              Prof. PhD. Kay Berkling

 6712294 & 8045949

                         7. April 2015
Entwicklung eines 2D Tiled Map LibGDX Game Framework - DHBW KARLSRUHE TINF12B5
Inhaltsverzeichnis
1 Vorwort                                                                                                   1

2 Plattformunabhängigkeit                                                                                   1
   2.1    Cocos2D-x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       2
   2.2    MonoGame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .        3
   2.3    Unity3D . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .     3
   2.4    LibGDX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      3
   2.5    Unsere Entscheidung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       4

3 Umsetzung                                                                                                 5
   3.1    Umgebung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      5
          3.1.1   Git und Github . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      5
          3.1.2   Gradle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    7
          3.1.3   jUnit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    11
          3.1.4   Continuous Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .     13
          3.1.5   Metrics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    15
          3.1.6   JavaDoc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    16
   3.2    Das Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      19
          3.2.1   Tiled Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    19
          3.2.2   Entity System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    28
          3.2.3   Layer System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .     37
          3.2.4   Kollisionserkennung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      39
          3.2.5   Settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .   59
          3.2.6   Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      61
          3.2.7   Hintergründe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .     62
          3.2.8   Der Menü Layer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       65
   3.3    Performance Optimizations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      74
          3.3.1   Entity Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .   74
          3.3.2   Immutable Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      76
          3.3.3   Der GarbageCollector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       77

4 Fazit                                                                                                    78

                                                      I
Entwicklung eines 2D Tiled Map LibGDX Game Framework - DHBW KARLSRUHE TINF12B5
Listings
  1    Eclipse Optionen in gradle setzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .   9
  2    Gradle in der Kommandozeile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       10
  3    Eine Assert Methode für zwe Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . .      11
  4    TravisCI Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    13
  5    Gradle in der Kommandozeile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       17
  6    TravisCI Konfiguration für das Erzeugen von JavaDoc . . . . . . . . . . . . . . . . . .         17
  7    Das Javadoc Publish Bashscript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      18
  8    Ermitteln der aktuell sichtbare Tiles . . . . . . . . . . . . . . . . . . . . . . . . . . . .   26
  9    Beipiel einer TMX Datei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .     27
  10   Framerate unabhängiges bewegen eines Entities . . . . . . . . . . . . . . . . . . . . . .       32
  11   Die Klasse EntityCollisionGeometry . . . . . . . . . . . . . . . . . . . . . . . . . . . .      40
  12   Erkennen von Kollisionen (Kreis-Kreis) . . . . . . . . . . . . . . . . . . . . . . . . . .      42
  13   Erkennen von Kollisionen (Kreis-Dreieck) . . . . . . . . . . . . . . . . . . . . . . . . .      42
  14   Erkennen von Kollisionen (Box-Box) . . . . . . . . . . . . . . . . . . . . . . . . . . .        43
  15   Erkennen von Kollisionen (Kreis-Box) . . . . . . . . . . . . . . . . . . . . . . . . . . .      44
  16   Erkennen von Kollisionen (Box-Dreieck) . . . . . . . . . . . . . . . . . . . . . . . . .        45
  17   Erkennen von Kollisionen (Dreieck-Dreieck) . . . . . . . . . . . . . . . . . . . . . . .        45
  18   Berechnen der CollisionMap-Position aus der Tile-Position . . . . . . . . . . . . . . . .       47
  19   Bewegen einer Geometrie in der CollisionMap . . . . . . . . . . . . . . . . . . . . . .         48
  20   Kommutative Eigenschaft von canCollideWith . . . . . . . . . . . . . . . . . . . . . . .        58
  21   Eine Beispiel AGDXML Menü Definition . . . . . . . . . . . . . . . . . . . . . . . . .          72

                                                  II
Entwicklung eines 2D Tiled Map LibGDX Game Framework - DHBW KARLSRUHE TINF12B5
Abbildungsverzeichnis
  1    Git Network Graph mit smartgit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .        6
  2    Screenshot SmartGit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       7
  3    Gradle Dependency Graph absGDX . . . . . . . . . . . . . . . . . . . . . . . . . . . .            9
  4    Beispiel der jUnit Test Anzeige . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    11
  5    Beispiel der Online Ansicht von TravisCI . . . . . . . . . . . . . . . . . . . . . . . . .       15
  6    absGDX Metrics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       15
  7    absGDX Test Coverage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .       16
  8    Visualisierung einer Tiled Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .     19
  9    Visualisierung des FixedMapScaleResolver . . . . . . . . . . . . . . . . . . . . . . . .         21
  10   Visualisierung des ShowCompleteMapScaleResolver . . . . . . . . . . . . . . . . . . .            22
  11   Visualisierung des MaximumBoundaryMapScaleResolver . . . . . . . . . . . . . . . .               23
  12   Visualisierung des MinimumBoundaryMapScaleResolver . . . . . . . . . . . . . . . . .             24
  13   Visualisierung des LimitedMinimumBoundaryMapScaleResolver . . . . . . . . . . . .                25
  14   Visualisierung des SectionMapScaleResolver . . . . . . . . . . . . . . . . . . . . . . .         26
  15   Lebenszyklus eines einzelnen Entities . . . . . . . . . . . . . . . . . . . . . . . . . . .      29
  16   Riemann-Integral über die Beschleunigung eiens Entities . . . . . . . . . . . . . . . . .        34
  17   Spezielle Entities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .   36
  18   Der Layer Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .    38
  19   Beispiel eines Entities mit mehreren Hitboxen . . . . . . . . . . . . . . . . . . . . . . .      41
  20   3 Möglichkeiten einer Kreis-Dreieck Kollision . . . . . . . . . . . . . . . . . . . . . . .      42
  21   Kollisionserkennung Kreis-Rechteck (1) . . . . . . . . . . . . . . . . . . . . . . . . . .       44
  22   Kollisionserkennung Kreis-Rechteck (2) . . . . . . . . . . . . . . . . . . . . . . . . . .       44
  23   Performance Graph der CollisionMap . . . . . . . . . . . . . . . . . . . . . . . . . . .         50
  24   Berechnung des minimalen X-Abstands zweier Kreise . . . . . . . . . . . . . . . . . .            52
  25   Berechnung des minimalen X-Abstands eines Kreises und eines Rechtecks . . . . . . . .            53
  26   Berechnung des minimalen X-Abstands zweier Rechtecke . . . . . . . . . . . . . . . .             53
  27   Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (1) . . . . . .            54
  28   Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (2) . . . . . .            55
  29   Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (3) . . . . . .            55
  30   Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (4) . . . . . .            56
  31   Discrete-Time-Issue: Ein Objekt "phased" durch ein anderes . . . . . . . . . . . . . . .         58
  32   Die absGDX Einstellungen in Baumdarstellung . . . . . . . . . . . . . . . . . . . . . .          60
  33   Beispiel der Debugansicht (In-Game) . . . . . . . . . . . . . . . . . . . . . . . . . . .        61
  34   Beispiel der Debugansicht (In-Menu) . . . . . . . . . . . . . . . . . . . . . . . . . . . .      62

                                                  III
35   Eine Seitensansichts-Map mit Hintergrund . . . . . . . . . . . . . . . . . . . . . . . . .   63
36   Erklärung zu Hintergründen mit Bewegungsparallaxe . . . . . . . . . . . . . . . . . . .      64
37   Klassendiagramm der Menü Elemente . . . . . . . . . . . . . . . . . . . . . . . . . . .      65
38   Die neun Texturen einer Menüfläche . . . . . . . . . . . . . . . . . . . . . . . . . . . .   67
39   Der AGDXML Tag Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .      70
40   Screenshot des AGDXML Menudesigner . . . . . . . . . . . . . . . . . . . . . . . . .         73
41   Performancevergleich LinkedList vs. ArrayList . . . . . . . . . . . . . . . . . . . . . .    76
42   Verlauf des RAMs unter Betrachtung des GC . . . . . . . . . . . . . . . . . . . . . . .      77

                                              IV
Erklärung
gemäSS ğ 5 (3) der Studien- und Prüfungsordnung DHBW Technik vom 22. September 2011.

Ich habe die vorliegende Arbeit selbstständig verfasst und keine anderen als die angegebenen

                            Quellen und Hilfsmittel verwendet.

 ———————————                   ———————–              ———————————————

             Ort                     Datum                       Unterschrift

                                             V
1    Vorwort

Eine groSSe Anzahl von Spielen, vor allem sogenannte mobile Games teilen sich eine Reihe von Merk-

malen. So ist ein GroSSteil dieser Spiele zumindest von der Spiellogik her zweidimensional und die Karte

ist in einem Grid angeordnet. Neben der gerasterten Karte gibt es eine Reihe von Entities die sich frei

auf dieser bewegen können. Die Welt ist demnach entweder eine Draufsicht oder eine Seitenansicht. Im

letzteren wird oft einfache zweidimensionale Physik und Kollisionserkennung benötigt.

    Da dies Merkmale sind, welche diese Gruppe von Spielen gemeinsam haben, ist es unsere Idee ein

Framework zu entwickeln, dass diese Funktionalitäten abstrahiert und es dem Entwickler somit einfacher

macht Spiele dieser Art zu entwickeln. Solche einfacheren 2D Spiele wie oben beschrieben findet man

besonders häufig auf Handys und Tablets. Dies liegt einerseits an den schwächeren Geräten, welche es

nicht ohne weiteres schaffen grafisch aufwendigere 3D Umgebungen darzustellen und andererseits daran,

dass von solchen Gelegenheitsspielen einfache und schnell begreifbare Spielprinzipien erwartet werden.

    Von technischer Seite haben wir uns dagegen entschieden ein Framework von Grund auf, ohne jede

Basis zu schreiben. Denn in dieser Richtung gibt es schon mehrere fertige Produkte und es wäre unnötig

hier das Rad neu zu erfinden. Die Basis die wir für unser Projekt gewählt haben ist LibGDX. LibGDX

ist ein OpenGL Framework mit dem Ziel Programme für unterschiedliche Plattformen zu kompilieren.

Unterstützt werden unter anderen auch Android Geräte und Desktop PCs. Dies bietet den Vorteil, dass

man seine Programme auf dem Rechner schnell testen und debuggen kann und nur ab und zu auch auf

ein echtes Android Gerät exportieren muss. Hauptsächlich aus diesem Grund haben wir uns entschieden

LibGDX als Grundlage für unser Framework zu verwenden

    Das Ziel, dass wir uns für unser Produkt gesetzt haben, ist es damit einfache 2D Spiele mit einer

Tilemap und einem Entity-System entwickeln zu können. Unser Framework übernimmt die Verwaltung

von Map, Entities und optional Elementen wie Menü und Netzwerk. Und durch LibGDX kann man das

Projekt direkt am PC testen.

2    Plattformunabhängigkeit

Ein entscheidender Vorteil das unser Framework von Anfang an haben soll ist die Möglichkeit den Code

sowohl für Android als auch für Windows zu kompilieren. Da wir mit Java arbeiten bedeutet dies, dass

                                                   1
wir einerseits Bytecode für die DalvikVM (Android) und die JVM (Windows) erzeugen müssen. Trotz-

dem ist unser Ziel nicht Spiele zu erstellen die man auf beiden Plattformen vertreiben kann. Denn neben

den verschiedenen Java Umgebungen gibt es zwischen der Plattform PC und der Plattform Smartpho-

ne auch noch andere wichtige Unterschiede. Vor allem die unterschiedliche BildschirmgröSSe und die

verschiedenen Eingabemethoden sorgen dafür, dass eine Handyspiel nicht einfach auf den Rechner zu

portieren ist ohne auf diesem fremd zu wirken. Trotzdem bietet die Möglichkeit eine App, welche spezi-

ell für Smartphones entwickelt wurde, auf dem PC auszuführen einige Vorteile. Hauptsächlich führt es zu

einem einfacheren Arbeitszyklus des Entwicklers. Möchte man normalerweise eine App testen muss man

sie entweder, mehr oder weniger umständlich, auf sie echte Hardware kopieren und dort ausführen oder

einen Emulator benutzen welcher oft Anforderungen wie Geschwindigkeit und Korrektheit nicht gerecht

wird. AuSSerdem wird das Finden von Fehlern und das Debuggen des Programms vereinfacht wenn man

es direkt auf der Maschine ausführen kann auf der es auch entwickelt wird. Die Möglichkeit das Projekt

auch auf einem PC auszuführen ist somit für den Entwickler gedacht und idealerweise verlässt ein solches

Build niemals den internen Entwicklerkreis.

   Obwohl sowohl Android als auch Windows (und Mac/Linux) die Möglichkeit besitzen Java Code

auszuführen ist es dennoch nicht einfach Code zu schreiben der ohne Veränderungen auf beiden Platt-

formen läuft. Ebenfalls ist der plattformunabhängige Zugriff auf die OpenGL Treiber nicht einfach da es

hier viele kleinere Unterschiede gibt.

   Die Aufgabe ein Framework zu schreiben welches die Aufgabe übernimmt Code soweit zu abstra-

hieren, dass er sowohl in der DalvikVM als auch in der JVM lauffähig ist, würde den Umfang dieser

Studienarbeit sprengen - wir haben uns deshalb dafür entschieden ein schon bestehendes Framework zu

benutzen. IM folgenden werden einige unserer Optionen vorgestellt.

2.1 Cocos2D-x

Cocos2D-x ist ein Open-Source C++ Framework. Dies ist unter Android ein besonderer Vorteil weil die

DalvikVM umgangen wird und nativer Code auf dem Gerät ausgeführt wird. Man verliert dadurch jedoch

einige Features der VM wie Garbage Collecting, gewinnt aber unter Umständen an Performance weil eine

Zwischenschicht wegfällt. Cocos2D-x kann neben Windows und Android auch nach iOS kompilieren und

auch mittels Javascript und HTML5 Browser spiele erzeugen.

                                                   2
Neben der Fähigkeit Programme für verschiedene Systeme zu erzeugen kommt Cocos2D-x auch mit

einer Vielzahl an Bibliotheken und Werkzeugen um Spiele zu entwickeln an.

2.2 MonoGame

MonoGame ist ein Open-Source C# Framework. Es unterstützt, ebenso wie Cocos2D-x, mehrere Ziel-

plattformen. Neben Windows, Android und iOS kann man hier seine Programme aber auch auf der Play-

station oder XBox ausführen.

   MonoGame sieht sich selbst als Nachfolger des jetzt nicht mehr weiterentwickelten    1   XNA Frame-

work 4. Zu diesem Zweck wurde das Interface von Microsoft XNA 4 fast vollständig implementiert und

in neueren Versionen auch erweitert. Ein groSSer Vorteil von MonoGame ist .NET Framework. Microsoft

stellt mit dem .NET 4.5 ein sehr mächtiges Framework zur Verfügung und mit MonoGame ist man in der

Lage all seine Features zu nutzen.

2.3 Unity3D

Unity3D ist das wohl gröSSte Framework, dass wir uns angeschaut haben. Es ist nicht nur eine Ansamm-

lung von Bibliotheken sondern stellt auch eine Vielzahl an Werkzeugen und Editoren zur Verfügung. Der

zentrale Code wird in C# geschrieben jedoch flieSSen weitere Skriptsprachen wie Boo oder UnityScript

in die Projekte mit ein. Zusätzlich zu einer speziell für Unity3D entwickelten IDE gibt es auch Editoren

für die verschiedenen Modellformate die Unity3D benutzt.

   Unity3D ist - wie der Name es schon sagt - primär eine dreidimensionale Spieleengine, kann jedoch

auch auf zwei Dimensionen reduziert werden. Unity3D unterstützt eine Vielzahl an Zielplattformen. Nicht

nur Windows, Mac, Linux, Android und iOS sondern auch Konsolen wie die Playstation 3, Playstation 4,

XBox360 und die Wii. Zwar ist es nicht möglich die Spiele nach Javascript zu kompilieren jedoch könne

sie über den Unity-WebPlayer ähnlich wie Flash Inhalte in eine HTML Seite eingebunden werden.

2.4 LibGDX

Als letztes schauen wir uns LibGDX an. LibGDX ist ein OpenSource Java-Framework. Es ist primär auf

Android, PC und Browser ausgelegt obwohl es auch Möglichkeiten gibt den Code nach iOS zu portieren.
   1 http://microsoft-news.com/microsoft-confirms-end-of-xnadirectx-development/

                                                   3
LibGDX ist im Vergleich zu den bisher vorgestellten Framework eher leicht gehalten, obwohl es eine

Reihe an Bibliotheken gibt um seine Funktionen zu erweitern.

2.5 Unsere Entscheidung

Aus diesen verschiedenen Möglichkeiten haben wir als Basis für unser Framework LibGDX ausgewählt.

   Wir haben uns dagegen entschieden Cocos2D zu verwenden, da der native Code zwar Vorteile hat,

eines unserer Ziele es jedoch war das Entwickeln von Spielen für den Entwickler zu vereinfachen, und

dies geht mit einer interpretierten Sprache einfacher. AuSSerdem kommt Cocos2D schon mit einer so gut

ausgebauten Sammlung an Werkzeugen für zweidimensionale Spiele, dass wir entweder nur sehr wenig

selber zu programmieren hätten oder an vielen Stellen schon bestehende Funktionalitäten neu Schreiben

müssten.

   MonoGame wäre unsere beste Wahl gewesen wenn es nicht einen starken Nachteil unter Android

gäbe: Für Android gibt es keine kostenlose .NET VM und somit keinen kostenlosen Weg Programme die

mit MonoGame geschrieben wurden auf Android auszuführen. Die einzigen bestehenden .NET Imple-

mentierungen unter Android sind kostenpflichtig und scheiden somit für uns aus.

   Unity ist zwar eine sehr potente Spiele Engine, macht es jedoch durch das abgeschlossene Ökosystem

mit eigener IDE etc. nicht besonders einfach etwas eigenes darauf aufzubauen. AuSSerdem ist es primär

eine 3D Engine mit sehr vielen Features für den dreidimensionalen Raum. Hier eine 2D Engine aufzu-

bauen würde primär bedeuten Features wegzuschneiden und deshalb haben wir uns gegen Unity3D als

Basis für unser Framework entschieden.

   Zum Schluss blieb nur noch LibGDX übrig: Wir haben uns für LibGDX entschieden weil es all un-

seren Anforderungen gerecht wurde, man kann Code (in Java) schreiben und sowohl für die DalvikVM

als auch für die JVM kompilieren. LibGDX ermöglicht es mittels eines eigenen Wrappers plattformunab-

hängig auf OpenGL zuzugreifen, Trotzdem haben wir noch relativ direkten Zugriff auf die OpenGL API

und können unser eigenes Rendering betreiben. Dies sorgt auch dafür, dass es leicht für uns ist OpenGL

als zweidimensionalen Renderer zu benutzen. Da LibGDX Gradle als Build Tool verwendet ist es eben-

falls einfach LibGDX als Dependency in unserem eigenen Projekt einzubinden und somit stellt es kein

Problem dar ein eigenes Framework zu bauen welches LibGDX als Grundlage für seine Plattformunab-

hängigkeit nimmt.

                                                  4
Es ist noch zu erwähnen, dass LibGDX durchaus ein paar allgemein gehaltene Funktionen besitzt

um die Spiele Entwicklung zu vereinfachen. Diese Benutzen wir jedoch nicht, da dass Ziel mit unserem

Framework ist, einen viel spezielleren Fall (2D Tiled Games) abzudecken.

3       Umsetzung

3.1 Umgebung

3.1.1    Git und Github

Da wir an diesem Projekt zu zweit programmieren war es notwendig den Code zentral auf einem Server

zu haben so dass wir beide Zugriff darauf haben. AuSSerdem brauchten wir ein System um Konflikte zu

regeln falls wir zum Beispiel zeitgleich die gleiche Datei verändern wollten.

    Deshalb haben wir beschlossen ein Revision Control System zu verwenden. Genauer gesagt git1 . Mit-

tels git ist der Code in einer Repository organisiert in der die Änderungen jeweils mit Commits eingepflegt

werden. Auf Github haben wir ein Remote-Repository angelegt auf das wir beide unsere Änderungen pus-

hen. Somit bekommt jeder die Änderungen des jeweils anderen mit und falls ein Konflikt auftritt kann

und muss dieser ebenfalls in git oder mit einem externen Tools gelöst werden. Dieses System sorgt damit

dafür, dass man einfach zusammen an einem Projekt arbeiten kann.
    1 http://git-scm.com/

                                                    5
Abbildung 1: Git Network Graph mit smartgit

       AuSSerdem bietet git noch den Vorteil dass man Änderungen durch die einzelnen Commits zurück-

verfolgen kann und den Code unter Umständen auch auf einen alten Stand zurücksetzen kann. Besonders

beim Suchen nach Fehlern kann man so den Commit identifiziern bei dem dieser das erste mal aufgetreten

ist.

       Git ist an sich ein dezentrales Protokoll und jeder Teilnehmer hat eine eigenständige Kopie der Repo-

sitory auf seinem Gerät. Das bedeutet falls einer unserer Rechner ausfällt - oder sogar der zentrale Server

von Github, sind keine Daten verloren gegangen da der Code und die komplette History immer noch bei

den übrigen Personen vorhanden ist. Dies ist jedoch kein Ersatz für ein Backup, da es trotzdem möglich

ist die Repository zu zerstören oder die History zu fälschen. (3) S 5f

       Zwar kann man git direkt über eine Kommandozeile bedienen, jedoch ist es oft einfacher und über-

sichtlicher dafür spezielle Programme zu benutzen die einem Änderungen und die History visualisieren

können. Wir benutzen dafür das Programm smartgit, da dies für Open Source Projekte kostenlos ist.

                                                      6
Abbildung 2: Screenshot SmartGit

3.1.2   Gradle

Zwar commiten wir unser Code mit git in unsere Repository, jedoch nicht alle Dateien. Es gibt eine Reihe

an Dateien die nur temporär sind und beispielsweise bei jedem Kompiliervorgang neu erzeugt werden. Es

wäre sinnlos solche Dateien in die Versionskontrolle aufzunehmen, vor allem da sie sich immer ändern

würden und so die History mit unnötigen Daten belasten würden. Es gibt jedoch auch Dateien die zwar

wichtig sind, trotzdem aber nicht in die Versionskontrolle gehören. Dies sind einerseits Konfigurationsda-

teien mit lokalen Pfaden die ungültig auf anderen System wären, IDE spezifische Dateien die unnütz für

Nutzer anderer IDE’s wären oder externe Libraries die oftmals zu groSS sind um sie alle in die Repository

zu commiten.

   Trotzdem sind Libraries und IDE-Dateien wichtig und eine dritte Person, die das Projekt zum ersten

mal öffnet sollte diese Dateien nicht erst selbst erstellen beziehungsweise manuell herunterladen müssen.

   Um diese Probleme kümmert sich nun ein weiteres Tool namens gradle 1 . Gradle ist ein Build-

Managment-Automatisierung-Tool was bedeutet, dass wir in unser Git-Repository nur noch die gradle

Konfigurationsdateien commiten und diese dann jeweils auf den Entwicklerrechnern ausführen. Diese

löst dann die Projektabhängigkeiten auf und lädt fehlende Libraries automatisch nach und erstellt au-

SSerdem Dateien wie die IDE-Projektdateien automatisch.
   1 http://www.gradle.org/

                                                    7
Dependency Management

   In gradle kann man angeben von welchen Bibliotheken oder Projekten ein Programm abhängig ist.

Diese Bibliotheken können dann wiederum von anderen Bibliotheken abhängig sein und so bildet sich ein

Abhängigkeitsbaum ausgehen vom Anfangsprojekt. Gradle lädt dann diese Bibliotheken, falls sie noch

nicht auf dem Rechner vorhanden sind. Dies hat auch den Vorteil, dass wenn eine Bibliothek öfters im

Abhängigkeitsbaum auftaucht sie trotzdem nur einmal geladen werden muss. (1) S. 55

   Gradle unterstützt viele verschiedene Repositories in welcher nach fehlenden Bibliotheken gesucht

werden kann, standardmäSSig wird jedoch die Maven Central Repository 1 benutzt. In dieser Repository

sind sehr viele der frei verfügbaren java Libraries in vielen Versionen enthalten und können zum Beispiel

einfach über gradle geladen werden.

Konfiguration

   Die Konfiguration geschieht über build.gradle Dateien welche in der Sprache Groovy geschrieben

sind. absGDX ist ein Multiprojekt, dies bedeutet, dass es aus mehreren Gradle Projekten besteht mit

jeweils eigenen Konfigurationen die voneinander abhängen. (1) S. 79ff

   In der folgenden Grafik kann man unseren Abhängigkeitsbaum sehen. Die orange markierten Felder

sind unsere Projekte und die anderen sind externe Abhängigkeiten.
   1 http://search.maven.org/

                                                   8
Abbildung 3: Gradle Dependency Graph absGDX

   Das Projekt absGDX-framework ist hierbei das eigentliche Framework das wir entwickeln. asGDX-

test ist das Testprojekt, es enthält alle Unit-tests für das Framework. absGDX-core und die beiden Platt-

formprojekte desktop und android sind zum testen und debuggen. Wird das Framework später für ein

Projekt benutzt kommen in diese Projekte der eigentliche Code und absGDX-framework wird als De-

pendency eingebunden. Während wir jedoch das Framework noch entwickeln sind diese Projekte not-

wendig damit wir es auch ausprobieren können.

   Da wir alle an dem Projekt mit der Eclipse IDE entwickeln haben wir ein paar extra Eclipse Einstel-

lungen in die build.gradle Dateien ausgelagert:

eclipse.jdt.file.withProperties { props ->
   props.setProperty(’org.eclipse.jdt.core.formatter.
      number_of_blank_lines_at_beginning_of_method_body’, ’0’)
   props.setProperty(’org.eclipse.jdt.core.formatter.
      number_of_empty_lines_to_preserve’, ’1’)
   props.setProperty(’org.eclipse.jdt.core.formatter.
      put_empty_statement_on_new_line’, ’true’)
   props.setProperty(’org.eclipse.jdt.core.formatter.tabulation.char’
      , ’tab’)
   props.setProperty(’org.eclipse.jdt.core.formatter.tabulation.size’
      , ’4’)

                                                   9
props.setProperty(’org.eclipse.jdt.core.formatter.use_on_off_tags’
       , ’false’)
    props.setProperty(’org.eclipse.jdt.core.formatter.
       use_tabs_only_for_leading_indentations’, ’false’)
    props.setProperty(’org.eclipse.jdt.core.formatter.
       wrap_before_binary_operator’, ’true’)
    props.setProperty(’org.eclipse.jdt.core.formatter.
       wrap_before_or_operator_multicatch’, ’true’)
    props.setProperty(’org.eclipse.jdt.core.formatter.
       wrap_outer_expressions_when_nested’, ’true’)
    // ...
}

    Dies ist ein gutes Beispiel wie mächtig die Groovy Konfigurationsdateien sind, wir fügen in die ge-

nerierten Eclipse property files hier noch unsere eigenen Felder ein. In diesem Beispiel setzen wir die

Einstellungen für projektspezifische Formatierungen, damit der automatische Quellcodeformattierer bei

allen die das Projekt in Eclipse laden gleich funktioniert.

Verwendung in unserem Projekt

    Um Problemen mit verschiedenen Versionen von gradle entgegen zu wirken ist in unserer git Re-

pository nicht nur die build.gradle Dateien vorhanden sondern auch der gradle Wrapper. Dies ist eine

vollständige unabhängige Version von gradle die man anstatt der lokal installierten verwenden soll. Da-

mit ist garantiert, dass jeder die gleiche Version von gradle verwendet.

    Ausgeführt wird sie dann über die Kommandozeile mit Befehlen wie

> gradlew cleanEclipse eclipse afterclipseImport

    Dieses Beispiel führt zuerst den Task cleanEclispe aus um alle Dateien die mit der Eclipse IDE zu-

sammenhängen zu löschen, dann werden sie mit eclipse neu aus den gradle Einstellungen erzeugt und zu-

letzt werden mit afterEclipseImport einige Änderungen vorgenommen. Der letzte Task ist von LibGDX

vorgegeben damit das Android Projekt richtig konfiguriert ist.

                                                     10
3.1.3   jUnit Tests

Um die Codequalität zu erhöhen haben wir uns entschieden unseren Code mit Unittests abzusichern. Die

Unittests schreiben wir mit Test-Framework jUnit 4 1 . Dies hat den Vorteil, dass es schon fertige Eclipse

Plugins gibt um die Tests auszuführen und die Ergebnisse zu visualisieren.

                              Abbildung 4: Beispiel der jUnit Test Anzeige

   Das Ziel der Unittests ist es einen GroSSteil der Funktionen und Methoden des Frameworks zu testen

in dem sie mit verschiedenen Parametern getestet werden und dann überprüft wird ob die Ergebnisse

stimmen. Besonders bei der Kollisionserkennung ist dies nützlich, da wir so überprüfen können ob nach

Änderungen an der Collisionmap beispielsweise noch alle Kollisionen erkannt werden. Und es gibt sehr

viele verschiedene Fälle von Kollisionen die überprüft werden müssen.

Eigene Asserts

   Da das Ergebnis meist in Form eines Vector2 vorliegt haben wir eine Assert Methode geschrieben,

welche überprüft ob zwei Vektoren gleich sind

   1 http://junit.org/

                                                   11
public static void assertEqualsExt(Rectangle expected, Rectangle
   actual, float epsilon) {
      boolean a = !fcomp(expected.x, actual.x, epsilon);
      boolean b = !fcomp(expected.y, actual.y, epsilon);
      boolean c = !fcomp(expected.width, actual.width, epsilon);
      boolean d = !fcomp(expected.height, actual.height, epsilon);

          if (a || b || c || d) {
                throw new AssertionFailedError("expected: but was:" );
          }
}

private static boolean fcomp(float expected, float actual, float
   delta) {
      return Float.compare(expected, actual) == 0 || (Math.abs(
         expected - actual)
jUnit ist seinerseits von der Hamcrest Bibliothek abhängig1 . Das Android Projekt android, dass wir zum

Kompilieren für Android verwenden ist von den Android Libraries und absGDX-framework abhängig.

Problematisch wird es hier jedoch da auch die Standard Android Libraries Hamcrest enthalten und Ham-

crest im root Ordner der jar/apk eine Datei LICENSE.txt anlegen will. Der Android Compiler verbietet es

jedoch, dass zwei Libraries die gleiche Datei anlegen wollen (auch wenn es in diesem Fall zweimal die

gleiche Bibliothek ist). Gradle hat hierfür eine Lösung. Mit der Einstellung packagingOptions.exclude

kann man Dateien aus dem Paket ausschlieSSen. Der Eclipse Compiler ist jedoch nicht so intelligent und

somit führt das alles dazu, dass wir nur noch mit gradle das Android projekt bauen können und nicht

mehr direkt in Eclipse.

   Die Lösung hierfür bestand darin die Unittests in ein eigenes Projekt absGDX-test auszulagern wel-

ches von absGDX-framework abhängig ist. Somit ist absGDX-framework selbst nicht mehr von jUnit

und hamcrest abhängig und das Projekt android bekommt die hamcrest-Bibliothek nur noch einmal ein-

gebunden.

3.1.4   Continuous Integration

gZusammen mit den Unittests und Git wenden wir auch noch eine dritte Praktik an: Continuous Inte-

gration. Jedes mal wenn jemand auf den main-Branch unseres Repositories pushed wird automatisch ein

Skript auf einem Buildserver ausgeführt. Dieses Skript cloned die Repository lokal auf den Buildserver

und versucht den Code zu kompilieren. Dies wird zusätzlich vereinfacht durch unsere Benutzung von

Gradle und dem gradlewrapper. Wir müssen nur gradlew mit den jeweiligen Parametern aufrufen und

Gradle lädt automatisch alle benötigten Dependencies nach und buildet das Projekt.

   Als Buildserver benutzen wir den kostenlosen Online-Buildservice TravisCI 2 welcher mit einer Text-

datei ".travis.yml" im Repository root konfiguriert wird.

language: java

before_install:
- chmod +x gradlew

install: ./gradlew desktop:assemble

   1 http://hamcrest.org/JavaHamcrest/
   2 https://travis-ci.org/Mikescher/absGDX

                                                    13
script:
 - ./gradlew absGDX−framework:check
 - ./gradlew absGDX−test:check
 - ./gradlew core:check
 - ./gradlew desktop:check

notifications:
  email:
    recipients:
       - m.......@m.........de
       - b...........@g.....com
    on_success: always
    on_failure: always

    Unser Skript ruft zuerst das OS command chmod -x auf der gradlewrapper Datei auf um sicherzu-

stellen, dass wir die Rechte haben die Datei auszuführen. Dies ist nötig da unser CI Server Linux als

Betriebssystem hat. Danach wird als erster Befehl gradlew desktop:assemble aufgerufen. Dies führt den

gradle Task desktop:assemble aus, es wird versucht das Projekt zu einer jar zu kompilieren. Schlägt dies

fehl, weil zum Beispiel gepushed wurde ohne den Code vorher getestet zu haben, dann schlägt das ganze

Skript fehl und es wird eine Email an alle recipients geschickt.

    Eigentlich müsste an dieser Stelle auch android:assemble aufgerufen werden, leider ist dies auf dem

Linux Server von Travis nicht möglich. Den Fall, dass das Projekt nur auf Android nicht mehr lauffähig

ist haben wir also nicht automatische abgedeckt und dies müssen wir manuell testen.

    Nach dem erfolgreichen Build Vorgang werden die anderen Punkte unter script abgearbeitet. Hier

liegt die zweite Stärke von Continuous Integration wir führen für alle Subprojekte die Unittests aus, vor

allem für absGDX-test, in welchem sich alle Tests unseres Frameworks befinden. Es ist also gesichert,

dass bei jedem Push auf den Main Branch automatisiert überprüft wird, ob das Projekt "broken" ist,

sowohl vom reinen Compiler Standpunkt als auch von den Unittests her und in beiden Fällen werden alle

Teilnehmer des Projektes jeweils per E-Mail über den Zustand informiert.

    Zusätzlich können sowohl Teilnehmer des Projektes als auch Personen die es benutzen wollen sich

online den aktuellen Zustand ansehen:

                                                    14
Abbildung 5: Beispiel der Online Ansicht von TravisCI

3.1.5   Metrics

Code Metrics

   Um eine Übersicht über den aktuellen Stand unseres Codes zu erhalten setzten wir das Metrics Tool

Metrics 1.3.9 ein.

                                  Abbildung 6: absGDX Metrics

   Es zeigt eine Reihe an Statistiken über den Quellcode an und warnt wenn Teile des Codes komplex

                                                15
werden. Die Warnungen sind jedoch nicht immer korrekt, in unserem Beispiel sind drei Methoden mit

Problemen markiert, wir haben uns jedoch bei allen drei entschieden sie so zu lassen. Zwar sehen sie rein

statistisch sehr komplex aus, jedoch sind sie aus einer menschlichen Sichtweise sehr gut zu lesen.

   Trotzdem sind die Metriken nützlich um schnell und einfach einen Überblick über den aktuellen Stand

zu bekommen und eventuelle Problemstellen zu identifizieren

Test Coverage

   Da wir Unit Tests mit jUnit einsetzen, ist eine interessante Frage wie viele Statements von den Unit

Tests abgedeckt werden. Das Code Coverage Tool EclEmma analysiert hierfür unsere Tests und zeigt -

nach Paket sortiert - jeweils an wie viele Statements abgedeckt sind

                                 Abbildung 7: absGDX Test Coverage

   Wie hier zu sehen ist haben wir keine hundert prozentige Testabdeckung. Trotzdem sind wichtige und

fehleranfällige Pakete wie collisiondetection, math oder mapscaleresolver fast vollständig abgedeckt. Für

die Zukunft kann es durchaus von Vorteil sein noch mehr des Quellcodes mit Tests abzudecken. Jedoch

sind vorerst die wichtigsten Methoden schon abgedeckt.

3.1.6   JavaDoc

Da unser Ziel es ist ein Framework zu entwickeln, dass auch von anderen Entwicklern genutzt wird doku-

mentieren wir alle öffentlichen Methoden. Mithilfe des Dokumentationswerkzeugs javadoc kann man aus

diesen Quellcode Kommentaren HTML Dokumentationsseiten erzeugen. Im nachfolgenden möchte ich

                                                   16
die verschiedenen Schritte und Werkzeuge aufzeigen welche wir benutzen um eine Dokumentationsseite

automatisch online zu hosten.

   Die eigentlichen Dokumentationskommentare sind als Kommentare in den Quellcodedateien vorhan-

den und werden somit auch in unser git Repository commited. Unsere IDE Eclipse hilft uns dabei die

Kommentare syntaktisch korrekt nach den javadoc Vorgaben zu schreiben, so dass diese später geparsed

werden können.

   Das Erzeugen der entsprechenden HTML, CSS, und JS Dateien wird komfortabler Weise von gradle

übernommen. Im gradle Plugin java gibt es den Task javadoc und mit dem folgenden Befehl kann man

sich einfach eine javadoc Webseite generieren lassen:

> gradlew absGDX-framework:javadoc

   Die nächste Frage ist wo man das javadoc hosten soll. Hierfür gibt es bei unserem git Hoster GitHub

die sogenannten GitHub-pages. Mit ihnen kann man eine beliebige HTML Seite für jede Repository

anlegen. Dafür pushed man die entsprechenden Dateien in ein eigenen Branch gh-pages. Unter der URL

http://USERNAME.github.io/REPOSITORY/ kann man dann auf diese Webseite zugreifen. In unserem

Fall kann man unter http://mikescher.github.io/absGDX/javadoc/ online die Dokumentation einsehen.

   Trotzdem ist es müSSig nach jeder Änderung die Dokumentation neu zu erstellen und hochzuladen.

Dieser Prozess wartet nur darauf, dass man dies irgendwann einmal vergisst. Wir können jedoch unser

Continuous Integration Skript verwenden um nicht nur das Projekt zu überprüfen sondern auch um au-

tomatisiert bei jedem push auf das Repository die javadoc Dateien zu aktualisieren. Hierfür legen wir in

dem Ordner ".utility" das Bash Skript "push-javadoc-to-gh-pages.sh" an. In unserer ".travis.yml" Konfi-

gurationsdatei müssen wir dann nach einem erfolgreichen Test zuerst das javadoc erzeugen und dann die

Dateien per git commiten. Ein Problem ist dabei, dass wir vom TravisCI Server aus keine Rechte haben

auf unser Repository zu pushen. Deshalb generieren wir für unser Repository ein Access Token und geben

dieses verschlüsselt in unserem Skript an. Dies ist möglich da TravisCI automatisch für jede Repository

ein RSA Schlüsselpaar generiert. Unser Token haben wir nun mit dem Public Key verschlüsselt und nur

Travis kann ihn mit dem Private Key wieder entschlüsseln. Somit sind wir sicher davor, dass sich jemand

anders die (öffentliche) ".travis.yml" Datei ansieht und den Access Token missbraucht.

language: java
env:
  global:

                                                  17
- secure: "?? encrypted ??"

before_install:
 - chmod +x gradlew
 - chmod +x .utility/push-javadoc-to-gh-pages.sh

after_success:
 - ./gradlew absGDX-framework:javadoc
 - .utility/push-javadoc-to-gh-pages.sh

   In unserem eigentlichen Skript überprüfen wir zuerst einmal ob wir auch wirklich die aktuelle Do-

kumentation commiten wollen. Zuerst muss der Commit auf dem richtigen Repository erfolgt sein, zur

Sicherheit damit niemand aus versehen das Skript forked und dann auf seiner Repository ausführt. Au-

SSerdem muss es ein Commit auf dem Branch master sein und ein echter push (nicht nur ein Pull Re-

quest).

   Danach kopieren wir die generierten Dateien in das Verzeichnis "tmp_jd" und klonen mit dem ima-

ginären Benutzer "travis-ci" und unserem Access Token den aktuellen Zustand der Branch gh-pages.

Die gerade geklonten Dateien löschen wir jedoch sofort wieder und ersetzen sie mit den Neuen. Für

unveränderte Dateien ist das effektiv eine No-Operation, nur geänderte Dateien wurden mit den neue-

ren Versionen ersetzt. Diese geänderten Exemplare werden dann mit "git add" zum index hinzugefügt.

Danach werden sie mit "git commit" committed und mit "git push" zum Remote (Github) gepushed.

if [ "$TRAVIS_REPO_SLUG" == "Mikescher/absGDX" ] && [ "
   $TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "
   master" ]; then
      cp -R absGDX-framework/build/docs/javadoc $HOME/tmp_jd

          cd $HOME

          git config --global user.email "travis@travis-ci.org"
          git config --global user.name "travis-ci"
          git clone --branch=gh-pages https://${GH_TOKEN}@github.com/
             Mikescher/absGDX gh-pages
          cd gh-pages

          git rm -rf ./javadoc

          cp -Rf $HOME/tmp_jd ./javadoc

          git add -f .
          git status
          git commit -m "Lastest javadoc on successful travis build

                                                18
$TRAVIS_BUILD_NUMBER auto-pushed to gh-pages"
         git push -f origin gh-pages
fi

3.2 Das Framework

3.2.1   Tiled Map

Als Karte benutzt absGDX ein Tiled-Map System. Die Karte ist unterteilt in viele einzelne quadratische

Tiles. Jedes Tile hat eine eigene Textur und zusammen bilden sie die Karte. Die Texturen kommen meist

aus einer sogenannten Tilemap in der eine groSSe Anzahl an Texturen in einer einzigen Datei zusammen-

gefasst sind.

                             Abbildung 8: Visualisierung einer Tiled Map

     In absGDX wird eine solche Map durch eine Instanz der Klasse TileMap repräsentiert. Diese Klasse

verwaltet intern seine einzelnen Tiles in einem zweidimensionalen Array und bietet Methoden um Tiles

                                                  19
abzufragen oder zu ändern.

   Die einzelnen Tiles müssen von der abstrakten Klasse Tile abgeleitet werden. Man kann seine Tiles

entweder direkt von dieser Klasse ableiten oder eine Unterklasse benutzen die jeweils für speziellere Fälle

zugeschnitten sind:

    • AnimationTile Wenn von AnimationTile abgeleitet wird kann man der Tile nicht nur eine statische

      Textur zuweisen sondern eine sich wiederholende Animation

    • EmptyTile EmptyTiles sind Tiles ohne Textur oder Logik - sie werden standardmäSSig für neue

      Maps eingesetzt

    • StaticTile StaticTiles haben eine statische Textur und keine Logik, rein-grafische Tiles könne hier-

      von abgeleitet werden

    • AutoTile AutoTiles sind nützlich in Zusammenhang mit aus Dateien geladenen Maps, sie beziehen

      ihre Textur automatisch aus der gegebenen Tilemap

Ermitteln der absoluten GröSSe

   Intern besitzen alle Tiles die Dimension 1.0 x 1.0. Beim eigentlichen Anzeigen müssen diese Werte je-

doch auf eine konkrete Pixelzahl skaliert werden. Dies kann komplex werden da besonders mobile Geräte

in einer Vielzahl an Auflösungen und Verhältnissen kommen. AuSSerdem kann es anwendungsspezifi-

sche Vorgaben geben, wie beispielsweise eine minimale Anzahl an Tiles die auf jeden Fall sichtbar sein

sollten. Um dies zu lösen gibt es verschiedene Ansätze, jeweils repräsentiert durch eine Klasse abgeleitet

von AbstractMapScaleResolver. Ein MapScaleResolver berechnet aus den Kartendimensionen und den

Dimensionen des Anzeigegerätes die echte Höhe und Breite eines Tiles in pixel. StandardmäSSig sind in

absGDX sechs verschiedene MapScaleResolver enthalten:

   FixedMapScaleResolver

Dies ist der einfachste MapScaleResolver, er gibt immer eine konstante, vorher bestimmte GröSSe zu-

rück. (zum Beispiel 60px)

                                                    20
Abbildung 9: Visualisierung des FixedMapScaleResolver

   ShowCompleteMapScaleResolver

Hier wird immer die komplette Karte angezeigt, falls das Verhältnis von Kartenbreite und Höhe nicht

mit dem des Anzeigegerätes übereinstimmt gibt es Teile des Bildschirms die nicht von der Karte bedeckt

sind.

                                                 21
Abbildung 10: Visualisierung des ShowCompleteMapScaleResolver

   MaximumBoundaryMapScaleResolver

Bei dieser Lösung wird eine Grenzfläche festgelegt die immer angezeigt wird, beispielsweise drei mal

drei Tiles. Es kann vorkommen dass mehr Tiles angezeigt werden (z.B.: 3x3.5 Tiles), es ist jedoch garan-

tiert, dass die Grenzfläche selbst immer zu sehen ist.

                                                    22
Abbildung 11: Visualisierung des MaximumBoundaryMapScaleResolver

   MinimumBoundaryMapScaleResolver

Dies ist das Gegenstück zum MaximumBoundaryMapScaleResolver. Hier wird eine Grenzfläche fest-

gelegt die nie überschritten wird. Ist diese zum Beispiel 10x5 Tiles kann es sein, dass nur 10x3 angezeigt

werden - es wird aber nie mehr als die Grenzfläche gezeigt und immer versucht sich dieser soweit wie

möglich anzunähern ohne die Karte zu verzerren.

                                                   23
Abbildung 12: Visualisierung des MinimumBoundaryMapScaleResolver

   LimitedMinimumBoundaryMapScaleResolver

Dieser MapScaleResolver baut auf dem MinimumBoundaryMapScaleResolver auf. Er nimmt zuerst

die gleichen Regeln wie dieser, beinhaltet aber einen Ausnahmefall. Es ist wird niemals mehr als x Prozent

der ursprünglichen Grenzfläche weggeschnitten. Diese Regel hat eine höhere Priorität als die minimale

Grenzfläche und somit kann es dazu kommen, dass eben doch mehr als die Grenzfläche gezeigt wird.

                                                   24
Abbildung 13: Visualisierung des LimitedMinimumBoundaryMapScaleResolver

   SectionMapScaleResolver

Dies ist eine nochmalige Erweiterung des LimitedMinimumBoundaryMapScaleResolver. Neben der

maximalen Schnittfläche gibt es hier jetzt auch noch eine minimale GröSSe eines Tiles. Ein Tile kann

niemals kleiner als eine angegebene Anzahl Pixel werden, diese Regel hat die höchste Priorität und kann

gegebenenfalls die anderen beiden auSSer Kraft setzen.

                                                  25
Abbildung 14: Visualisierung des SectionMapScaleResolver

Das Rendern

   Das Rendern der Karte wird von dem aktuellen Layer übernommen, dies ist in den meisten Fällen ein

GameLayer da nur dieser eine TiledMap besitzt. In den meisten Fällen ist nicht die ganze Map sichtbar

da der aktuelle MapScaleResolver die Tiles groSS genug macht um mehr als das gesamte Display aus-

zufüllen. Deshalb gibt es im GameLayer einen MapOffset. Dieser gibt an wie weit die Karte in X und

Y Richtung verschoben ist. Beim Rendern ist nun darauf zu achten, dass man nur die Tiles rendert die

ganz oder teilweise sichtbar sind, man könnte zwar auch einfach alle anzeigen dies wäre jedoch nicht sehr

performant. Es ist jedoch einfach zu berechnen welche Tiles aktuell sichtbar sind: (2) S 232f.

public Rectangle getVisibleMapBox() {
      float tilesize = mapScaleResolver.getTileSize(owner.
         getScreenWidth(), owner.getScreenHeight(), map.height, map.
         width);

                                                    26
Rectangle view = new Rectangle(map_offset.x, map_offset.y,
            owner.getScreenWidth() / tilesize, owner.getScreenHeight() /
             tilesize);

         return view;
}

Laden aus dem TMX Format

    TileMaps kann man mit dem Programm Tiled erstellen     1   welche TMX Dateien erstellt. Da dies ein

bekanntes Format ist habe wir auch in absGDX die Funktion implementiert solche Dateien zu laden. Eine

TMX Datei ist eine einfache xml Datei mit einem vorgegebenen Format 2 :

   H4sIAAAAAAAAC72d2Z5r13HeD/DtFs1zlMFJbjLcA==
  
    TMX unterstützt verschiedene Karten und TilegröSSen, mehrere Layer und auch fest eingebunde-

ne Tilesets. Die eigentlichen Daten sind entweder in XML, CSV oder als base64-binär Daten kodiert.

Zusätzlich kann entweder keine, gzip oder zlib Kompression vorliegen3 .

    Als XML Parser haben wir uns für die XMLReader Library entschieden, welche schon in LibGDX

integriert ist und auf allen Zielplattformen funktioniert. XMLReader ist ein java-XML-DOM Parser, dies

bedeutet, dass die komplette Datei analysiert und in den Speicher geladen wird. Dies macht das Auswer-

ten der Datei sehr einfach, verbraucht aber mehr Arbeitsspeicher als beispielsweise ein SAX-Parser. Da

die TMX Dateien jedoch nicht sehr groSS sind im Verhältnis zum gewöhnlich verfügbaren Arbeitsspei-

cher sollte dies keine Probleme bereiten.

    Das Parsen einer TMX Datei wird von der Klasse TmxParser übernommen. Nach dem Laden der

XML Datei geht dieser zuerst die einzelnen Layer von dem niedrigsten angefangen durch. Für jeden
    1 http://www.mapeditor.org/
    2 http://mapeditor.org/dtd/1.0/map.dtd
    3 https://github.com/bjorn/tiled/wiki/TMX-Map-Format#data

                                                  27
Layer müssen die Daten dekodiert werden. Sind sie in gzip oder zlib komprimiert muss man sie zuerst

dekomprimieren. Sind die Daten dann in XML kodiert kann der XOM Parser einfach ebenfalls die Daten

parsen. Auch CSV kodierte Daten können recht einfach mit der in java enthaltenen String.split Funktion

analysiert werden. Bei Base64 Daten ist der Prozess jedoch komplizierter. Zuerst muss der String als

Reihe von Bytes interpretiert werden. Danach gruppiert man jeweils 3 bytes zusammen und interpretiert

sie als Little-Endian unsigned 32bit-Integer. Der entstehende Int32-Array sind dann die GIDs der Tiles.

   In allen 3 Fällen erhält man einen Integer Array. Die einzelnen Integer sind die sogenannten GIDs,

welche den Tiletyp eindeutig identifiziern (anhand der Texturposition im Tileset). Die GIDs fangen bei 1

an, eine 0 bedeutet "kein Tile". Unser TmxParser erstellt dann für jede GID die entsprechende Tile und

bildet aus ihnen eine TileMap.

   Die Zuordnung von GID und Tile-Klasse muss der Entwickler bestimmen. Er kann dafür mit der

Methode addMapping(int, class) eine GID einer bestimmten Tile-Klasse zuordnen. Beim Erstellen der

Map wird dann jeweils nach einer solchen Zuordnung gesucht. Falls es eine gibt wird mittels java Re-

flection der Konstruktor der Klasse ermittelt und eine neue Instanz erzeugt. Dafür ist es notwendig dass

alle Klassen die von Tile ableiten entweder einen Konstruktor ohne Parameter haben oder einen der eine

Settingsmap nimmt. Eine Settingsmap ist eine Hashmap in welcher verschiedene In-

formationen über das Tile enthalten sind wie Position, GID, MapgröSSe etc. Falls vorhanden wird immer

der Konstruktor mit der Hashmap genommen, nur falls dieser nicht existiert wird der leere benutzt.

   Eine geschickte Verwendung dieser Mechanik ist beispielsweise die AutoTile Klasse. Setzt man im

Mapping diese Klasse als Default Mapping ein und sonst keine andere Klasse werden alle GIDs auf Au-

toTile gemappt. Die AutoTile Klasse hat dann einen Konstruktor mit einer Settingsmap und berechnet

aus der darin enthaltenen GID zurück welche Textur im TILED MapEditor verwendet wurde. Man ver-

liert zwar somit die Möglichkeit unterschiedlichen Tiles speziellen Code zuzuordnen, braucht jedoch bei

einem groSSen Tileset nicht sehr viele Klassen die sich kaum unterscheiden.

   Man muss dem Programm nur die Tilmap und das Tileset zur Verfügung stellen und kann die Karte

direkt mit Grafiken in das Programm laden.

3.2.2   Entity System

Unser GameLayer besteht bisher nur aus einer Karte, dem Hintergrund. Zusätzlich werden aber auch

noch Objekte auf dem Spielfeld benötigt, wir nennen diese Objekte die Entities. Jede Entity ist von der

                                                  28
Klasse Entity abgeleitet und muss im aktuell aktiven GameLayer über die Methode addEntity(Entity e)

hinzugefügt wird.

   Der GameLayer übernimmt dann die Verwaltung des Lebenszyklus aller Entities.

                         Abbildung 15: Lebenszyklus eines einzelnen Entities

   Jede Entity hat ein alive Feld welches am Anfang auf true gesetzt wird, das Entity ist solange aktive

bis das Feld auf false gesetzt wird und das Entity im nächsten Zyklus entfernt.

   Entities besitzen, neben dem alive Attribut, eine Reihe weiterer Eigenschaften welche sie definieren.

Sie besitzen eine eindeutige zweidimensionale Position und eine Höhe und Breite, ihre Grundform ist

somit ein einfaches Rechteck. AuSSerdem verwalten sie ihre aktuelle Geschwindigkeit und Beschleu-

nigung um ein einfaches Interface zur flüssigen Bewegung zu bieten. Zuletzt kenne sie auch noch die

Textur, oder Texturen, welche sie zeichnen sollen.

                                                     29
Rendering

   Jedes Entity bestimmt selbst wie es gerendert wird, hierfür kann ein Entity entweder eine einzelne

statische Textur haben oder einen Array von Texturen welcher als Animation gerendert wird. AuSSerdem

stehen Optionen zur Verfügung wie Drehung oder Verzerrung der Textur. Auch muss die gezeichnete

Textur nicht mit der Position oder den AusmaSSen des eigentlichen Entities übereinstimmen, theoretisch

kann vollkommen frei auf dem aktuellen OpenGL Kontext gezeichnet werden.

   Zusätzlich hat jede Entity auch noch einen Z-Layer gesetzt welcher bestimmt welche Entities im

Vordergrund und welche im Hintergrund gezeichnet werden. Besetzen zwei Entities, beziehungsweise

deren Texturen, ganz oder teilweise den selben Punkt wird die Entity mit dem gröSSeren T-Layer über

der anderen gezeichnet. Die Verwaltung hiervon übernimmt der GameLayer.

Collision Detection

   Ein wichtiges Feature der Entities ist es Kollisionen mit anderen Entities zu erkennen, auf die techni-

schen Einzelheiten wird im Paragraph Kollisionserkennung genauer eingegangen.

   Aus der Sicht eines Entities ist es einfach, so dass man mithilfe der Methode addCollisionGeo(float

relativeX, float relativeY, CollisionGeometry geo) dem Entity eine Kollisionsgeometrie zuordnen kann.

Diese kann Kollisionen mit anderen Kollisionsgeometrien erkennen. Der Vorteil dieser Methode ist es,

dass Programme die ganz oder teilweise keine Kollisionserkennung brauchen keinen Performance Nach-

teil haben wenn diese (unnütz) im Hintergrund berechnet wird.

   In den meisten Fällen werden jedoch keine sonderlich komplexen Geometrien eingesetzt und noch

seltener hat ein Entity mehrere Geometrien, die ihm zugeordnet sind. Deshalb gibt es drei Methoden um

schnell und einfach die gebräuchlichsten Fälle abzudecken:

    • addFullCollisionBox(): Fügt ein Kollisions Rechteck hinzu mit gleichen AusmaSSen wie das En-

      tity

    • addFullCollisionCircle(): Fügt ein Kollisions Kreis hinzu, welcher das Entity komplett ausfüllt

    • addFullCollisionTriangle(AlignCorner4 align): Fügt ein Kollisions Dreieck hinzu, welches drei

      Ecken des Entities benutzt (Abhängig von dem übergebenen Alignment).

                                                   30
Jede dieser Geometrien wird zwar wiederum vom GameLayer verwaltet, kennt aber sein jeweiliges

Vater-Entity. Und sobald eine Kollision erkannt wird, wird das Entity über eine Event-Methode benach-

richtigt. Dies ist im Grunde ein Listener-Pattern bei dem das Entity automatisch als Listener als seiner

Geometrien eingetragen wird, hierfür besitzt jedes Entity auch die Interfaces CollisionListener und Col-

lisionGeometryOwner.

   Über das Interface CollisionListener werden vier Methoden implementiert:

    • onActiveCollide

    • onPassiveCollide

    • onActiveMovementCollide

    • onPassiveMovementCollide

   Das active bedeutet jeweils, dass die Kollision durch eine Bewegung der eigenen Geometrie ausge-

löst wurde, und das passiv entsprechend dass der Grund die Bewegung der anderen Geometrie war. Ist

die Veränderung der Position Teil einer Bewegung wird die Methode onActiveMovementCollide oder on-

PassiveMovementCollide aufgerufen. Wurde vorher festgelegt, dass die beiden Entities sich nicht über-

schneiden dann entspricht dies einem ZusammenstoSS, bei dem das bewegende Entity zum Stillstand

gekommen ist und als Resultat beide Entity sich nur berühren nicht schneiden. Ist dies andererseits das

Resultat eines direkten Aufrufs von setPosition(float x, float y), dann werden die Methoden onActiveCol-

lide oder onPassivCollide aufgerufen.

Framerate unabhängige Bewegung

   Eine der grundsätzlichen Aufgaben eines Entities ist es sich zu bewegen. Als Benutzer dieses Frame-

works hat man dazu zwei grundsätzliche Optionen. Einerseits kann man über setPosition(float x, float y)

das Entity in jedem Update Zyklus manuell auf eine Position setzen. Dies hat jedoch Nachteile. Man muss

sich bei dieser Methode selbst darum kümmern, dass die Bewegung unabhängig von der Updaterate ist,

denn auch mit nur halb so vielen Updates pro Sekunde sollte sich jedes Entity immer noch gleich schnell

bewegen. Andernfalls wäre das Spiel nur auf einem Rechner mit gleichen oder ähnlichen UPS Updates

per second wie denen des Entwicklerrechners spielbar. AuSSerdem erschwert dies das Programmieren

der allgemeinen Spiellogik. In den meisten Fällen denkt man über Objekte nicht in der Form von "An

                                                  31
diesem Zeitpunkt sind sie an diesem Ort" sondern eher mit welcher Geschwindigkeit sie sich in welche

Richtung bewegen oder wie stark sie beschleunigen.

   Um dies zu Lösen hat unser Entity - wie oben schon erwähnt - die Eigenschaften speed und List. Als Programmierer kann man nun entweder den Vektor Geschwindigkeit (speed) beein-

flussen um dem Entity eine Geschwindigkeit und eine Richtung vorzugeben, oder man gibt ihm eine

oder mehrere Beschleunigungen (accelerations) welche ihrerseits wiederum Vektoren sind welche die

Geschwindigkeit beeinflussen. Die genaue Berechnung der Position wird dann von Entity selber über-

nommen und Framerate unabhängig berechnet. Ein zusätzlicher Vorteil dieses Weges ist, dass man besser

mit Kollisionen arbeiten kann. Hat man festgelegt, dass sich zwei Entities nicht überschneiden dürfen,

dann bewegen sich diese nicht ineinander. Es werden die Entities stattdessen nur soweit bewegt bis sie

sich berühren und danach wird die Geschwindigkeit auf null gesetzt.

   Intern wird deshalb nach jedem update() die Methode updateMovements(float delta) aufgerufen. delta

ist hierbei die Zeit (in millisekunden) die seit dem letzten Update vergangen ist, bei 60 FPS sollte die

ungefähr 17 ms sein. Zur Sicherheit ist dieser Wert bei 100ms gedeckelt. Damit einzelne Lag Spikes nicht

dazu führen, dass innerhalb eines Frames Objekte eine sehr groSSe Distanz zurücklegen, dies würde auch

die Kollisionserkennung stark beeinträchtigen.

   Beweis Korrektheit

Die Bewegung ist im Framework wie folgt implementiert:

private void updateMovement(float delta) {
      for (Vector2 acc : accelerations) {
            speed.x += acc.x * delta;
            speed.y += acc.y * delta;
      }
      if (movePositionX(this.speed.x * delta)) {
            // Collision appeared
            speed.x = 0;
      }
      if (movePositionY(this.speed.y * delta)) {
            // Collision appeared
            speed.y = 0;
      }
}

                                                  32
Das erste was wir uns klarmachen müssen ist, dass wir die Bewegungen beziehungsweise Beschleuni-

gungen in X und Y Richtung unabhängig betrachten. Somit können wir das Problem auf eindimensionale

herunterbrechen.

   Fassen wir zusammen was wir im obigen Code tun: Zuerst bilden wir die Summe aus allen Beschleu-

nigungen um eine Gesamtbeschleunigung zu erhalten Die Geschwindigkeit erhalten wir dann indem wir

die Beschleunigung mit delta multiplizieren und zur bisherigen Geschwindigkeit addieren. Genauso er-

halten wir die neue Position in dem wir zur alten Position die aktuelle Geschwindigkeit multipliziert mit

delta addieren

                                                 k
                                           a = ∑ ai
                                                i=1

                                          vn = vn−1 + an ∗ ∆t

                                          xn = xn−1 + vn ∗ ∆t

   Schauen wir uns im Vergleich dazu die echten physikalischen Zusammenhänge an. Der wesentliche

Unterschied ist, dass diese sich in einem kontinuierlichen System befinden und wir in unserer Simulation

nur diskrete Zeitabschnitte haben (von der Breite 16ms).

                                                 k
                                         a(t) = ∑ ai (t)
                                                i=1
                                                       Z t
                                         v(t) = v0 +           a(t) dt
                                                           0
                                                       Z t
                                         x(t) = x0 +           v(t) dt
                                                           0

   Die Berechnung von Geschwindigkeit v und Position x benötigt jeweils ein Integral über eine kon-

tinuierliche Funktion, da wir uns aber in einem Zeit diskreten Raum befinden könne wir hierfür das

Riemann-Integral einsetzen (4) .

                                                      33
Sie können auch lesen