Entwicklung eines 2D Tiled Map LibGDX Game Framework

Ähnliche Dokumente
Entwicklung eines 2D Tiled Map LibGDX Game Framework

Entwicklung eines 2D Tiled Map LibGDX Game Framework

Entwicklung eines 2D Tiled Map LibGDX Game Framework

Datensicherung. Beschreibung der Datensicherung

Diese Ansicht erhalten Sie nach der erfolgreichen Anmeldung bei Wordpress.

Eine eigene Seite auf Facebook-Fanseiten einbinden und mit einem Tab verbinden.

Er musste so eingerichtet werden, dass das D-Laufwerk auf das E-Laufwerk gespiegelt

OP-LOG

Qt-Projekte mit Visual Studio 2005

Universal Dashboard auf ewon Alarmübersicht auf ewon eigener HTML Seite.

Die Dateiablage Der Weg zur Dateiablage

DOKUMENTATION VOGELZUCHT 2015 PLUS

WOT Skinsetter. Nun, erstens, was brauchen Sie für dieses Tool zu arbeiten:

Es sollte die MS-DOS Eingabeaufforderung starten. Geben Sie nun den Befehl javac ein.

Hex Datei mit Atmel Studio 6 erstellen

Task: Nmap Skripte ausführen

! " # $ " % & Nicki Wruck worldwidewruck

Im Folgenden wird Ihnen an einem Beispiel erklärt, wie Sie Excel-Anlagen und Excel-Vorlagen erstellen können.

Kurzanleitung. MEYTON Aufbau einer Internetverbindung. 1 Von 11

Kleines Handbuch zur Fotogalerie der Pixel AG

Switching. Übung 7 Spanning Tree. 7.1 Szenario

1. Zuerst muss der Artikel angelegt werden, damit später die Produktvarianten hinzugefügt werden können.

Update und Konfiguraton mit dem ANTLOG Konfigurations-Assistenten

Abschluss Version 1.0

Anleitung über den Umgang mit Schildern

SFTP SCP - Synology Wiki

Punkt 1 bis 11: -Anmeldung bei Schlecker und 1-8 -Herunterladen der Software

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3

GITS Steckbriefe Tutorial

Gruppenrichtlinien und Softwareverteilung

4D Server v12 64-bit Version BETA VERSION

Wordpress: Blogbeiträge richtig löschen, archivieren und weiterleiten

Java Script für die Nutzung unseres Online-Bestellsystems

Installationsanleitungen

INSTALLATION VON INSTANTRAILS 1.7

Übung: Verwendung von Java-Threads

Tutorial -

Wo möchten Sie die MIZ-Dokumente (aufbereitete Medikamentenlisten) einsehen?

Daten Sichern mit dem QNAP NetBak Replicator 4.0

In 12 Schritten zum mobilen PC mit Paragon Drive Copy 11 und Microsoft Windows Virtual PC

Abamsoft Finos im Zusammenspiel mit shop to date von DATA BECKER

Informationen zur Verwendung von Visual Studio und cmake

Artikel Schnittstelle über CSV

mysql - Clients MySQL - Abfragen eine serverbasierenden Datenbank

Erstellen einer digitalen Signatur für Adobe-Formulare

FTP-Server einrichten mit automatischem Datenupload für

Benutzerhandbuch MedHQ-App

GeoPilot (Android) die App

Upgrade auf die Standalone Editionen von Acronis Backup & Recovery 10. Technische Informationen (White Paper)

Bilder zum Upload verkleinern

Nach der Anmeldung im Backend Bereich landen Sie im Kontrollzentrum, welches so aussieht:

Dokumentation IBIS Monitor

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang

1 topologisches Sortieren

Arbeiten mit UMLed und Delphi

Informationen zum neuen Studmail häufige Fragen

Der Kalender im ipad

Primzahlen und RSA-Verschlüsselung

Dokumentation von Ük Modul 302

Wichtige Hinweise zu den neuen Orientierungshilfen der Architekten-/Objektplanerverträge

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC

CMS.R. Bedienungsanleitung. Modul Cron. Copyright CMS.R Revision 1

SANDBOXIE konfigurieren

Patch Management mit

Installationsanleitung Maschinenkonfiguration und PP s. Release: VISI 21 Autor: Anja Gerlach Datum: 18. Dezember 2012 Update: 18.

plus Flickerfeld bewegt sich nicht

Programme im Griff Was bringt Ihnen dieses Kapitel?

Inhalt. 1 Einleitung AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER

Speichern. Speichern unter

Die Beschreibung bezieht sich auf die Version Dreamweaver 4.0. In der Version MX ist die Sitedefinition leicht geändert worden.

Workshop: Eigenes Image ohne VMware-Programme erstellen

Step by Step Webserver unter Windows Server von Christian Bartl

Anleitung zum Extranet-Portal des BBZ Solothurn-Grenchen

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Installation und Inbetriebnahme von Microsoft Visual C Express

Powermanager Server- Client- Installation

Ihre Interessentendatensätze bei inobroker. 1. Interessentendatensätze

Datenbank-Verschlüsselung mit DbDefence und Webanwendungen.

Sichern der persönlichen Daten auf einem Windows Computer

Wie halte ich Ordnung auf meiner Festplatte?

Nutzung der VDI Umgebung

OpenVPN unter Linux mit KVpnc Stand: 16. Mai 2013

Kurzanleitung zu. von Daniel Jettka

Mit jedem Client, der das Exchange Protokoll beherrscht (z.b. Mozilla Thunderbird mit Plug- In ExQulla, Apple Mail, Evolution,...)

Datensicherung. Mögliche Vorgehensweisen:

Eigenen Farbverlauf erstellen

Visualisierung auf Büro PC s mit dem ibricks Widget

Datensicherung und Wiederherstellung

Zwischenablage (Bilder, Texte,...)

FTP-Leitfaden RZ. Benutzerleitfaden

Netzwerk einrichten unter Windows

Informatik I Tutorial

TeamSpeak3 Einrichten

Anleitung mtan (SMS-Authentisierung) mit SSLVPN.TG.CH

Java: Vererbung. Teil 3: super()

Der einfache Weg zum CFX-Demokonto

.htaccess HOWTO. zum Schutz von Dateien und Verzeichnissen mittels Passwortabfrage

Um dies zu tun, öffnen Sie in den Systemeinstellungen das Kontrollfeld "Sharing". Auf dem Bildschirm sollte folgendes Fenster erscheinen:

Transkript:

DHBW KARLSRUHE TINF12B5 STUDIENARBEIT Entwicklung eines 2D Tiled Map LibGDX Game Framework Author: Armin Benz & Mike Schwörer Betreuer: Prof. PhD. Kay Berkling 6712294 & 8045949 3. Mai 2015

Inhaltsverzeichnis 1 Vorwort 1 2 Plattformunabhängigkeit 2 2.1 Cocos2D-x......................................... 3 2.2 MonoGame......................................... 4 2.3 Unity3D........................................... 4 2.4 LibGDX........................................... 5 2.5 Unsere Entscheidung.................................... 5 3 Umsetzung 7 3.1 Umgebung.......................................... 7 3.1.1 Git und Github................................... 7 3.1.2 Gradle........................................ 9 3.1.3 junit Tests..................................... 13 3.1.4 Continuous Integration............................... 16 3.1.5 Metrics....................................... 18 3.1.6 JavaDoc....................................... 20 3.2 Das Framework....................................... 23 3.2.1 Tiled Map...................................... 23 3.2.2 Entity System.................................... 33 3.2.3 Layer System.................................... 42 3.2.4 Kollisionserkennung................................ 44 3.2.5 Settings....................................... 65 3.2.6 Debugging..................................... 67 3.2.7 Hintergründe.................................... 69 3.2.8 Der Menü Layer.................................. 72 3.3 Performance Optimizations................................. 81 3.3.1 Entity Liste..................................... 82 3.3.2 Immutable Objects................................. 84 3.3.3 Der GarbageCollector............................... 85 4 Fazit 86 I

Listings 1 Eclipse Optionen in gradle setzen.............................. 10 2 Gradle in der Kommandozeile............................... 11 3 Eine Assert Methode für zwei Vektoren.......................... 14 4 TravisCI Konfiguration................................... 16 5 Gradle in der Kommandozeile............................... 20 6 TravisCI Konfiguration für das Erzeugen von JavaDoc.................. 21 7 Das Javadoc Publish Bashscript.............................. 21 8 Ermitteln der aktuell sichtbare Tiles............................ 30 9 Beipiel einer TMX Datei.................................. 31 10 Framerate unabhängiges bewegen eines Entities...................... 36 11 Die Klasse EntityCollisionGeometry............................ 44 12 Erkennen von Kollisionen (Kreis-Kreis).......................... 47 13 Erkennen von Kollisionen (Kreis-Dreieck)......................... 47 14 Erkennen von Kollisionen (Box-Box)........................... 48 15 Erkennen von Kollisionen (Kreis-Box)........................... 49 16 Erkennen von Kollisionen (Box-Dreieck)......................... 50 17 Erkennen von Kollisionen (Dreieck-Dreieck)....................... 50 18 Berechnen der CollisionMap-Position aus der Tile-Position................ 52 19 Bewegen einer Geometrie in der CollisionMap...................... 53 20 Kommutative Eigenschaft von cancollidewith....................... 63 21 Eine Beispiel AGDXML Menü Definition......................... 79 II

Abbildungsverzeichnis 1 Die verschiedenen Layer eines absgdx Programmes................... 3 2 Git Network Graph mit smartgit.............................. 7 3 Screenshot SmartGit.................................... 8 4 Gradle Dependency Graph absgdx............................ 10 5 Beispiel der junit Test Anzeige............................... 13 6 Beispiel der Online Ansicht von TravisCI......................... 17 7 absgdx Metrics...................................... 18 8 absgdx Test Coverage................................... 19 9 Visualisierung einer Tiled Map............................... 23 10 Visualisierung des FixedMapScaleResolver........................ 25 11 Visualisierung des ShowCompleteMapScaleResolver................... 26 12 Visualisierung des MaximumBoundaryMapScaleResolver................ 27 13 Visualisierung des MinimumBoundaryMapScaleResolver................. 28 14 Visualisierung des LimitedMinimumBoundaryMapScaleResolver............ 29 15 Visualisierung des SectionMapScaleResolver....................... 30 16 Lebenszyklus eines einzelnen Entities........................... 33 17 Riemann-Integral über die Beschleunigung eiens Entities................. 38 18 Spezielle Entities...................................... 40 19 Der Layer Stack....................................... 42 20 Beispiel eines Entities mit mehreren Hitboxen....................... 46 21 3 Möglichkeiten einer Kreis-Dreieck Kollision....................... 47 22 Kollisionserkennung Kreis-Rechteck (1).......................... 49 23 Kollisionserkennung Kreis-Rechteck (2).......................... 49 24 Performance Graph der CollisionMap........................... 55 25 Berechnung des minimalen X-Abstands zweier Kreise.................. 57 26 Berechnung des minimalen X-Abstands eines Kreises und eines Rechtecks........ 58 27 Berechnung des minimalen X-Abstands zweier Rechtecke................ 58 28 Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (1)...... 59 29 Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (2)...... 60 30 Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (3)...... 60 31 Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (4)...... 61 32 Discrete-Time-Issue: Ein Objekt "phased" durch ein anderes............... 63 33 Die absgdx Einstellungen in Baumdarstellung...................... 65 34 Beispiel der Debugansicht (In-Game)........................... 67 III

35 Beispiel der Debugansicht (In-Menu)............................ 68 36 Eine Seitensansichts-Map mit Hintergrund......................... 69 37 Erklärung zu Hintergründen mit Bewegungsparallaxe................... 71 38 Klassendiagramm der Menü Elemente........................... 72 39 Die neun Texturen einer Menüfläche............................ 74 40 Der AGDXML Tag Tree.................................. 77 41 Screenshot des AGDXML Menudesigner......................... 80 42 Performancevergleich LinkedList vs. ArrayList...................... 83 43 Verlauf des RAMs unter Betrachtung des GC....................... 85 IV

Erklärung gemäß 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 große Anzahl von Spielen, vor allem sogenannte mobile Games teilen sich eine Reihe von Merkmalen. So ist ein Großteil 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 zwei dimensionale Draufsicht oder eine Seitenansicht. Oft wird hier eine 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 (siehe 2.5). 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 damit einfache 2D Spiele mit einer Tilemap und einem Entity-System entwickeln zu können. Unser Framework übernimmt die Verwaltung von Maps, Entities und optional Elementen wie Menü und Netzwerk. Durch LibGDX kann man das Projekt direkt am PC testen. 1

2 Plattformunabhängigkeit Ein entscheidender Vorteil, dass 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 wir einerseits Bytecode für die DalvikVM (Android) und die JVM (Windows) erzeugen müssen. Trotzdem 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 Smartphone auch noch andere wichtige Unterschiede. Vor allem die unterschiedliche Bildschirmgröße 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 speziell 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. Außerdem 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. 2

Abbildung 1: Die verschiedenen Layer eines absgdx Programmes 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 Plattformen 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 abstrahieren, 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 3

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. 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 Zielplattformen. Neben Windows, Android und ios kann man hier seine Programme aber auch auf der Playstation oder XBox ausführen. MonoGame sieht sich selbst als Nachfolger des jetzt nicht mehr weiterentwickelten 1 XNA Framework 4. Zu diesem Zweck wurde das Interface von Microsoft XNA 4 fast vollständig implementiert und in neueren Versionen auch erweitert. Ein großer 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ößte Framework, dass wir uns angeschaut haben. Es ist nicht nur eine Ansammlung von Bibliotheken sondern stellt auch eine Vielzahl an Werkzeugen und Editoren zur Verfügung. Der zentrale Code wird in C# geschrieben jedoch fließen 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. 1 http://microsoft-news.com/microsoft-confirms-end-of-xnadirectx-development/ 4

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. 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. Außerdem 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 Implementierungen 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. Außerdem ist es primär eine 3D Engine mit sehr vielen Features für den dreidimensionalen Raum. Hier eine 2D Engine aufzubauen würde primär bedeuten Features wegzuschneiden und deshalb haben wir uns gegen Unity3D als Basis für unser Framework entschieden. Sprache Erweiterbar Kostenlos Performance Auf PC testbar Dynamik Ausführung Cocos2D-x C++ O + + + 2D Compile MonoGame C# + - O + 3D JIT Unity3D C# - + O + 3D Compile LibGDX Java + + - + 3D JIT Tabelle 1: Vergleich verschiedener Spieleframeworks und Engines Zum Schluss blieb nur noch LibGDX übrig: Wir haben uns für LibGDX entschieden weil es all un- 5

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 plattformunabhä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 ebenfalls 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 Plattformunabhängigkeit nimmt. 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. 6

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. Außerdem 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 git 1. Mittels 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 pushen. 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/ Abbildung 2: Git Network Graph mit smartgit 7

Außerdem bietet git noch den Vorteil dass man Änderungen durch die einzelnen Commits zurückverfolgen 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 Repository 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.[1, S 5f] Zwar kann man git direkt über eine Kommandozeile bedienen, jedoch ist es oft einfacher und übersichtlicher 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. Abbildung 3: Screenshot SmartGit 8

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 Konfigurationsdateien 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 groß 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ßerdem Dateien, wie die IDE-Projektdateien, automatisch. 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, ausgehend vom Anfangsprojekt. Gradle lädt dann diese Bibliotheken, falls sie noch nicht auf dem Rechner vorhanden sind. Dies hat auch den Vorteil, falls eine Bibliothek öfters im Abhängigkeitsbaum auftaucht sie trotzdem nur einmal geladen werden muss.[2, S. 55] Gradle unterstützt viele verschiedene Repositories in welcher nach fehlenden Bibliotheken gesucht werden kann, standardmäßig wird jedoch die Maven Central Repository 2 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. 1 http://www.gradle.org/ 2 http://search.maven.org/ 9

Konfiguration Die Konfiguration geschieht über build.gradle Dateien welche in der Sprache Groovy geschrieben sind.[3][s. 73] absgdx ist ein Multiprojekt, dies bedeutet, dass es aus mehreren Gradle Projekten besteht mit jeweils eigenen Konfigurationen die voneinander abhängen. [2, 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. Abbildung 4: Gradle Dependency Graph absgdx Das Projekt absgdx-framework ist hierbei das eigentliche Framework das wir entwickeln. asgdxtest ist das Testprojekt, es enthält alle Unit-tests für das Framework. absgdx-core und die beiden Plattformprojekte 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 Dependency eingebunden. Während wir jedoch das Framework noch entwickeln sind diese Projekte notwendig damit wir es auch ausprobieren können. Da wir alle an dem Projekt mit der Eclipse IDE entwickeln haben wir ein paar extra Eclipse Einstellungen in die build.gradle Dateien ausgelagert: 10

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 ) 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 generierten 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 Repository 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. Damit 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- 11

sammenhängen zu löschen, dann werden sie mit eclipse neu aus den gradle Einstellungen erzeugt und zuletzt werden mit aftereclipseimport einige Änderungen vorgenommen. Der letzte Task ist von LibGDX vorgegeben damit das Android Projekt richtig konfiguriert ist. 12

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 5: Beispiel der junit Test Anzeige Das Ziel der Unittests ist es einen Großteil 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. [4, S 62] 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/ 13

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:<" + expected + "> but was:<" + actual + ">" ); } private static boolean fcomp(float expected, float actual, float delta) { return Float.compare(expected, actual) == 0 (Math.abs( expected - actual) <= delta); } Da wir hier Gleitkommazahlen vergleichen, welche eine endliche Genauigkeit haben, muss ein epsilon Wert eingeführt werden. Dieser ist die maximale Abweichung die zwei floats haben dürfen, um noch als gleich angesehen zu werden. Parametrisierte Tests Ein Feature von junit 4, dass wir benutzen sind die parametrisierten Tests 1. Hierbei führt man einen Test mehrmals unter verschiedenen Voraussetzungen aus. Wir benutzen dies intensiv bei den Tests der Kollisionserkennung. Jeder Kollisionstest wird mit mehreren verschiedenen Collisionmap-Größen ausgeführt. Einmal mit normal großer Map, einmal mit einer Map der Größe 1x1 und auch noch mit verschiedenen anderen Größen. Dies erhöht zwar die Anzahl der auszuführenden Tests stark, sorgt jedoch dafür, dass es wahrscheinlicher wird, Fehler zu erkennen und bei Unittests ist die Performance in soweit nicht sehr wichtig, da sie nur ab und zu auf dem Entwicklerrechner oder dem CI-Server ausgeführt werden. 1 https://github.com/junit-team/junit/wiki/parameterized-tests 14

Android Build Probleme Ein Problem, dass wir mit junit jedoch hatten war der Android Build Vorgang. Anfangs waren alle unsere Unittests in dem absgdx-framework Projekt. Deshalb war dieses Projekt von junit abhängig und junit ist seinerseits von der Hamcrest Bibliothek abhängig 1. 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 Hamcrest 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 ausschließen. 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 welches 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 eingebunden. 1 http://hamcrest.org/javahamcrest/ 15

3.1.4 Continuous Integration Zusammen mit den Unittests und Git wenden wir auch noch eine dritte Praktik an: Continuous Integration. Immer wenn jemand auf den main-branch unseres Repositories pusht wird automatisch ein Skript auf einem Buildserver ausgeführt. Dieses Skript clont das 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 1 welcher mit einer Textdatei ".travis.yml" im Repository root konfiguriert wird. language: java before_install: - chmod +x gradlew install:./gradlew desktop:assemble 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 sicherzustellen, 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 gepusht wurde, ohne den Code vorher getestet zu haben, dann schlägt das ganze Skript fehl und es wird eine Email an alle recipients geschickt. 1 https://travis-ci.org/mikescher/absgdx 16

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: Abbildung 6: Beispiel der Online Ansicht von TravisCI 17

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 7: absgdx Metrics Es zeigt eine Reihe an Statistiken über den Quellcode an und warnt wenn Teile des Codes komplex 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 18

Abbildung 8: 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. 19

3.1.6 JavaDoc Da unser Ziel es ist ein Framework zu entwickeln, dass auch von anderen Entwicklern genutzt wird dokumentieren wir alle öffentlichen Methoden. Mithilfe des Dokumentationswerkzeugs javadoc kann man aus diesen Quellcode Kommentaren HTML Dokumentationsseiten erzeugen. Im nachfolgenden möchte ich 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 vorhanden 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üßig 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 automatisiert 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" Konfigurationsdatei 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 20

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: - 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 Dokumentation 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ßerdem muss es ein Commit auf dem Branch master sein und ein echter push (nicht nur ein Pull Request). Danach kopieren wir die generierten Dateien in das Verzeichnis "tmp_jd" und klonen mit dem imaginä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 neueren 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 21

cp -Rf $HOME/tmp_jd./javadoc fi git add -f. git status git commit -m "Lastest javadoc on successful travis build $TRAVIS_BUILD_NUMBER auto-pushed to gh-pages" git push -f origin gh-pages 22

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 große Anzahl an Texturen in einer einzigen Datei zusammengefasst sind. Abbildung 9: 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 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 23

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äßig für neue Maps eingesetzt StaticTile StaticTiles haben eine statische Textur und keine Logik, rein-grafische Tiles könne hiervon 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öße Intern besitzen alle Tiles die Dimension 1.0 x 1.0. Beim eigentlichen Anzeigen müssen diese Werte jedoch 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. Außerdem kann es anwendungsspezifische 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äßig sind in absgdx sechs verschiedene MapScaleResolver enthalten: FixedMapScaleResolver Dies ist der einfachste MapScaleResolver, er gibt immer eine konstante, vorher bestimmte Größe zurück. (zum Beispiel 60px) 24

Abbildung 10: 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. 25

Abbildung 11: 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 garantiert, dass die Grenzfläche selbst immer zu sehen ist. 26

Abbildung 12: Visualisierung des MaximumBoundaryMapScaleResolver MinimumBoundaryMapScaleResolver Dies ist das Gegenstück zum MaximumBoundaryMapScaleResolver. Hier wird eine Grenzfläche festgelegt 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. 27

Abbildung 13: 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. 28

Abbildung 14: 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öße 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 außer Kraft setzen. 29

Abbildung 15: 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 groß genug macht um mehr als das gesamte Display auszufü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:[5, S 232f.] public Rectangle getvisiblemapbox() { float tilesize = mapscaleresolver.gettilesize(owner. getscreenwidth(), owner.getscreenheight(), map.height, map. width); 30

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 : <?xml version="1.0" encoding="utf-8"?> <map version="1.0" orientation="orthogonal" width="128" height="128" tilewidth="16" tileheight="16"> <layer name="kachelebene 1" width="128" height="128"> <data encoding="base64" compression="gzip"> H4sIAAAAAAAAC72d2Z5r13HeD/DtFs1zlMFJbjLcA== </data> </layer> </map> TMX unterstützt verschiedene Karten und Tilegrößen, mehrere Layer und auch fest eingebundene 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 vorliegen 3. 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 Auswerten der Datei sehr einfach, verbraucht aber mehr Arbeitsspeicher als beispielsweise ein SAX-Parser. Da die TMX Dateien jedoch nicht sehr groß sind im Verhältnis zum gewöhnlich verfügbaren Arbeitsspeicher 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 31

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 Reflection 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<String, String> in welcher verschiedene Informationen über das Tile enthalten sind wie Position, GID, Mapgröße 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 AutoTile 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 verliert zwar somit die Möglichkeit unterschiedlichen Tiles speziellen Code zuzuordnen, braucht jedoch bei einem großen 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. 32

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 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 16: 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 so- 33

mit ein einfaches Rechteck. Außerdem verwalten sie ihre aktuelle Geschwindigkeit und Beschleunigung um ein einfaches Interface zur flüssigen Bewegung zu bieten. Zuletzt kenne sie auch noch die Textur, oder Texturen, welche sie zeichnen sollen. 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. Außerdem stehen Optionen zur Verfügung wie Drehung oder Verzerrung der Textur. Auch muss die gezeichnete Textur nicht mit der Position oder den Ausmaßen 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ößeren 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 technischen 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 Nachteil 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 Ausmaßen wie das Entity addfullcollisioncircle(): Fügt ein Kollisions Kreis hinzu, welcher das Entity komplett ausfüllt 34

addfullcollisiontriangle(aligncorner4 align): Fügt ein Kollisions Dreieck hinzu, welches drei Ecken des Entities benutzt (Abhängig von dem übergebenen Alignment). 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 benachrichtigt. 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 CollisionGeometryOwner. Ü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 ausgelö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 onpassivemovementcollide aufgerufen. Wurde vorher festgelegt, dass die beiden Entities sich nicht überschneiden dann entspricht dies einem Zusammenstoß, 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 onactivecollide oder onpassivcollide aufgerufen. Framerate unabhängige Bewegung Eine der grundsätzlichen Aufgaben eines Entities ist es sich zu bewegen. Als Benutzer dieses Frameworks 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 35

bewegen. Andernfalls wäre das Spiel nur auf einem Rechner mit gleichen oder ähnlichen UPS Updates per second wie denen des Entwicklerrechners spielbar. Außerdem erschwert dies das Programmieren der allgemeinen Spiellogik. In den meisten Fällen denkt man über Objekte nicht in der Form von "An 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<accelerations>. Als Programmierer kann man nun entweder den Vektor Geschwindigkeit (speed) beeinflussen 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 übernommen 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 große 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; 36

} } Das erste was wir uns klarmachen müssen ist, dass wir die Bewegungen beziehungsweise Beschleunigungen 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 Beschleunigungen um eine Gesamtbeschleunigung zu erhalten Die Geschwindigkeit erhalten wir dann indem wir die Beschleunigung mit delta multiplizieren und zur bisherigen Geschwindigkeit addieren. Genauso erhalten wir die neue Position in dem wir zur alten Position die aktuelle Geschwindigkeit multipliziert mit delta addieren a = k a i i=1 v n = v n 1 + a n t x n = x n 1 + v n 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). a(t) = k i=1 v(t) = v 0 + x(t) = x 0 + a i (t) t 0 t 0 a(t) dt v(t) dt Die Berechnung von Geschwindigkeit v und Position x benötigt jeweils ein Integral über eine kontinuierliche Funktion, da wir uns aber in einem Zeit diskreten Raum befinden könne wir hierfür das Riemann-Integral einsetzen[6]. 37

Abbildung 17: Riemann-Integral über die Beschleunigung eiens Entities Dies berechnet ein Integral indem es in Abschnitte unterteilt wird und für diese Abschnitte Rechtecke bildet. Da wir in unserem Fall im Zeitpunkt t immer die Werte im Zeitraum [t t,t) berechnen brauchen wir das Obersummen Riemann Integral. a(t) = k i=1 v(t) = v 0 + x(t) = x 0 + a i (t) t 0 t 0 a(t) dt v(t) dt Aktuell sind dies jedoch noch zeit kontinuierliche Gleichungen, wir wollen diese in zeit diskrete Sequenzen mit Schrittgröße t umwandeln. Aus der Variable t wird dabei der Schrittzähler n. Aus dem Riemann Integral wird die Summe über alle bisherigen Rechtecke, also jeweils Wert an der Position i mal Breite des i-ten Schrittes. 38

a n = k a n,i i=1 n v n = v 0 + x n = x 0 + i=1 n i=1 a i t i v i t i Als letzten Schritt können wir eine Vereinfachung vornehmen. (1) können wir auch als (2) schreiben indem wir das Riemann Integral aufteilen. Und der vordere Teil von (2) ist äquivalent zu v n 1 womit man (1) auch als (3) schreiben kann. v n = v 0 + n i=1 a i t i (1) n 1 v n = v 0 + a i t i + a n t (2) i=1 v n = v n 1 + a n t (3) Daraus ergeben sich dann also die Formeln a n = k a i i=1 v n = v n 1 + a n t x n = x n 1 + v n t Welches wieder unserem ursprünglichen Algorithmus entspricht. Wir haben also bewiesen, dass unabhängig von der Update rate unser Algorithmus im Zeit diskreten Raum für t 0 korrekt ist. Zwar ist t in unserem Fall größer null, jedoch stimmt unsere Simulation annähernd immer noch mit der Realität überein, und mehr wollten wir ja auch nicht erreichen. Es ist noch festzuhalten, dass damit auch die Einheiten der einzelnen Attribute klar ist. Die Position wird in Tiles festgehalten und die Geschwindigkeit somit in Tiles s und die Beschleunigung entsprechend 39

in Tiles s 2. Spezielle Entities Wie schon zuvor gesagt müssen alle Entities von der Stammklasse Entity abgeleitet werden. Zur Vereinfachung gebräuchlicher Fälle gibt es schon vorbereitet abstrakte Kindklassen, von welchen man wieder ableiten kann. Abbildung 18: Spezielle Entities SimpleEntity Leitet man von der Klasse Entity muss man eine Vielzahl an Methoden zwangsweise Überschreiben (da diese als abstract markiert sind). Möchte man jedoch ein simples, statisches, rein visuelles Entity implementieren kommt man in die Situation alle oder viele überschriebene Methoden einfach leer zu lassen. Stattdessen kann man von SimpleEntity ableiten, diese Klasse hat alle sinnvollen Methoden schon überschrieben und Handler/Listener leer gelassen beziehungsweise Standardwerte zurückgeben lassen. Möchte man trotzdem auf einige diese Events reagieren oder bei einigen Methoden andere Werte zurückgeben kann man diese immer noch überschreiben, standardmäßig existiert jedoch kein Zwang diese zu 40

implementieren. Gerade deshalb sollte man dies jedoch nur machen wenn man schon Erfahrung mit dem Framework hat und mit Sicherheit weiß was es bedeutet auf keines der Events sinnvoll zu reagieren. PhysicsEntity Die PhysicsEntity implementiert zusätzlich einige physikalische Eigenschaften auf die Entities. Aktuell ist es jedoch nur die Schwerkraft. Diese wird als eigene Beschleunigung in die Liste der Beschleunigungen aufgenommen und erhält einen Wert entsprechend der gesetzten Masse. Setzt man die Masse des PhysicEntities auf null, entspricht es komplett einem normalen Entity, da die Gewichtskraft ein Nullvektor ist. 41

3.2.3 Layer System Unser Spiel kann sich während seiner Laufzeit in verschiedenen Phasen befinden. Darunter zum Beispiel eine Level laden Phase, eine Hauptmenü Phase oder eine Spiel Phase. Die Verwaltung dieser Phasen erfolgt über die sogenannten Layer. Alles in einem absgdx Programm muss sich innerhalb eines Layers befinden, standardmäßig gibt es den MenuLayer und den GameLayer es können jedoch auch eigene Layer von der Stammklasse AgdxLayer abgeleitet werden. Zu jedem Zeitpunkt ist nur ein Layer aktiv, dieser bekommt sämtliche Eingabe Events mit und dessen update() und render() Methoden werden aufgerufen. Organisiert werden die verschiedenen Layer in einem Stack. Abbildung 19: Der Layer Stack Diese Organisation hat vor allem beim Menübaum Vorteile: Geht man - wie im obigen Beispiel - vom Hauptmenü in das Levelmenü ist das Hauptmenü immer noch auf dem Stack, jedoch nicht mehr TOS (Top of Stack) und wird somit nicht gerendert. Verlässt man nun aber das Levelmenü wieder, beispielsweise über den Zurück Knopf muss nur der oberste MenuLayer vom Stack entfernt werden und man ist 42

wieder zurück auf dem Hauptmenü. Auch wenn man im oberen Beispiel das Spiel verlässt landet man automatisch wieder zurück im Levelmenü um von dort aus entweder ins Hauptmenü zu gehen oder ein neues Level zu starten. Für diese Operationen gibt es in der Klasse AgdxGame die Methoden pushlayer() und poplayer(). Falls diese ganze Funktionalität nicht erwünscht ist, kann auch einfach die Methode setlayer() benutzt werden. Diese leert den kompletten Stack und setzt den neuen Layer als einziges Element auf ihn. Verwendet man konsistent nur die Methode setlayer() hat man keinerlei Funktionalitäten des Stacks und kann arbeiten als gäbe es eine einzige Variable welche festlegt welcher Layer aktuell benutzt wird. Der GameLayer Der wohl wichtigste Layer ist der GameLayer. In ihm spielt sich das eigentliche Spiel ab. Um ein eigenes Spiel zu entwickeln muss man zuerst eine Klasse von GameLayer ableiten und auf den Layer Stack pushen. Der GameLayer sorgt für die Verwaltung der Entities, der Karte und auch der Kollisionsgeometrien auf welche in einem späteren Kapitel genauer eingegangen wird. Der Menü Layer Der MenuLayer stellt eine einzelne Menüseite dar. Er verwaltet seine einzelnen Komponenten und dessen Events. In einem späteren Kapitel wird genauer darauf eingegangen wie man ihn benutzt. 43

3.2.4 Kollisionserkennung Eine wichtige Funktionalität, die unser Framework übernehmen soll ist es Kollisionen zwischen Entities zu erkennen. Beispielsweise muss ein Spiel erkennen ob das Spieler-Entity gerade mit einem Gegner kollidiert oder ob die Kugel-Entities etwas treffen. Eine solche Kollisionserkennung kann man unterschiedlich komplex implementieren, da unser Framework als primäre Zielplattform jedoch mobile Geräte hat ist hierbei die Performance besonders wichtig. Das Ziel ist es nun ein Algorithmus zu entwickeln, welcher folgende Eigenschaften erfüllt:[7, S 73] Der Algorithmus sollte auch bei vielen Entities auf der Karte noch schnell sein und die Update-Time nicht unnötig erhöhen [7, S 14f] Der Algorithmus sollte genau sein und weder False-Positives noch False-Negatives haben. Zwar kann es Anwendungen geben, wo eine genaue Erkennung nicht immer nötig ist, doch da unser Framework für eine Vielzahl an Anwendungen funktionieren soll muss der Algorithmus exakt sein. Es sollte möglich sein Entities ohne Kollisionserkennung hinzuzufügen ohne dass diese Einfluss auf die Update-Time haben. Die CollisionGeometry Klasse Wir haben uns in unserem Ansatz dagegen entschieden die Kollision zwischen Entities direkt zu berechnen. Dies würde zwar die Handhabung des Frameworks vereinfachen würde aber auch zu einigen Problemen führen: Einerseits könnte es keine rein-visuellen Entities geben welche mit nichts kollidieren und keinen Einfluss auf die Performance haben. Außerdem kann es auf der anderen Seite auch keine Entities mit mehreren Bereichen geben welche jeweils einzeln kollidieren können. Stattdessen gibt es neben den Entities auf der Karte auch noch sogenannte CollisionGeometries welches die Elemente sind die miteinander kollidieren. Ein Entity kann nun ein- / kein-, oder mehrere CollisionGeometries haben und auf deren Kollisionen reagieren. Beispielsweise kann der Kopf einer Figur eine andere Hitbox als der Körper haben und anders auf eintreffende Kugeln reagieren. Jede Entity hat eine Liste mit von EntityCollisionGeometry. Dies ist ein einfacher Wrapper über die Klasse CollisionGeometry, die die Geometrie zusammen mit der relativen Position zum Entity enthält. 44

public class EntityCollisionGeometry { public final Vector2 relativeposition; public final CollisionGeometry geometry; public EntityCollisionGeometry(Vector2 relativepos, CollisionGeometry geo) { super(); } this.relativeposition = relativepos; this.geometry = geo; } public void updateposition(float x, float y) { geometry.setcenter(x + relativeposition.x, y + relativeposition.y); } Jedes mal wenn das Entity eine Veränderung seiner Position erfährt wird updateposition() aufgerufen und auch die CollisionGeometry bewegt sich mit. Mit dieser Technik kann man auch komplexere Geometrie Gebilde bauen welche die Form des Entities beliebig genau annähern kann. [5, S 500] 45

Abbildung 20: Beispiel eines Entities mit mehreren Hitboxen Aktuell gibt es drei verschiedene Arten von CollisionGeometry: CollisionCircle, CollisionBox und CollisionTriangle. In den meisten Fällen möchte man Hitboxen in der Form von Rechtecken, es kann jedoch überlegenswert sein nur Kreise zu verwenden da bei diesen die Kollisionsberechnung schneller ist. Oder eben Dreiecke da man aus diesen beliebige Polynome zusammenbauen kann. Dreiecke sollten jedoch nicht im Übermaß verwendet werden da deren Kollisionen am aufwendigsten zu berechnen sind. CollisionCircle Der Kreis ist die einfachste CollisionGeometry, und bei Programmen mit vielen Entities sollte darüber nachgedacht werden nur Kreisgeometrien zu verwenden. Für den Fall Kollision Kreis-Kreis ist die Formel um zu berechnen ob sie sich schneiden sehr einfach: Abstand < (Radius 1 + Radius 2 ) was in unserem 46

Fall zu sqrt((x 1 x 2 ) 2 + (y 1 y 2 ) 2 ) < (r 1 + r 2 ) wird. Dies kann noch weiter optimiert werden indem man beide Seiten quadriert: x 1 x 2 ) 2 + (y 1 y 2 ) 2 < (r 1 + r 2 ) 2 [5, S 499]. Dies liegt daran, dass eine Multiplikation um mehrere Größenordnungen schneller als eine Quadratwurzel ist, ersteres ist meist ein einzelner Maschinenbefehl. In unserem Projekt wird mit folgender Methode überprüft ob sich zwei Kreise schneiden: public boolean dogeometriesintersect(collcircle a, CollCircle b) { return fsquare(a.centerx-b.centerx) + fsquare(a.centery-b. centery) < fsquare(a.radius + b.radius); } Den Vorteil einer Kreis-Kreis Kollision kann man auch auf alle anderen Geometrien anwenden indem man alle Geometrien als umschließende Kreise abschätzt und diese zuerst kollidieren lässt. Nur wenn die umschließenden Kreise sich überschneiden muss man dann den komplizierteren Schritt machen und die genauen Geometrien kollidieren lassen. Außer der Kreis-Kreis Kollision gibt es auch noch das Problem zu erkennen ob sich eine Kreisgeometrie und einen Rechtecksgeometrie schneiden. Den Algorithmus um diese Kollision zu erkennen ist im nachfolgenden Paragraph "CollisionBox" beschrieben. Der dritte Fall ist die Kollision des Kreises mit einem Dreieck. Hier muss man 3 Fälle beachten. Entweder der Kreis schneidet einer der Ecken, eine der Kanten oder der Kreis ist komplett innerhalb des Dreieckes. Wieder verwenden wir hier den Trick den Abstand in quadriertem Zustand zu vergleichen da dies 2 Aufrufe von sqrt spart. Abbildung 21: 3 Möglichkeiten einer Kreis-Dreieck Kollision 47

public boolean dogeometriesintersect(collisiontriangle a, CollisionCircle b) { return b.containspoint(a.p1_x, a.p1_y) b.containspoint(a.p2_x, a.p2_y) b.containspoint(a.p3_x, a.p3_y) getlinepointdistancesquared(b.centerx, b.centery, a.p1_x, a.p1_y, a.p2_x, a.p2_y) <= (b.radius*b.radius) getlinepointdistancesquared(b.centerx, b.centery, a.p2_x, a.p2_y, a.p3_x, a.p3_y) <= (b.radius*b.radius) getlinepointdistancesquared(b.centerx, b.centery, a.p3_x, a.p3_y, a.p1_x, a.p1_y) <= (b.radius*b.radius) } a.containspoint(b.centerx, b.centery); CollisionBox Unsere zweite Standard Geometrie ist das Rechteck. Es ist zu beachten, dass dies kein frei drehbares Rechteck ist sondern eines, dass an den Koordinatenachsen ausgerichtet ist. Deshalb wird es auch nur von width, height und center definiert. Diese Geometrie wird wohl am meisten verwendet da einerseits Kollisionen zwischen zwei Rechtecken sehr schnell zu berechnen sind und andererseits es sich oft natürlich anfühlt Objekte in ein Rechteck zusammenzufassen. Um zu erkennen ob zwei Rechtecke sich überschneiden dreht man die Frage zuerst um. Zwei Rechtecke überschneiden sich nicht wenn die Rechtecke übereinander/untereinander oder nebeneinander liegen. Negiert man dies bekommt man ganz einfach heraus ob sich zwei Rechtecke schneiden: public boolean isintersectingwith(collisionbox other) { return! (this.rightx < other.x other.rightx < this.x this.topy < other.y other.topy < this.y); } Falls man auf Performance wert legt sollte man Rechtecke nicht (zu oft) mit anderen Geometrien kollidieren lassen. Beispielsweise ist die Kollisionserkennung mit einem Kreis schon komplizierter. Zuerst muss man bestimmen ob der Kreis neben, über oder unter dem Rechteck liegt, falls ja bestimmt man den Abstand der jeweiligen Seite und des Kreismittelpunktes. Wenn dieser Abstand kleiner als der Kreisradius ist schneiden sich die beiden Geometrien. 48

Abbildung 22: Kollisionserkennung Kreis-Rechteck (1) Wenn der Kreis andererseits schräg im Verhältnis zum Rechteck liegt muss man schauen ob das jeweilige Eck innerhalb des Kreises liegt, hierfür bestimmt man den Abstand Eck-Kreismittelpunkt und überprüft ob dieser kleiner dem Kreisradius ist. Abbildung 23: Kollisionserkennung Kreis-Rechteck (2) public boolean dogeometriesintersect(collisioncircle a, CollisionBox b) { if (a.centery > b.y && a.centery < b.topy) return a.centerx > (b.x - a.radius) && a.centerx < (b. rightx + a.radius); else if (a.centerx > b.x && a.centerx < b.rightx) return a.centery > (b.y - a.radius) && a.centery < (b. topy + a.radius); else return fsquare(a.centerx-b.x) + fsquare(a.centery-b.y) < fsquare(a.radius) fsquare(a.centerx-b.rightx) + fsquare(a.centery-b.y ) < fsquare(a.radius) fsquare(a.centerx-b.rightx) + fsquare(a.centery-b. topy) < fsquare(a.radius) fsquare(a.centerx-b.x) + fsquare(a.centery-b.topy) < fsquare(a.radius); 49

} Als letztes müssen wir wiederum auch die Kollision mit einem Dreieck erkennen. Hierbei nutzen wir aus das sich in jedem Fall mindestens zwei Seiten der zwei Geometrien überschneiden müssen. Dies gilt aber nicht in den Spezialfällen wo das Rechteck komplett innerhalb des Dreiecks ist, oder umgekehrt. Diese beiden Möglichkeiten müssen als Sonderfälle ebenfalls überprüft werden. public boolean dogeometriesintersect(collisiontriangle a, CollisionBox b) {od return dolinesintersect(a.p1_x, a.p1_y,a.p2_x, a.p2_y, b.x, b.y, b.x, b.topy) /*... */ dolinesintersect(a.p3_x, a.p3_y,a.p1_x, a.p1_y, rightx, b.y, b.x, b.y) b.containspoint(a.p1_x, a.p1_y) a.containspoint(b.x, b.y); } CollisionTriangle Kollisionsgeometrien in der Form von Dreiecken sind unsere letzte Möglichkeit. Es ist zu Beachten, dass diese auch am aufwendigsten zu berechnen sind. Trotzdem kann man mit ihrer Hilfe nahezu beliebig komplexe Strukturen darstellen. Genauer gesagt ist es mit Polynom Triangulation möglich jedes Polygon in Dreiecke zu zerlegen. [7, S 32] Den Fall Kollision mit einem Kreis beziehungsweise mit einem Rechteck haben wir in den vorherigen Kapiteln schon besprochen. Das einzige was noch übrig bleibt ist die Kollision mit einem anderen Dreieck. Hierbei ist die Vorgehensweise jedoch die Gleiche wie bei der Dreieck-Rechteck Kollision. Wir suchen nach Kanten die sich überschneiden und beachten die beiden Sonderfälle in denen eine Geometrie komplett innerhalb der anderen liegt. public boolean isintersectingwith(collisiontriangle other) { return ShapeMath.doLinesIntersect(p1_x, p1_y, p2_x, p2_y, other. p1_x, other.p1_y, other.p2_x, other.p2_y) /*... */ ShapeMath.doLinesIntersect(p3_x, p3_y, p1_x, p1_y, other. p3_x, other.p3_y, other.p1_x, other.p1_y) containspoint(other.p1_x, other.p1_y) 50

} other.containspoint(p1_x, p1_y); Optimierung mit der Collisionmap Das grundlegende Problem, dass dieser Algorithmus bis jetzt noch hat ist dass mit steigender Geometrie Anzahl die Anzahl der Vergleiche quadratisch ansteigt. Es müssen aktuell nämlich jede CollisionGeometry mit jeder anderen getestet werden und überprüft werden ob sie sich überschneiden. Dies hat den Aufwand O(n 2 ). Die meisten dieser Operationen sind offensichtlich unnötig - wenn sich zwei Geometrien auf unterschiedlichen Seiten der Karte befinden können sie in keinem Fall miteinander kollidieren (den Fall von Entities die fast so groß wie die Karte sind ausgeschlossen). Unsere Lösung liegt darin die Datenstruktur zu ändern. Bisher wurden die CollisionGeometrien in einer einzigen großen Liste gespeichert, jetzt speichern wir sie in einem zweidimensionalen Raster. Man muss sich das so vorstellen, dass man die Karte in ein Raster unterteilt ähnlich dem Vorgehen bei der TileMap. Jedes dieser Raster-Felder wird von einem Objekt der Klasse CollisionMapTile dargestellt und jedes Objekt enthält eine Liste der CollisionGeometrien die auf diesem Tile sind. Das bedeutet, dass jedes mal wenn eine CollisionGeometry hinzugefügt wird oder sich bewegt, die Tiles berechnet werden auf denen die Geometrie sich befindet. Die CollisionGeometry wird dann in die Listen aller dieser Tiles hinzugefügt. Möchte man nun wissen ob eine Geometrie mit einer anderen kollidiert geht man alle Tiles durch auf denen diese Geometrie liegt. Den eigentlichen Überschneidung-Test-Code führt man dann auf allen Geometrien aus die man in diesem Tiles findet. Die vorherigen Optimierungen, wie dass man zuerst eine Kreiskollision durchführt, bleiben immer noch intakt. Mit dieser Methode haben wir die Anzahl der zu überprüfenden Geometrien von allen enthaltenen auf ein paar in der Nähe eingeschränkt. Diese CollisionMap muss jedoch immer aktuell gehalten werden, jedes mal wenn eine Geometrie hinzugefügt/bewegt/entfernt wird muss die jeweilige Methode aufgerufen werden um die CollisionMap zu aktualisieren. Dies ist auch der technische Grund warum sowohl bei Entity als auch bei CollisionGeometry der position-vektor nicht öffentlich ist sondern nur durch Setter verändert werden kann. Denn dadurch kann man diese Events abfangen und an die CollisionMap weitergeben. Eine Frage ist nun wie die CollisionMapTiles skaliert werden sollen. Der Standard Ansatz unseres Frameworks ist es die CollisionMapTiles gleich groß wie die normalen Map Tiles zu haben. Dies ist 51

als Standard Ansatz in soweit gut, dass normalerweise eine Map weder nur ein paar Tiles noch eine extreme Anzahl Tiles hat und meist sind maximal eine Hand voll Entities gleichzeitig auf einer Tile. Trotzdem bieten wir dem Benutzer die Möglichkeit die Tilegröße manuell anzupassen. Dafür gibt es im CollisionMap-Konstruktor den Parameter exptilescale. Dieser ist das Verhältnis von MapTiles zu CollisionTiles in der Form 2 n. Der Wert 0 ist hierbei Standard und beschreibt den Fall MapTiles == CollisionTiles. Ein Wert von 1 würde CollisionTiles bewirken die doppelt so groß sind wie die MapTiles (2 1 = 2) und ein Wert von -1 halb so große (2 1 = 1 2 ) Um dies nun implementieren zu können muss man für eine Geometrie berechnen können auf welchen Tiles sie sich befindet. Zuerst muss man die Position auf den Map Tiles in eine Position auf den CollisionTiles umwandeln: private int gettilex(float x) { if (exptilescale < 0) { return (int) (x * 1d * (1 << -exptilescale)); } else if (exptilescale == 0) { return (int) x; } else { return (int) (x * 1d / (1 << exptilescale)); } } private int gettiley(float y) { if (exptilescale < 0) { return (int) (y * 1d * (1 << -exptilescale)); } else if (exptilescale == 0) { return (int) y; } else { return (int) (y * 1d / (1 << exptilescale)); } } Jedoch muss man hier auch den Ausnahmefall beachten. Eine Geometrie muss nicht zwangsweise auf der Karte sein, theoretisch kann sie sich auch daneben befinden. Für diesen Fall haben wir einen zweiten 3x3 CollisionMapTile-Array welcher die 8 Bereiche um die Karte herum bezeichnet (Oben, Oben-Rechts, Rechts, Unten-Rechts, Unten, Unten-Link, Links, Oben-Links). Zwar hat man für den Fall von Kollisionen außerhalb der Karte keinen Vorteil durch die CollisionMap, sondern effektiv wieder den gleichen Fall wie vor deren Einführung. Aber dafür sind auch dort Kollisionserkennung möglich. Neben der Tile-Position muss nun auch der Radius berechnet werden, alle CollisionTiles innerhalb 52

dieses Radius werden dann mit der entsprechenden Geometrie gefüllt. Dieser Collision-Radius berechnet sich aus dem umschließenden Radius der Geometrie mit der Formel ceil(r geometry 2 exptilescale ). Dieser Radius ist immer mindestens 1, was dazu führt, dass jede Geometrie immer mindestens auf 9 Tiles liegt. In den meisten Fällen liegt die Geometrie nicht wirklich auf allen diesen Tiles. Dies ist jedoch kein Problem da die endgültige Kollisionserkennung immer noch durch die genauen Formeln berechnet wird. Es ist nur wichtig, dass es keine Tiles gibt auf denen die Geometrie liegt, die aber nicht diese enthalten. Eine weitere Optimierung kann man beim Bewegen von Geometrien implementieren. Normalerweise werden, wenn eine Geometrie bewegt werden bei allen Tiles auf denen sie vorher gelegen ist die Einträge entfernt und bei allen neuen die Einträge hinzugefügt. Falls die Geometrie jedoch auf der gleichen CollisionMapTile bleibt muss die Map nicht aktualisiert werden. Deshalb wird in der Methode move- Geometry() zuerst überprüft ob eine Änderung passiert ist und nur dann die Map aktualisiert: public boolean movegeometry(float prevcenterx, float prevcentery, CollisionGeometry geo) { if (gettilex(prevcenterx) == gettilex(geo.getcenterx()) && gettiley(prevcentery) == gettiley(geo.getcentery())) return true; boolean success = removegeometry(geo, prevcenterx, prevcentery) ; addgeometry(geo); } return success; Eine weitere Möglichkeit die Kollisionserkennung zu optimieren ist durch Einführung von Pseudo- Geometrien. Möchte eine Geometrie nur selbst mit anderen kollidieren aber nicht als Kollisionsobjekt dienen, muss es sich nicht in die CollisionMap eintragen. Ein Beispiel hierfür wäre eine Kugel-Entity. Diese besitzt eine CollisionGeometry um zu erkennen wann sie etwas getroffen hat. Jedoch gibt es normalerweise kein Fall in dem eine andere Entity wissen müsste ob die mit einer Kugel kollidiert. Deshalb kann man einen Performancevorteil erlangen indem man die Kugel-Geometrie nicht in die CollisionMap einträgt und stattdessen selber verwaltet. Es gilt zu beachten dass man dies nicht mit zu vielen Entity- Typen machen sollten um nicht den Überblick zu verlieren wer mit wem kollidieren kann. Außerdem muss die Kugel in jedem Schritt selbst ihre Kollisionen überprüfen, da es sonst keiner für sie tut. 53

Verwendung mit Entities In den meisten Fällen werden CollisionGeometries nicht alleinstehend benutzt sondern in Zusammenhang mit einer Entity. Aus diesem Grund sind die beiden Klassen auch eng miteinander verknüpft. So kann man einer Entity eine Liste an Geometrien hinzufügen, die diese Geometrie verwaltet. Wenn sich das Entity bewegt werden auch die Geometrien bewegt. Hierfür wird den Geometrien eine relative Position im Verhältnis zum Entity gegeben. Außerdem wird jedes mal wenn die Entity sich bewegt, auf Kollisionen überprüft. Falls eine Kollision geschieht wird auf der bewegten Entity die Methode onactivecollide() aufgerufen und auf der anderen Entity onpassivecollide(). Diese beiden Methoden werden im Interface CollisionListener implementiert - welches die Klasse Entity standardmäßig hat. Außerdem wird, wenn man eine Geometrie zu einem Entity hinzufügt, dieses automatisch in den listener-array der Geometrie aufgenommen. Dies ist der Array mit allen Klassen die benachrichtigt werden wenn eine Kollision stattfindet. Analyse der Performance Um die Vorteile der einzelnen Optimierungen darzustellen, hier eine Tabelle mit Messwerten unter verschiedenen Bedingungen. In jedem Test wurden eine gewisse Anzahl an Entities mit jeweils einer CollisionGeometry erstellt und gleichmäßig auf einer 128x128 Karte verteilt. Jedes mal wird die Zeit gemessen die ein Update- Vorgang braucht, in dem für jede Geometrie berechnet wird mit welchen anderen Geometrien sie kollidiert. Falls die Zeit so groß war, dass wir kein Ergebnis bekamen, wurde NaN eingetragen Test 1: Rechteckige Hitboxen Test 2: Kreis Hitboxen (mit sqrt) Test 3: Kreis Hitboxen (ohne sqrt) Test 4: Rechteckige Hitboxen (mit vorheriger Kreis Abschätzung) Test 5: Rechteckige Hitboxen in einer vollständigen CollisionMap 54

Anzahl Geo s Test 1 Test 2 Test 3 Test 4 Test 5 128 1.03 ms 0.95 ms 0.76 ms 0.66 ms 0.28 ms 1024 65.42 ms 63.81 ms 55.32 ms 46.08 ms 1.10 ms 16384 22139.79 ms 22199.35 ms 18604.07 ms 20451.18 ms 58.59 ms 65536 NaN NaN NaN NaN 738.22 ms 262144 NaN NaN NaN NaN 11891.37 ms 16777216 NaN NaN NaN NaN 219178.66 ms Ein paar Dinge kann man direkt aus dieser Tabelle erkennen. Einerseits den offensichtlich extremen Vorteil der CollisionMap im Gegensatz zu allen anderen Ansätzen. Außerdem gibt es einen erkennbaren Performance Einbruch durch Verwendung von sqrt. Der Unterschied zwischen Kreis und Rechteck Kollision ist fast nicht da - dies ist in soweit erklärbar, dass beides mathematisch einfach zu berechnen ist. Abbildung 24: Performance Graph der CollisionMap 55

Bewegung mit Kollisionen Kollisionserkennung ist nicht nur gut um zu erkennen wann zwei Entities kollidieren, sondern auch um dies zu verhindern. Hat man beispielsweise eine vom Spieler gesteuerte Figur möchte man nicht, dass diese stirbt wenn sie in einer Wand endet, sondern dass sie dies überhaupt nicht kann. Dafür hat die Klasse Entity außer der setposition() Methode auch noch die moveposition()-methode. Diese bewegt das Entity entweder die komplette Strecke oder nur partiell falls zwischenzeitlich eine Kollision stattfindet. Um dies zu zu schaffen bewegt man das Entity und all seine Geometrien zuerst die komplette Strecke. Dann sucht man die erste fremde Geometrie mit denen eine eigene Geometrie kollidiert. Für diese Geometrie berechnet man dann die Entfernung die man rückwärts gehen muss, bis sich die beiden Geometrien nicht mehr überschneiden. Dies wiederholt man solange bis es für keine eigene Geometrie mehr eine Überschneidung gibt. Jedoch führt man dies nicht für die ganze Bewegung aus sondern einmal für die Bewegung in X-Richtung und dann noch einmal in Y-Richtung. X und Y Bewegung sind somit unabhängig und es ist möglich beispielsweise an einer Wand "entlang zu gleiten" indem die X-Bewegung verhindert wird aber die Y-Bewegung immer noch ausgeführt. Mathematisch interessant ist hierbei die Algorithmen um den minimalen Abstand zu berechnen. Wir brauchen eine Methode die zwei Geometrien, positioniert durch ihren Mittelpunkt, bekommt und uns den minimalen Abstand in X oder Y Richtung sagt bei denen dies sich gerade nicht schneiden. Dies führt zu unterschiedlichen Methoden für jede Art der Kollision: Kreis-Kreis Abstand Der Kreis-Kreis Abstand lässt sich einfach durch diese Formel berechnen: d x = (r 1 + r 2 ) 2 (y 2 y 1 ) signum(x 2 x 1 ) : 56

Abbildung 25: Berechnung des minimalen X-Abstands zweier Kreise Kreis-Rechteck Abstand Der Kreis-Rechteck Abstand muss wieder zwei Fälle abdecken. Wenn der Kreis neben dem Rechteck ist, ist die Formel:. d x = (r 1 + width 2 ) signum(x 2 x 1 ) 2 Befindet sich der Kreis über oder unter dem Rechteck ist die Formel die gleiche wie bei dem Kreis- Kreis Abstand: d x = (r 1 ) 2 (y 2bl y 1 ) signum(x 2 x 1 ) 57

Abbildung 26: Berechnung des minimalen X-Abstands eines Kreises und eines Rechtecks Der Rechteck-Kreis Fall ist der selbe wie Kreis-Rechteck nur mit getauschten Vorzeichen. Dies gilt auch später für alle symmetrischen Fälle wie Kreis-Dreieck oder Dreieck-Rechteck. Rechteck-Rechteck Abstand Beim Rechteck-Rechteck Fall bestimmt man den Abstand mit: d x = width 1 + width 2 2 Abbildung 27: Berechnung des minimalen X-Abstands zweier Rechtecke Es ist interessant, dass dies der einzige Fall bei dem der minimal X-Abstand unabhängig von der Y Position ist (solange es überhaupt eine Kollision gibt) 58

Dreieck-Kreis Abstand Den Abstand zwischen einem Dreieck und einem Kreis zu berechnen ist etwas komplizierter als die vorangegangenen Beispiele. Zur Vereinfachung brechen wir das Problem in drei Kreis-Strecke Abstände auf. Aus allen validen Abständen die wir so bekommen nehmen wir dann den, dessen absoluter Wert minimal ist. Hierbei ist es wichtig, dass man nur die validen Abstände betrachtet. Kollidiert der Kreis zum Beispiel niemals mit der dritten Kante, egal wie weit er sich auf der X-Achse bewegt, darf dieser Abstand auch nicht beachtet werden. Da wir bereits mit Gleitkommazahlen arbeiten, haben für den Keine-Kollision Fall den Wert NaN genommen. Abbildung 28: Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (1) Zuerst berechnen wird den Winkel αden die Gerade (definiert durch P1 und P2) mit der X-Achse hat. Dies geschieht über die Funktion atan2 aus der Math-Bibliothek von java. Der Winkel βist der Komplementwinkel zu α, es gilt also α + β = 90 59

Abbildung 29: Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (2) Als nächstes möchten wir den Punkt ausrechnen an dem der Kreis die Strecke berühren würde. An dieser Stelle liegt die Gerade tangential zum Kreis. Mittels des Winkels βkönnen wir also diesen Punkt ermitteln und auch seine Lage relativ zum Kreismittelpunkt (dcx und dcy). Zwar kann es immer zwei Punkte auf gegenüberliegenden Seiten des Kreises geben, dies kann jedoch unterschieden werden indem man sich anschaut ob der Kreis rechts oder links neben der Strecke liegt. Je nachdem muss dcx positiv oder negativ sein. Abbildung 30: Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (3) Da wir nun die Y-Position der Kollisionspunkte haben müssen wir noch den X-Anteil des Punktes auf der Strecke berechnen, dies ist in soweit kein Problem da wir ja dessen Y-Anteil schon kennen. In diesem Schritt können wir auch erkennen ob es überhaupt eine Kollision und somit einen minimalen X-Abstand gibt. Falls der berechnete Punkt auf der Strecke nicht zwischen P1 und P2 gibt es auch keine Kollision. 60

Abbildung 31: Berechnung des minimalen X-Abstands eines Kreises und eines Dreieckes (4) Als letzten Schritt muss man nur noch Pcc.x von Pcl.x abziehen. Dies ist jetzt der minimale Weg den der Kreis sich bewegen darf um nicht mit dem Dreieck zu kollidieren. Da wir aber bisher immer den minimalen Abstand gesucht haben muss dieser noch daraus berechnet werden indem man ihm vom absoluten Abstand der beiden Geometrien abzieht: minx = abs(g1.x g2.x) dx Dreieck-Dreieck Abstand Der Abstand Dreieck-Dreieck kann auf mehrere Punkt-Strecke Abstände vereinfacht werden. Denn bei jeder Kollision muss eine Ecke eines Dreiecks auf eine Kante des anderen treffen. Wieder benutzt man den Ansatz von Kreis-Dreieck und berechnet alle gültigen Abstände und nimmt dann denjenigen dessen absoluter Wert am kleinsten ist. Insgesamt sind das 18 Punkt-Strecke Berechnungen. Jeweils drei Ecken des einen Dreiecks mal 3 Kanten des anderen mal zwei Dreiecke. Zwar könnte man dies noch optimieren, jedoch ist die Punkt-Strecke Berechnung so schnell, dass wir eine Optimierung noch nicht für nötig befunden haben. Dreieck-Rechteck Abstand Der Dreieck-Rechteck Abstand ist effektiv genau das gleiche wie der Dreieck-Dreieck Abstand. Man kann ihn mit dem gleichen Ansatz berechnen, nur dass man anstatt achtzehn vierundzwanzig Punkt- Strecke Berechnungen durchführen muss. Dies liegt an der einen Kante und Ecke die das Rechteck mehr 61

als das Dreieck hat. Praktische Probleme Nun gibt es in der Klasse CollisionListener auch noch Events für solche verhinderten Kollisionen. Kollidiert eine Geometrie in ihrer eigenen Bewegung wird die Methode onactivemovementcollide() bei dem bewegten Entity aufgerufen und bei dem anderen onpassivemovementcollide. Ein Problem können hier Entities mit mehreren CollisonGeometries sein. Hierbei muss drauf geachtet werden, dass alle Geometrien in der Kollision beachtet werden und jeweils der größte "Rücksetz-Wert" benutzt wird. Ein weiteres Problem sind entstehende Rundungsfehler bei Verwendung von FloatingPoint Zahlen. Führt man den Algorithmus exakt so wie oben beschrieben aus kann es vorkommen, dass sich die beiden Geometrien auch nach dem Rücksetzen immer noch leicht schneiden. Der einzige Weg um dieses Problem herum ist es einen delta Wert in der Berechnung des minimalen Abstands einzuführen.[7, S 18] Damit wird nicht mehr der minimale Abstand zurückgeliefert sondern der minimale Abstand plus einen delta Wert. Somit können sich Entities nicht mehr "wirklich" berühren sondern nur noch bis maximal delta Einheiten nähern. Ein letztes Problem ist das sogenannte Discrete-Time-Issue [5, S 503]. Falls die Update rate niedrig genug ist kann es vorkommen, dass ein Objekt links neben einem anderen ist und sich dann schnell genug nach rechts bewegt um im nächsten Schritt rechts neben dem Objekt zu sein. Obwohl sich das Entity nun direkt durch ein anderes bewegt hat, wurde an keiner Stelle eine Kollision bemerkt. Zwar gibt es mathematische Ansätze dieses Problem zu lösen, diese gehen jedoch auf Kosten der Performance.[7, S 11ff] Einfacher ist es eine konstante Update-Rate aufrecht zu erhalten und keine extrem dünne Entities zu verwenden, wo Kollisionen wichtig sind. 62

Abbildung 32: Discrete-Time-Issue: Ein Objekt "phased" durch ein anderes Limitierte Kollisionen In unserer aktuellen Implementation kollidiere noch all Entities mit allen. Das ist in soweit kein Problem, dass man in den Event-Handlern überprüfen kann ob die Kollision Sinn macht und wenn nicht sie einfach verwirft. Jedoch kann es trotzdem hilfreich sein die Kollisionen im Vorfeld zu Filtern. Dafür gibt es in der Klasse CollisionGeometryOwner die Methode cancollidewith(). Bevor eine Kollision ausgeführt wird, wird überprüft ob die beiden Geometrien auch wirklich miteinander kollidieren können. Problematisch wird es jedoch, da die Methode kommutativ sein soll. Dies bedeutet, dass a.cancollidewith(b) das gleiche Ergebnis wie b.cancollidewith(a) liefert. Assert.True(a.CanCollideWith(b) == b.cancollidewith(a)) Dass diese Bedingung erfüllt ist liegt in der Verantwortung des Programmierers. Eine zweite Methode ist die canmovecollide() Methode. Welche bestimmt mit welchen Entities in der moveposition() Methode kollidiert wird. Ohne diese Methode würde das Entity mit jeder anderen CollisionBox kollidieren. Mit 63

ihr lässt sich dies auf die Objekte einschränken mit der das Entity auch wirklich zusammenstoßen soll. Es ist jedoch zu beachten, dass cancollidewith() eine höhere Priorität hat als canmovecollide(). Wenn also cancollidewith() false ist, dann wird canmovecollide() überhaupt nicht mehr abgefragt. Kollisionen mit der Karte Im Gegensatz zu Kollisionen mit anderen Geometrien sind Kollisionen mit der Karte sehr einfach und sogar in konstanter Zeit zu überprüfen (der einzige Faktor ist hier die Geometriegröße). Wir haben zuvor für die CollisionMap schon eine Methode entwickelt um festzustellen auf welchen (CollisionMap- )tiles eine Geometrie eventuell liegen kann. Die gleiche Methode könne wir nun wiederverwenden um zu ermitteln mit welchen MapTiles eine Geometrie eventuell kollidieren kann. Zur exakten Ermittlung erstellen wir CollisionGeometrien für die einzelnen Tiles und lassen sie mit unserer Geometrie kollidieren. Da es eine von Anfang an bekannte Anzahl an Tiles gibt können wir diese Geometrien gleich am Anfang des Programms erzeugen und dann für später speichern. Jedoch fügen wir sie nicht einfach in die CollisionMap ein. Dies würde zwar funktionieren, jedoch würden die vielen Geometrien unserer Kollisionserkennung stark verlangsamen und wie oben schon besprochen müssen sie ja nicht in der Map sein um zu erkennen wann mit ihnen kollidiert wird. Da auch hier eine Entity nicht mit allen Tiles kollidieren möchte implementiert die Klasse Tile das Interface CollisionGeometryOwner. Und so kann unsere normale Methodik zur Limitierung von Kollisionen angewandt werden. Die Tiles werden dabei automatisch als CollisionGeometryOwner eingetragen. 64

3.2.5 Settings In absgdx gibt es zwei Arten von Einstellungen: Anwendungseinstellungen und Frameworkoptionen. Anwendungseinstellungen sind Einstellungen im eigentlichen Spiel, ihre Verwaltung liegt in der Hand des Entwicklers. Zusätzlich gibt es verschiedene Optionen die angeben wie das Framework sich verhält. Für beide Dinge werden DependentProperty Objekte verwendet. Framework Optionen Eine Instanz der Klasse DependentProperty repräsentiert einen einzelnen Wert. Dies ist meist ein Wahrheitswert kann aber auch ein String, eine Farbe oder eine Zahl sein. Der besondere Vorteil ist, dass jede Property von einer anderen abhängig sein kann. Eine Einstellung ist nur dann aktiv wenn alle Einstellungen von denen er abhängig ist ebenfalls aktiv sind. Daraus entsteht dann ein Abhängigkeitsbaum der einzelnen Einstellungen. Abbildung 33: Die absgdx Einstellungen in Baumdarstellung Dies ist beispielsweise für die Debugoptionen in absgdx nützlich: Wird die übergeordnete Option debugmode deaktiviert sind auch alle anderen untergeordneten Debugoptionen deaktiviert. Ist die Einstellung ein Boolean entscheidet sich ob sie aktiv ist einfach mittels ihres Wertes. Bei String-Properties werden alle Werte außer null als wahr gewertet und Zahlen werden immer als wahr gewertet. 65

Zusätzlich zu den normalen DependentProperties gibt es auch noch speziellere Subklassen: ConstantBooleanProperty: Dies ist eine BooleanProperty die nicht mehr veränderlich ist. Dies ist nützlich für zur Compilezeit festgelegte Werte (wie ob der Debugmode überhaupt möglich ist) MetaProperty: Diese Einstellung ist immer wahr und dient der Kategorisierung mehrere anderer Einstellungen unter sich. RootProperty: Dies ist ähnlich einer MetaProperty kann jedoch keine Abhängigkeiten haben und bildet somit die Wurzel des Baumes. 66

3.2.6 Debugging Es ist oftmals nötig ein Programm zu debuggen. Hierfür gibt es zwei Ansätze, einerseits kann man das Programm mit einem Debugger an einer bestimmten Stelle anhalten, den Speicher auslesen und den Code Zeile für Zeile durchlaufen. Dies ist nützlich bei Programmen welche etwas berechnen. Da wir hier jedoch ein fortwährend laufendes Programm haben ist es manchmal nötig das Programm live zu beobachten. Hierfür haben wir den DebugTextRenderer. Mit ihm kann man, ohne das Programm anzuhalten, im Bildschirm Debuginformationen anzeigen lassen. Abbildung 34: Beispiel der Debugansicht (In-Game) Außerdem können die Abgrenzungen von Entities, Tiles und Kollisionsgeometrien angezeigt werden. Zusätzlich werden die aktuellen physikalischen Eingenschaften (Position, Geschwindigkeit, Beschleunigung) von Entities mittels Pfeilen visualisiert. Ähnliche Anzeigen sind auch im MenuLayer für MenuElements vorhanden und können wenn nötig vom Benutzer um eigene erweitert werden. 67

Abbildung 35: Beispiel der Debugansicht (In-Menu) Die Anzeige der Debugansicht wird über die DependentProperties geregelt. Die Root Property debugenabled regelt ob die Debugansicht aktiv ist. Mit eventuellen Unterproperties kann man verschiedene Ansichten aktivieren oder deaktivieren. Es ist nützlich das Ein/Ausschalten der Debugansicht auf eine Tastaturtaste oder einen Menüknopf zu legen (aufrufen der doswitch() Methode). 68

3.2.7 Hintergründe In verschiedenen Fällen kann es vorkommen, dass Bildschirmbereiche nicht von einer Textur bedeckt sind. Zum Beispiel sorgt der ShowCompleteMapScaleResolver fast immer dafür, dass man den Hintergrund hinter den MapTiles sieht. Außerdem haben EmptyTiles keine Textur zum rendern und so kann man auch durch sie hindurchsehen. Auch normale Tile Texturen könne transparente Stellen besitzen. Die Frage ist also, was hinter den Tiles ist: Hierfür gibt es die Hintergrundobjekte, mit ihnen kann man einen Hintergrund definieren. Dies kann zum Beispiel bei Maps aus der Seitenansicht benutzt werden, um einen Himmel darzustellen. Abbildung 36: Eine Seitensansichts-Map mit Hintergrund Ein Hintergrund kann ebenfalls transparente Bereiche beinhalten und so kann man mehrere Hintergründe hintereinander anzeigen. Ein einzelner GameLayer kann somit beliebig viele Hintergründe besitzen, welche hintereinander angezeigt werden. Man kann eigene Hintergrundtypen definieren indem man von der Klasse MapBackground ableitet, vom Framework aus sind jedoch schon drei verschiedene Typen definiert: SingleBackground Der SingleBackground ist der einfachste Hintergrund. Er besitzt eine einzelne Textur welche unbeweglich an der gleichen Position angezeigt wird. Dies erzeugt die Illusion eines sehr weit entfernten Hintergundes, zum Beispiel könnte man hier die Textur eines Himmels mit SOnne und Wolken benutzen. 69

RepeatingBackground Der RepeatingBackground besteht ebenfalls aus einer einzigen Textur, diese wird jedoch so oft in X und Y Richtung wiederholt bis der ganze Bildschirm ausgefüllt ist, außerdem bewegt sich der Hintergrund mit der Karte mit. Dies erzeugt die Wahrnehmung, dass der Hintergrund auf der gleichen Ebene wie die Karte liegt. Man könnte hier zum Beispiel Hintergründe wie Büsche oder Häuser einfügen. Außerdem kann man diesen Hintergrund benutzen um einfach eine einfarbige Fläche als Hintergrund zu erhalten ohne eine extrem große Textur in den SingleBackground zu laden. ParallaxBackground Dieser Hintergrundtyp ist ein parallax-scrollender Hintergrund. Dies bedeutet, dass er aus nur einer einzigen Textur besteht, welche sich mit dem Kartenoffset zusammen bewegt. Um einen echten parallaxen Effekt zu bekommen muss man mehrere dieser Hintergründe benutzen, welche sich unterschiedlich schnell bewegen. Dies erzeugt dann die Illusion von mehreren Ebenen, die unterschiedlich weit weg vom Spieler sind. Die Geschwindigkeit mit der sich ein Hintergrund im Verhältnis zum Spieler (und damit im Verhältnis zur Karte) bewegt hängt von seinen Dimensionen ab. Der Hintergund scrollt genau so schnell, dass er bei einem X-Offset von 0% links am Bildschirm anliegt und bei einem Offset von 100% rechts am Bildschirm. 70

Abbildung 37: Erklärung zu Hintergründen mit Bewegungsparallaxe Wie man sieht ist es für die Textur wichtig mindestens so breit und hoch wie der Bildschirm zu sein. Für einen normalen parallaxen Effekt sollte die Textur auch kleiner als die Karte sein. Ist sie im Gegensatz breiter (oder höher) als die Karte gelten immer noch die gleichen Regeln wie zuvor, was dazu führt, dass der Hintergrund sich in die entgegengesetzte Richtung wie die Karte bewegt. 71

3.2.8 Der Menü Layer Neben dem GameLayer ist der MenuLayer der zweite wichtige Layer Typ. Mit ihm kann man eine einzelne Menüseite darstellen. Ein Menü besteht aus einem MenuFrame welcher verschiedene Menüelemente enthält, wie zum Beispiel Buttons, Edits, Label, Images etc. Mit einem MenuLayer kann man beispielsweise das Hauptmenü gestalten oder auch die Levelauswahl. Unser Layersystem ist dabei eine große Hilfe, denn wenn man auf den Layerstack ein Untermenü pushed muss man dieses nur wieder vom Stack entfernen um auf dem Obermenü zu landen. Komponenten Sämtliche Komponenten sind von der Klasse MenuBaseElement abgeleitet. Jedes MenuElement hat eine feste Position, Größe und eine eindeutige ID. Vom Framework aus sind schon eine Reihe an Standard Komponenten definiert, jedoch können auch eigene neue erstellt werden. Alle Komponenten befinden sich immer innerhalb eines MenuFrames (ausgenommen der MenuFrame selbst) und dieser befindet sich innerhalb eines MenuLayers. In einem Layer ist immer eine Komponente aktuell fokusiert. Der Fokus wird gewechselt wenn auf eine neue Komponente geklickt wird. Die Bedeutung des Fokus ist von der entsprechenden Komponente abhängig und kann sowohl visuelle als auch funktionelle Veränderung bedeuten. Abbildung 38: Klassendiagramm der Menü Elemente Die meisten Komponenten werden durch Komposition anderer Komponenten gebildet. So zeigt der MenuButton beispielsweise seinen Text nicht selbst an, sondern besitzt intern ein Label mit dem Text des 72

Buttons. Indem man das rendern immer wieder auf Subkomponenten auslagert kann man relativ einfach auch komplexere Komponenten bilden. So besteht beispielsweise der MenuSettingsTree aus mehreren MenuImages. MenuLabels und MenuCheckboxes. Klasse MenuButton Funktion Ein einfacher Knopf zum Drücken. Das OnClick-Event kann mittels eines ButtonListeners abgefangen werden. MenuCheckBox MenuRadioButton Eine Optionsbox, welche entweder aktiviert oder deaktiviert ist Eine Optionsbox, welche entweder aktiviert oder deaktiviert ist. Im Kontext des übergeordneten Containers ist immer nur maximal ein RadioButton aktiviert MenuEdit Ein Text Eingabefeld. Es sind nur Eingaben möglich die von der übergebenen BitmapFont dargestellt werden können. Bei Eingaben länger als die Komponentenbreite entsteht ein Scrolleffekt. MenuImage Zeigt entweder eine statische Textur an oder eine sich wiederholende Animation MenuLabel Zeigt einen Text an. Der Text ist entweder fest skaliert oder passt sich den Dimensionen des Labels an MenuContainer Ein Container welcher mehrere Kindelemente enthält. Die Positionen der Kinder sind relativ zu der des Containers. Möchte man einen Container zum reinen Zweck der logischen Gruppierung kann man direkt diese Klasse verwenden. Ist eine visuelle Gruppierung erwünscht sollte man die Klasse MenuPanel verwenden. MenuPanel MenuFrame Ein Container welcher eine eigene Textur besitzt. Der Wurzelcontainer jedes MenuLayers. Er besitzt immer die Breite des aktuellen Anzeigegerätes und kann nicht ausgetauscht werden. MenuSettingsTree Eine Komponente um einen Baum von DepenedentProperties anzuzeigen. Die einzelnen Knoten könne individuell aktiviert/deaktiviert werden und die einzelnen Unterbäume zusammengeklappt oder ausgeklappt werden. Einige Elemente benötigen einen Möglichkeit um Schrift anzuzeigen. Hierfür hat jedes MenuElement ein Feld font welches vom Typ BitmapFont ist. Dieses Feld wird von den Komponenten an ihre 73

Kinder weitervererbt. Es reicht also theoretisch dem MenuFrame ein Font zu geben und der Frame wird es an alle seine Kinder weitergeben. Hat eines der Kinder jedoch einenen eigenen Font definiert erhält dieser Priorität. So ist es auch möglich, zwei Containern jeweils unterschiedliche Fonts zu geben und damit ihnen und all ihren Kindern ein unterschiedlichen Aussehen zu spendieren. Texturen und GUITextureProvider Außer dem MenuContainer werden alle Komponenten angezeigt. Standardmäßig haben die Komponenten eine Renderroutine welcher allein mit dem ShapeRenderer auskommt. Dies bedeutet, dass die Komponenten mittels farbigen Rechtecken und Linien gezeichnet werden. Dies ist jedoch eigentlich nur zum Debuggen gedacht. Im Produktiveinsatz braucht jede Komponente eine Reihe an Texturen um gezeichnet zu werden. Ein MenuButton beispielsweise braucht insgesamt 36 Texturen um gezeichnet zu werden. Er besitzt vier Zustände (normal, gedrückt, fokusiert, deaktiviert) und Jeder dieser Zustände braucht neun Texturen: Die vier Ecken, die vier Kanten und eine für die eigentliche Fläche. Abbildung 39: Die neun Texturen einer Menüfläche Diese Unterteilung in neun Texturen sorgt dafür, dass man Elemente in beliebiger Dimensionierung anzeigen kann, ohne die Texturen verzerren zu müssen. Außerdem ist es hier relativ einfach ein neues UI Kit zur Verfügung zu stellen - alles was man hierfür tun muss ist die Texturen auszutauschen. Die Verteilung der Texturen erfolgt über eine Instanz der Klasse GUITextureProvider. Dies ist primäre eine HashMap welche einem 3-Tupel aus Klasse, Identifier und Modifier eine Textur zuordnet. So ist beispielsweise der Klasse MenuButton, dem Identifier texture-topleft und dem Modifier focused eine spezielle Textur zugeordnet. Den Weg über diese Klasse hat den Vorteil, dass man allen Buttons den gleichen TextureProvider geben kann und sie alle damit die gleiche Button-Textur bekommen. Möchte man einen Button welcher eine andere Textur hat, muss man ihm nur einen eigenen TextureProvider geben. Die Identifikation über die Klasse ermöglicht es auch verschiedene Texturen für verschiedene Subklassen 74

von MenuButton zur Verfügung zu stellen und eine einfache Unterscheidung zwischen den Texturen von zum Beispiel einer CheckBox und einem RadioButton zu haben. Die einzige Ausnahme bilden MenuImages. Da diese im Normalfall alle eine eigene Textur haben wird ihre Anzeigetextur über die Methode setimage() gesetzt wird und nicht über den TextureProvider. Events Jedes MenuElement hat eine Liste von MenuElementListener. In diesem Listener sind alle Events integriert welche Element unabhängig auftreten: onpointerdown Die Maus/der Touchpointer drückt auf dieses Element onpointerup Die Maus/der Touchpointer löst sich von diesem Element onclicked Die Maus/der Touchpointer führt ein vollständiges Klick-Manöver aus (PointerDown + PointerUp) onhover Die Maus/der Touchpointer betritt die Abgrenzung dieser Komponente onhoverend Die Maus/der Touchpointer verlässt die Abgrenzung dieser Komponente onfocus Diese Komponente erhält den Fokus onfocuslost Diese Komponente verliert den Fokus Zusätzlich können Elemente von dem Interface MenuElementListener erben und somit neue Events einführen welche speziell für ein Element gedacht sind. So gibt es beispielsweise das Interface MenuCheckboxListener welches zu den vorhandenen Methoden noch die Methode onchecked hinzufügt. 75

Menüs mit AGDXML definieren Bisher haben wir Menüs manuell im Code erstellt. Dies hat zwei Nachteile. Einerseits kann es sehr kompliziert werden komplexere Menüs zusammenzustellen- Andererseits sind diese Menüs statisch und können sich nicht an verschiedene Größen oder dynamische Größenänderungen anpassen. Als Lösung hierfür gibt es den AgdxmlLayer, welcher vom MenuLayer ableitet. Dieser bekommt eine Menüdefinition in Form einer speziellen XML Datei. Mittels dieser Definition wird dann dynamisch ein Menü erstellt. Die Menüdefinition kann entweder auf fixen Maßen beruhen oder die Komponenten dynamisch und relativ zur verfügbaren Fläche definieren. 76

Das AGDXML Format Abbildung 40: Der AGDXML Tag Tree Das AGDXML Format besteht aktuell aus acht verschiedenen Tags die jeweils einem MenuElement zugeordnet sind. Der Root-Tag des AGDXML Dokumentes muss immer <frame> sein. Darunter kann man mittels anderer Tags Menükomponenten erzeugen. Es gibt einige Attribute, welche alle Tags (außer dem <frame> Tag) haben. Zum Beispiel kann man für alle Komponenten die Position, Höhe und Breite festlegen. Hierbei kann man entweder ein konstantes Maß in der Einheit Pixel angeben oder einen Prozentwert, welcher relativ zum übergeordneten Element interpretiert wird. Diese Prozentwerte sind die 77

erste Möglichkeit um dynamische Menüs zu erzeugen. Außerdem kann man für alle Komponenten die ID festlegen, so wie den GUITextureProvider und die Sichtbarkeit. Der TextureProvider wird in einem AGDXML Baum automatisch vererbt. Möchte man also allen Komponenten den gleichen Provider zur Verfügung stellen so muss man diesen nur im <frame> Tag definieren und er wird nach unten im Baum an alle Komponenten weitergegeben. Angegeben wird der GUITextureProvider. über einen identifier Dieser muss im Programmcode mittels der Methode addagdxmlguitextureprovider(string key, GUITextureProvider value) einem TextureProvider zugeordnet werden. Auch Events kann man in AGDXML registrieren, hierzu muss man in der AGDXML Datei nur den Methodennamen angeben. Über Type Introspection wird automatisch die passende Methode in der Klasse welche von AgdxmlLayer ableitet gesucht. Dabei ist zu beachten, dass eine Methode mit richtigem Namen und richtiger Parameterliste existieren muss, sonst wird eine Exception vom Type AgdxmlParsingException geworfen. Dies vereinfacht die Definition von Events erheblich - da man Methoden nicht umständlich doppelt registrieren muss, sondern sie einfach deklarieren kann und in der AGDXML Datei angeben. Der wirklich große Vorteil von AGDXML ist jedoch der <grid> Tag. Mit ihm kann man einen dynamischen Container erzeugen. Mithilfe der Tags <columndefinitions> und <rowdefinitions> kann man das Grid in ein Raster unterteilen. Und mit den Pseudo-Attributen grid.row und grid.column können Elemente in dieses Raster eingebettet werden. Sind keine zusätzlichen Attribute angegeben füllt ein Element immer seine gesamte Rasterzelle aus. Jedoch kann dies auch mit den Attributen position, width und height manipuliert werden. Als Spalten-/Zellenmaße gibt es drei verschiedene Definitionsmöglichkeiten: 78

Name Schreibweise Funktion PIXEL Zahlenwert alleine angeben Die Spalte/Reihe bekommt exakte die festgelegten Maße PERCENTAGE Zahlenwert mit angefügtem Prozentzeichen Die Größe der Spalte/Reihe ist prozentual abhängig von der Breite/- Höhe des gesamten Grid-Elements WEIGHT Zahlenwert mit angefügtem Stern Der verbleibende Freiraum (nach PIXEL und PERCENTAGE) wird gemäß der Gewichtung verteilt. Eine Reihe mit doppeltem Gewicht ist auf jeden Fall doppelt so hoch wie eine mit nur einfachem Gewicht. Die einzelnen Definitionen werden am beste in Kombination genutzt. So kann man ein Grid mit 3 Reihen definieren bei dem zwei gleich groß sind und die dritte hundert Pixel hoch. Einige Attribute sind Farbdefinitionen. Hier kann man entweder direkt einen Hexadezimal-Wert angeben (zum Beispiel #FF8800). Alternativ kann man auch direkt einen Wert aus einer Liste an vordefinierten Farbkonstanten benutzen (zum Beispiel BLACK oder MAGENTA). Zuletzt gibt es einige Attribute welche ein Padding definieren. Dies sind vier Werte welche den vier Seiten eines Rechtecks entsprechen. Diese Werte kann man in unterschiedlichen Formaten angeben: 2, 8, 2, 8 Man gibt alle vier Werte in der Reihenfolge top - left - bottom - right an. 2, 8 Man gibt zuerst den top-bottom Wert an und dann den left-right Wert. 2 Man setzt alle vier Attribute auf den gleichen Wert. 2%, 4%, 2%, 4% Man kann die oberen Methoden auch mit den Prozent Schreibweise kombinieren Eine (extrem einfach gehaltene) AGDXML Datei könnte beispielsweise so aussehen: <?xml version="1.0" encoding="utf-8"?> <frame textures="provider1" > <grid container="true"> <grid.columndefinitions width="20, 2*, 10, 1*, 20" /> <grid.rowdefinitions height="8\%, 40, 4\%, 1*, 4\%" /> 79

<label grid.row="1" grid.column="1" content="hello, World!"/> <image grid.row="3" grid.column="3" texture="ani_01" animation="750" id="myimage"/> <edit grid.row="3" grid.column="1" text="playername" textcolor="black" halign="left" /> </grid> </frame> Der AGDXML Menudesigner Als zusätzliches Hilfsmittel für AGDXML Dateien gibt es den AGDXML-Menudesigner. Die ist ein WY- SIWYG Editor für AGDXML Dateien. Abbildung 41: Screenshot des AGDXML Menudesigner Der Vorteil des Menudesigners ist es, dass man direkt - während dem Schreiben - sehen kann ob ein syntaktischer Fehler in der AGDXML Datei ist und wie das resultierende Format ungefähr Aussehen wird. Außerdem kann man Sehen wie sich das Menü unter verschiedenen Auflösungen verhält. 80