Positionsabhängige Spiele
Transcrição
Positionsabhängige Spiele
Positionsabhängige Spiele Andreas Pecuch DIPLOMARBEIT 05/1/0305/021 eingereicht am Fachhochschul-Masterstudiengang Digitale Medien in Hagenberg im September 2007 c Copyright 2007 Andreas Pecuch Alle Rechte vorbehalten ii Erklärung Hiermit erkläre ich an Eides statt, dass ich die vorliegende Arbeit selbstständig und ohne fremde Hilfe verfasst, andere als die angegebenen Quellen und Hilfsmittel nicht benutzt und die aus anderen Quellen entnommenen Stellen als solche gekennzeichnet habe. Hagenberg, am 3. September 2007 Andreas Pecuch iii Inhaltsverzeichnis Erklärung iii Vorwort vi Kurzfassung vii Abstract viii 1 Einleitung 1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Zielsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 3 2 Theoretische Grundlagen 2.1 Arten der Positionsbestimmung . . . . . . . 2.1.1 GPS . . . . . . . . . . . . . . . . . . 2.1.2 Zellenidentifikationsverfahren . . . . 2.1.3 Zeitdifferenzverfahren . . . . . . . . 2.2 Fehler bei der Positionsbestimmung mittels GPS . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Selective Availability . . . . . . . . . 2.2.2 Mehrweg-Effekt . . . . . . . . . . . . 2.2.3 Atmosphärische Störungen . . . . . 2.2.4 Satelitenumlaufbahn . . . . . . . . . 2.2.5 Uhrengenauigkeit . . . . . . . . . . . 2.3 NMEA Übertragungsprotokoll . . . . . . . . 3 Prototypen 3.1 Spielprinzip . . . . . . . . . . . . 3.2 Technische Anforderungen . . . . 3.2.1 Displaygröße . . . . . . . 3.2.2 Bluetooth . . . . . . . . . 3.2.3 Dateiverwaltung . . . . . 3.2.4 J2ME-Laufzeitumgebung iv . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 6 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 9 9 10 13 14 . . . . . . 15 15 16 16 17 17 18 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v INHALTSVERZEICHNIS 3.3 3.4 3.5 3.6 3.7 3.8 3.2.5 Testumgebung . . . . . . Basisapplikation . . . . . . . . . 3.3.1 Bluetooth-Unterstützung 3.3.2 GPS-Anbindung . . . . . Controller . . . . . . . . . . . . . Logger . . . . . . . . . . . . . . . ErrorMessage . . . . . . . . . . . Potbanging Controller . . . . . . 3.7.1 Tastenbelegung . . . . . . 3.7.2 Spielablauf . . . . . . . . Crossgolf Controller . . . . . . . 3.8.1 Tastenbelegung . . . . . . 3.8.2 Spielablauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 20 23 27 29 30 33 34 36 36 40 41 42 4 Schlusswort 47 4.1 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 A Sourcecode B Inhalt der CD-ROM B.1 Diplomarbeit . . . B.2 LaTeX-Dateien . . B.3 Style-Dateien . . . B.4 Dokumentation . . B.5 Sonstiges . . . . . Literaturverzeichnis 51 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 81 81 82 82 82 83 Vorwort An dieser Stelle möchte ich mich bei jenen Personen bedanken, die es ermöglicht haben, dass das Projekt und somit diese Arbeit abgeschlossen werden konnte. Besonderer Dank geht dabei an den Betreuer des Projekts, DI Roman Divotkey, der mir jederzeit mit Rat und Tat zur Seite stand. Weiters möchte ich mich auch bei meinen Freunden Wolfgang Schermann und Andreas Böhme bedanken, die mir bei der Realisierung und beim Testen der Prototypen geholfen haben und mich immer mit guten Ratschlägen versorgt haben. Abschließend geht ein großer Dank an meine Eltern, die mir mein Studium überhaupt ermöglicht haben. An dieser Stelle könnte Ihre Werbung stehen... vi Kurzfassung Aufgrund der weiten Verbreitung von Mobiltelefonen, und der Möglichkeit an jedem Ort zu jeder Zeit zu spielen, ist die Anzahl an entwickelten Spielen für mobile Endgeräte in den letzten Jahren ständig gewachsen. Durch den Gebrauch von Lokalisierungstechnologien wie GPS wurde es möglich, die Fortbewegung der Spieler als zentrale Form der Interaktion mit der auf mobilen Endgeräten implementierten Spiellogik zu verwenden. Da die Positionsbestimmung mittels GPS nicht hundertprozentig Fehlerfrei ist, nimmt sie Einfluss auf die Spieldynamik. In dieser Arbeit wurden anhand von zwei Prototypen die Auswirkungen der Positionsbestimmung auf die Spiele evaluiert. vii Abstract Due to the widespread use of mobile telephones and the possibility to play mobile games anywhere and anytime, the number of number of mobile games has grown permanently within the last few years. By the use of localization technologies like GPS it got possible to use the players movement as central form of interaction with the game logic implemented on mobile devices. Since the position determination by means of GPS is not fault-free, the position determination interfere with the game dynamics. The effects of the position determination on the game dynamics are evaluated in this thesis on the basis of two location-based-game-prototypes. viii Kapitel 1 Einleitung 1.1 Motivation Nicht ohne Grund verkaufte sich der Gameboy mehr als jede andere Spielekonsole. Die Tatsache, dass es möglich ist ein Spiel zu jeder Zeit und an jedem beliebigen Ort spielen zu können, ist Grund genug für viele vom statischen Computer auf mobile Konsolen umzusteigen. Im Vergleich zu modernen Spielekonsolen oder dem Computer können Mobiltelefone nicht mit der Grafik- oder Klangqualität mithalten, allerdings haben die meisten Mobiltelefonbenutzer ihr Handy immer bei sich. Das kann bei einem einfachen und doch packenden Spielprinzip wie z.B. bei Snake (Abb. 1.1), dazu führen, dass in jeder freien Minute das Handy herausgeholt wird um einen neuen Rekord aufzustellen. Allerdings gab es nach Snake kein Spiel für Mobiltelefone, welches an den Erfolg des Vorgängers anknüpfen konnte. Ebenso setzte die Spieleindustrie mit der Entwicklung neuer Spielekonsolen wie dem Nintendo DS (NDS) oder der Playstation Portable (PSP) neue Maßstäbe für Bild- und Tonqualität bei tragbaren Spielekonsolen, welche mit einem Mobiltelefon nicht erreichbar sind. Um sich von den derzeitigen Computerspielen zu unterscheiden, deren Basis der Wettkampf ist, gilt es für den Bereich der mobilen Spiele die Vorteile der Mobilität in den Vordergrund zu rücken. Mit der Möglichkeit Daten aus dem Internet auf dem Mobiltelefon auszulesen began auch die Entwicklung der standortbezogenen Diensten (engl. Location-based Services). Dadurch ist es dem Benutzer möglich, in einer unbekannten Umgebung schnell Informationen über die örtliche Infrastruktur zu erhalten. Durch die Möglichkeit die Position des Mobiltelefons anhand der Funkzelle, in der das Gerät angemeldet ist, zu bestimmen oder aber auch mittels eingebautem oder externem GPS-Modul errechnen zu lassen, können Spiele entwickelt werden, welche die aktuelle Position des Spielers in das Spielgeschehen einfließen lassen [4]. Bot Fighters von It’s Alive! oder Battlemachine entwickelt von Unwi1 2 KAPITEL 1. EINLEITUNG Abbildung 1.1: Screenshot http://www.areamobile.de/. des Spieleklassikers Snake. Quelle: redFactory waren eine der ersten positionsabhängigen Spiele die für das Mobiltelefon entwickelt wurden, doch wie mit jeder neuen Technologie die verwendet wird, traten auch bei positionsabhängigen Spielen Probleme auf. So nahm die Positionsbestimmung mittels GPS-Empfänger beim Spiel Can You See Me Now durch ihre Ungenauigkeit und Unzuverlässigkeit Einfluss auf das Spielgeschehen (siehe [1, S. 6–10]). 1.2 Zielsetzung Ziel der Diplomarbeit ist es, herauszufinden welchen Einfluss Positionsbestimmungsverfahren auf das Spielprinzip von positionsabhängigen Spielen haben. Anhand der Auswertung von Usertests zweier Spielprototypen, umgesetzt als Java 2 Micro Edition (J2ME) Applikation, sollen die Auswirkungen bestimmt und mit vorhandenen Berichten verglichen werden. Ebenso sollen anhand der Prototypen Möglichkeiten aufgezeigt werden, diese Auswirkungen ins Spiel mit einzubeziehen oder zu kompensieren. Abschließend wird über das aus den Tests und Vergleichen gewonnene Wissen reflektiert und eine persönliche Schlussfolgerung gezogen, ob sich positionsabhängige Spiele in weiterer Zukunft behaupten können und welche Verbesserungen der derzeit bestehen Positionsbestimmungsverfahren noch vorgenommen werden müssen. Aufgrund der Vielfalt an Verfahren die es zur Positionsbestimmung gibt, wird im Folgenden nur auf die Ermittlung des Standorts mittels BluetoothGPS-Empfänger eingegangen um eine breite Basis an Mobiltelefonen für Tests zu ermöglichen, da nicht viele Mobiltelefone bisher einen GPS-Empfänger eingebaut haben. Andere Verfahren zur Standortbestimmung sind ausgeschlossen worden, da die Verwendung eines GPS-Empfängers kostengünstig ist und sich dadurch auszeichnet, dass man diesen ohne zusätzliche Installationen weltweit einsetzen kann. KAPITEL 1. EINLEITUNG 1.3 3 Aufbau der Arbeit Um Unklarheiten zu vermeiden, werden im Kapitel 2 themenrelevante Begriffe wie GPS oder NMEA erklärt. Des Weiteren wird auf die unterschiedlichen Möglichkeiten zur Positionsbestimmung und auf die Fehler beim GPSPositionsbestimmungsverfahren eingegangen. Kapitel 3 befasst sich mit den beiden Prototypen und deren Umsetzung und beschreibt den Hauptteil der Diplomarbeit. Im Abschnitt 3.1 bekommt der Leser ein kurzer Überblick über die beiden unterschiedlichen Spielprinzipien. Der Abschnitt 3.2 befasst sich anschließend mit den technischen Anforderungen, die an das Mobiltelefon und an den GPS-Empfänger gerichtet sind. Abschnitt 3.3 beschreibt die Basisapplikation und den Kommunikationsaufbau zwischen Mobiltelefon und GPS-Empfänger. Im Abschnitt 3.7 wird anschließend genauer auf die Spiellogik des ersten Prototypen eingegangen, während der Abschnitt 3.8 von der Logik und Umsetzung des zweiten Prototypen handelt. Im Kapitel 4 wird abschließend noch einmal zusammengefasst, wo die Vor- und Nachteile bei den beiden positionsabhängigen, mobilen Spielen liegen und was noch verbessert werden kann. Zum Schluss wird noch darüber reflektiert, welchen Einfluss die Positionsbestimmung bei den Testläufen hatte und wie dem entgegengewirkt werden kann. Kapitel 2 Theoretische Grundlagen 2.1 Arten der Positionsbestimmung Um den Einfluss von Genauigkeit und Messintervall auf das Spielprinzip bei positionsabhängigen Spielen besser verstehen zu können, wird in diesem Abschnitt auf die unterschiedlichen Techniken zur Positionsbestimmung eingegangen. Um den Standort eindeutig feststellen zu können kann man entweder mit absoluten oder relativen Positionsdaten arbeiten. Eine absolute Bestimmung des Standorts wäre zum Beispiel mittels Längen- und Breitengrad sowie einer Höhenmessung. Die Messung der Abweichung des Standorts zu einem gegebenen Punkt bezeichnet man als relative Position. Neben den eigentlichen Positionsdaten sind meistens auch die Orientierung im Raum und die Geschwindigkeit von Bedeutung. So ist es bei Navigationssystemen wichtig neben der aktuellen Position auch die Fahrtrichtung und die Geschwindigkeit des Fahrzeugs zu kennen um die Fahrtroute besser im Voraus berechnen zu können. Grundsätzlich unterscheidet man zwischen zwei Arten der Positionsbestimmung. Bei der aktiven Positionsbestimmung bestimmt das mobile Gerät selbst die Position über ein System aus Sendern. Der Empfänger im Endgerät ermittelt dabei aus den eintreffenden Funk-, Infrarot- oder Ultraschallsignalen der Sender die aktuelle Position (handset-based-positioning). Der Vorteil bei diesen Verfahren liegt darin, dass nur das Endgerät die aktuelle Position kennt und man so vor einem ungewollten, externen Zugriff auf die Positionsdaten geschützt ist. Bei der passiven Positionsbestimmung ermittelt ein Sensorennetzwerk die Position des Benutzers, wertet die Daten aus und übermittelt das Ergebnis anschließend an das Endgerät (network-basedpositioning). Neben der Unterscheidung zwischen aktiven und passiven Positionsbestimmungsverfahren kann ebenfalls noch zwischen Ortsbestimmung in geschlossen Räumen und im Freien unterschieden werden. Aufgrund der Viel4 KAPITEL 2. THEORETISCHE GRUNDLAGEN 5 Abbildung 2.1: Positionsbestimmung mit Satelliten. Aus [5]. zahl an Arten zur Positionsbestimmung wird in den folgenden Abschnitten nur auf die Verfahren eingegangen, welche für diese Diplomarbeit in Frage gekommen sind. Da mit Mobiltelefonen gearbeitet worden ist, wird neben dem GPS-Verfahren auch auf die Möglichkeiten zur Positionsbestimmung im GSM-Netz eingegangen. 2.1.1 GPS Wenn von GPS (Global Positioning System) die Rede ist, dann ist meistens das NAVSTAR-GPS (Navigational Satellite Timing and Ranging Global Positioning System) Navigationssytem gemeint, welches 1970 vom US-Verteidigungsministerium konzipiert wurde. Erst 1993 wurde mit 24 Satelliten (21 Systemsatelliten und 3 Reservesatelliten) eine erste Betriebsbereitschaft erreicht. Die volle Funktionsbereitschaft wurde im Juli 1995 erklärt. Derzeit befinden sich ca. 31 Satelliten zur Positionsbestimmung im Umlauf um die Erde. Durch die Anordnung der Satelliten ist gewährleistet, dass von jedem Ort auf der Erde mindestens fünf Satelliten über dem Horizont sichtbar sind. GPS ist ein satellitengestütztes Positionsbestimmungsverfahren. Hierfür ermittelt das Endgerät die exakte Position eines Satelliten und die Entfernung zu selbigem. Da der Empfänger die Position des Satelliten nicht bestimmen kann, sendet der Satellit ständig seine eigene Position und den Zeitpunkt der Positionsbestimmung. Durch den Zeitversatz zwischen dem Senden der Daten und dem Empfang am Endgerät kann die Distanz zwischen Satellit und Empfänger bestimmt werden. Bei einer gegebenen Entfernung zu einem Satelliten kann sich der Empfänger irgendwo auf einer Kugeloberfläche mit dem Radius der Entfernung befinden. Erst mit den Positionsund Entfernungsdaten zu drei unterschiedlichen Satelliten kann eine genaue KAPITEL 2. THEORETISCHE GRUNDLAGEN 6 Abbildung 2.2: Positionsbestimmung mittels Zellenidentifikationsverfahren (links: omnidirektionale Antenne, rechts: Antenne mit Richtcharakteristik). Aus [5]. Positionsbestimmung auf der Erdoberfläche erfolgen wie in (Abb. 2.1a) zu sehen ist. Eigentlich führt der Schnitt von drei Kugeloberflächen ja zu zwei Schnittpunkten. Da einer der Punkte allerdings im Weltall liegt (Abb. 2.1b) kann dieser vernachlässigt werden [5]. Ein Fehler bei der Zeitmessung von einer Sekunde würde eine Abweichung von 300000 km in der Entfernungsbestimmung ergeben. Aus diesem Grund sind die Satelliten zur Positionsbestimmung mit einer Atomuhr ausgerüstet um eine exakte Messung zu gewährleisten. Da in den GPSEmpfängern keine Atomuhr eingebaut ist, muss man neben der geographische Länge, geographische Breite, und der Höhe über der Erde auch die Zeit abschätzen. Deshalb benötigt man noch einen vierten Satelliten zur Bestimmung der Laufzeiten um eine exakte Positionsbestimmung durchführen zu können (siehe [5, Kap. 7.2.1]). 2.1.2 Zellenidentifikationsverfahren Da ein drahtloses Netz sehr leicht Störungen unterworfen ist und die Sendeleistung eines Mobiltelefons nur eine begrenzte Reichweite aufweist, wurde das Netz in so genannte Zellen eingeteilt. Jede Zelle besteht aus einer Sendeund Empfangsstation, die Basisstation genannt wird. Am Übergangsbereich zwischen 2 Zellen findet eine Überlappung zwischen zwei oder mehr Zellen statt, das heißt, der Teilnehmer befindet sich im Empfangsbereich mehrerer Basisstationen [5]. Durch die geographische Position der Basisstation und der Ausrichtung KAPITEL 2. THEORETISCHE GRUNDLAGEN 7 der Sendeantenne kann ungefähr auf die Position des Mobiltelefons im Mobilfunknetz geschlossen werden. Da die Zellengröße von Region zu Region schwankt (in dicht besiedelten Gebieten ist eine Mobilfunkzelle kleiner als in ländlichen Gebieten), variiert auch die Genauigkeit der Positionsbestimmung mittels Zellenidentifikation zwischen 100 Metern und 30 Kilometern. Dabei gilt, je größer der Abstrahlwinkel einer Antenne ist und je größer die Zelle ist, desto ungenauer ist auch die Positionsbestimmung. Innerhalb der Zelle kann eine genauere Positionsbestimmung mittels Timing Advance (TA) erreicht werden. Die Kommunikation zwischen Basisstation und Mobiltelefon funktioniert in Zeitschlitzen. Damit nun die vom Mobiltelefon gesendeten Daten zum richtigen Zeitpunkt bei der Basisstation eintreffen, teilt diese dem Mobiltelefon mit, um wie viele Mikrosekunden vorher die Daten vom Mobiltelefon gesendet werden müssen. TA kann Werte von 0-63 annehmen, wobei TA01 einer ungefähren Entfernung von 550 Metern zur Basisstation, bei reflektionsfreiem Übertragungsweg, entspricht. In Abbildung 2.2 ist das Zellenidentifikationverfahren dargestellt, die Größe der Zelle (gekennzeichnet durch den äußeren Kreis) beschreibt den Bereich der möglichen Position eines in der Mobilfunkzelle angemeldeten Teilnehmers. Aufgrund der TA-Information beschränkt sich der Bereich auf einen Ring um die Basisstation (rot gekennzeichnet). Wird anstatt einer omnidirektionalen Antenne mehrere Antennen mit Richtcharakteristik verwendet (in Abb. 2.2 zum Beispiel mit einem Abstrahlwinkel von 120◦ ) kann anhand der Ausrichtung der Antenne die Position des Teilnehmers auf ein Kreissegment eingegrenzt werden. Das rote Ringsegment beschreibt wiederum die Verbesserung der Positionsbestimmung mittels TA-Information. Eine höhere Genauigkeit beim Zellenidentifikationsverfahren (engl. Cell of Origin) kann auch dadurch erzielt werden, indem die Signalpegel der 6 nähesten Basisstationen ermittelt wird. Anhand der Signalpegel kann auf eine genauere Position in der aktuellen Zelle geschlossen werden. 2.1.3 Zeitdifferenzverfahren Wie bei der satellitengestützten Positionsbestimmung wird beim Zeitdifferenzverfahren (Time Difference of Arrival (TDOA)) die Signallaufzeit gemessen. Anhand der Signallaufzeitunterschiede zwischen Mobiltelefon und mehreren Basisstationen (mindestens drei) kann durch Triangulierung die Position des Mobiltelefons im GSM-Netz bestimmt werden (siehe Abb. 2.3). Wie beim Zellenidentifikationsverfahren werden auch beim Zeitdifferenzverfahren die Berechnungen zur Positionsbestimmung nicht dem Mobiltelefon überlassen. Im Network Subsystem (NSS), der Verbindung zwischen benachbarten Basisstationen, befindet sich das Serving Mobile Location Center (SMLC), welches die geographische Position des Mobiltelefons errechnet. Falls keine Position errechnet werden konnte, kann auf die letzte Position des Mobiltelefons zugegriffen werden. Jede Basisstation besitzt deshalb eine KAPITEL 2. THEORETISCHE GRUNDLAGEN 8 Abbildung 2.3: Positionsbestimmung mittels Zeitdifferenzverfahren. Location Mesurement Unit (LMU) welche die zuletzt bestimmte Position eines Mobiltelefons gespeichert hat. 2.2 2.2.1 Fehler bei der Positionsbestimmung mittels GPS Selective Availability Aufgrund von Sicherheitsbedenken seitens der USA über einen terroristischen Anschlag mit ferngelenkten Waffen auf Gebäude der amerikanischen Regierung, wurde mit der Einführung der Positionsbestimmung mittels GPS eine künstliche Fehlerquelle, die so genannte selective availability (SA) eingebaut. Bei der SA werden die Navigationsmitteilungen der Satelliten (Ephemeriden, Uhrzeit, etc.) durch gewollte Schwankungen im Signal verstümmelt. Die stündliche Veränderung der Ephemeriden ging unmittelbar als Fehler in die gemessene Pseudo-Entfernung ein, da die übermittelte Satellitenposition nicht mit der tatsächlichen Position des Satelliten übereinstimmte. Aufgrund der Unregelmäßig der Positionsschwankungen konnte der Fehler deshalb nicht korrigiert werden, was zu einer Ungenauigkeit der Position um 50 bis 150 Meter führte. Durch eine künstliche Verfälschung der von den Satelliten an die GPS-Empfänger übermittelte Uhrzeit führte bei zivilen Empfängern dazu, dass es zu Positionsschwankungen um ungefähr 50 Meter kam. Während bei eingeschalteter SA die Positionsgenauigkeit im Bereich von 100 Metern lag, wird jetzt eine Genauigkeit von 20 Meter erreicht, die KAPITEL 2. THEORETISCHE GRUNDLAGEN 9 in der Praxis häufig noch unterschritten wird [3]. Das erste Mal wurde die selective availability vorübergehend im Golfkrieg deaktiviert als amerikanische Soldaten aufgrund eines Versorgungsmangels an militärischen Empfangsgeräten auf zivile GPS-Empfänger zurückgreifen mussten. Aufgrund der großen Verbreitung von zivilen GPS-Empfängern wurde die selective availability im Mai 2000 bis auf weiteres abgeschalten [3]. 2.2.2 Mehrweg-Effekt Wie auch Schallwellen können elektromagnetische Wellen an Hindernissen reflektiert werden. Durch die Reflektion der Satellitensignale an Objekten kommt der Mehrwegeffekt (Multipath) zustande, der sich besonders stark in urbanem Gebiet mit hohen Häusern oder in einem engen Tal auf das Positionsbestimmungsverfahren auswirkt. Ein Fehler tritt deshalb auf, da das reflektierte Signal länger als ein direkt empfangenes Signal braucht, um den Empfänger zu erreichen. Der daraus resultierende Fehler liegt typischerweise bei wenigen Metern, kann aber auch mehrere Kilometer betragen. Die von den Satelliten ausgestrahlten Signale sind polarisiert und die Antennen der Empfänger sind so konstruiert, dass nur diese Signale optimal empfangen werden. Durch eine Änderung der Polarisierungsrichtung bei reflektierten Signalen hingegen können diese nicht mehr von der Antenne empfangen werden. Der Antennenaufbau beeinflusst demnach maßgeblich die Qualität, wie gut der durch den Mehrwegeffekt hervorgerufene Fehler unterdrückt wird [3]. 2.2.3 Atmosphärische Störungen Die, durch atmosphärische Effekte in der Troposphäre und Ionosphäre verringerte, Ausbreitungsgeschwindigkeit trägt ebenfalls zum Genauigkeitsfehler bei. Während sich elektromagnetische Wellen im einem Vakuum mit Lichtgeschwindigkeit ausbreiten, breiten sich diese in der Ionosphäre und der Troposphäre mit geringerer Geschwindigkeit aus [6]. Durch Strahlungseinflüsse der Sonne werden Valenzelektronen von den Atomen gelöst, wodurch freie Elektronen und positive Ionen übrig bleiben. Die Ionosphäre beginnt in einer Höhe von ungefähr 80 km, je höher man steigt, desto geringer wird die Ladungsträgerdichte da die Anzahl der Teilchen in der Atmosphäre abnimmt, deshalb liegt die natürliche Grenze der Ionosphäre bei circa 400 km. Diese konzentrieren sich in vier leitenden Schichten innerhalb der Ionosphäre (D-, E-, F1-, und F2- Schicht). Diese Schichten reflektieren bzw. brechen die elektromagnetischen Wellen der Navigationssatelliten. Daraus folgt eine längere Laufzeit der Satellitensignale [6]. Man weiß, dass sich elektromagnetische Wellen beim Durchgang der Ionosphäre umgekehrt proportional ihrer Frequenz zum Quadrat (1/f 2 ) verlangsamen. Elektromagnetische Wellen mit niedrigen Frequenzen werden demnach stärker verlangsamt als solche mit hohen Frequenzen. Schickt man KAPITEL 2. THEORETISCHE GRUNDLAGEN 10 nun zwei Signale mit unterschiedlichen Frequenzen (ein hochfrequentes und ein niederfrequentes Signal) aus, kann aufgrund des Laufzeitunterschiedes am Empfänger ermittelt werden, wie stark sich die Laufzeitverzögerung in der Ionosphäre auf das Signal auswirkt. Somit ist man in der Lage die Ungenauigkeit, hervorgerufen durch die Ionosphäre, mathematisch zu kompensieren [6]. Neben der Ionosphäre können Laufzeitverzögerungen auch in der Troposphäre auftreten. Troposphärenfehler entstehen durch die Brechung elektromagnetischer Wellen. Ursache dafür sind die durch unterschiedliche Wetterlagen bedingten unterschiedlichen Wasserdampfkonzentrationen in der Troposphäre. Da die Laufzeitverzögerung in der Troposphäre nicht frequenzabhängig ist, stellt die Troposphäre ein nicht-dispersives Medium dar, und der verursachte Fehler lässt sich nicht herausrechnen [6]. Durch Einführung von WAAS und EGNOS ist es möglich, Karten“ mit ” dem Einfluss der Atmosphäre (Ionosphäre) auf bestimmte Gebiete zu erstellen und diese Korrekturdaten an die Empfänger zu senden. Dadurch wird die Genauigkeit deutlich erhöht [6]. 2.2.4 Satelitenumlaufbahn Ein Maß für die Genauigkeit der Positionsbestimmung ist auch die Position der Satelliten, die an der Positionsbestimmung beteiligt sind, zueinander und zum Empfänger. Man spricht von der so genannten Satellitengeometrie“ [3]. ” Sind zum Beispiel vier Satelliten an der Positionsbestimmung beteiligt und befinden sich alle vier Satelliten nördlich vom Empfänger, so spricht man von einer schlechte Geometrie“. Dies kann sogar dazu führen, dass ” unter Umständen gar keine Positionsbestimmung zustande kommen kann, wenn alle Entfernungsmessungen aus der gleichen Richtung erfolgen. Wenn der Empfänger trotzdem eine Positionsbestimmung durchführen kann, so kann der Fehler im Bereich von 100 bis 150 Metern liegen, da die schlechte“ ” Satellitengeometrie alle anderen Fehler die bei der Positionsbestimmung auftreten vervielfacht [2]. Je besser die Satelliten zur Positionsbestimmung allerdings, vom Empfänger aus gesehen, über den Himmel verteilt sind, desto genauer wird auch die Bestimmung des Standorts. Angenommen der Winkel zwischen den vier Satelliten, die zur Bestimmung der Position herangezogen werden, beträgt jeweils 90◦ , so ist die Satellitengeometrie“ sehr gut, da die Entfernungsmes” sungen in allen Richtungen gemacht werden [2]. Wie man in der Abbildung 2.4 erkennen kann, befinden sich die Satelliten zur Positionsbestimmung in einer günstigen“ Anordnung (siehe Abb.), das ” heißt der Winkel der Sichtlinien zwischen GPS-Empfänger und den beiden Satelliten beträgt 90◦ . Nachdem die Laufzeit aufgrund von Uhrenungenauigkeit, Mehrwegeffekt und atmosphärische Störungen nicht exakt bestimmt werden, ist auch die Positionsbestimmung ungenau, was durch die grauen 11 KAPITEL 2. THEORETISCHE GRUNDLAGEN Abbildung 2.4: Günstige“ ” http://www.kowoma.de/. Anordnung zweier Satelliten. Quelle: Bereiche um die Laufzeitkreise“ dargestellt wird. Durch die Ungenauigkeit ” wird aus der exakten Empfängerposition im Schnittpunkt A eine Schnittfläche (blau) zwischen den beiden grauen Bereichen der Satelliten. Diese Schnittfläche beschreibt die mögliche Position des Empfängers und ist aufgrund der guten“ Satellitengeometrie relativ klein [2]. ” Befinden sich die beiden Satelliten näher beieinander, so verkleinert sich der Winkel der Sichtlinien zwischen Satelliten und Empfänger (siehe Abb. 2.5), aufgrund der schlechten“ Satellitengeometrie wird die Schnittfläche, welche ” die mögliche Position des Empfängers beschreibt, größer, was zur Folge hat, dass die Positionsbestimmung ungenauer wird [2]. Wird eine Positionsbestimmung in einem Fahrzeug durchgeführt oder in unmittelbarer Umgebung hoher Gebäude, so verschlechtert dies meistens die Satellitengeometrie. Nachdem in der Nähe von hohen Gebäuden ein Großteil des Himmels verdeckt ist, fallen einige Satelliten zur Positionsbestimmung weg. Falls mit den restlichen Satelliten eine Positionsbestimmung noch möglich ist, so ist diese meistens sehr ungenau. Viele Geräte zeigen ein Maß für die Genauigkeit der Messwerte an, die meist ein Kombinationswert verschiedener Faktoren ist und über deren genaue Berechnung die Hersteller nur ungern Auskunft geben. Für die Güte“ der Satellitengeometrie sind die ” DOP-Werte (dilution of precision) sehr verbreitet. Je nachdem, welche Daten bei der Berechnung herangezogen werden unterscheidet man zwischen verschiedenen DOP-Werte: • Geometric Dilution Of Precision (GDOP) Gesamtgenauigkeit 3D-Koordinaten und Zeit KAPITEL 2. THEORETISCHE GRUNDLAGEN 12 Abbildung 2.5: Ungünstige“ Anordnung zweier Satelliten. Quelle: ” http://www.kowoma.de/. • Positional Dilution Of Precision (PDOP) Positionsgenauigkeit 3D-Koordinaten • Horizontal Dilution Of Precision (HDOP) Horizontalgenauigkeit • Vertical Dilution Of Precision (VDOP) Vertikalgenauigkeit • Time Dilution Of Precision (TDOP) Zeitgenauigkeit 2D-Koordinaten Höhe Zeit Die HDOP Werte beschreiben, wie weit sich die Satelliten über dem Horizont befinden. Je höher der Winkel zwischen Horizont, Empfänger und Satellit ist, desto schlechter ist auch der HDOP Wert. VDOP Werte hingegen sind eher schlechter, wenn sich die Satelliten sehr nahe am Horizont befinden. Für die Positionsgenauigkeit (PDOP) ist es von Vorteil, wenn sich ein Satellit genau über dem Empfänger befindet und alle weiteren Satelliten zur Positionsbestimmung gleichmäßig über den Horizont verteilt sind. Der GDOP-Wert bildet die Summe aller DOP-Werte. Damit man von einer KAPITEL 2. THEORETISCHE GRUNDLAGEN 13 guten“ Satellitengeometrie und einer dementsprechend guten Positionsbe” stimmung sprechen kann, sollte der Wert für GDOP nicht größer als fünf sein. Informationen über die aktuellen Werte von PDOP, HDOP und VDOP Werte kann man aus dem NMEA-Datensatz $GPGSA auslesen [2]. Wie schon vorher erwähnt wurde, verursacht die Satellitengeometrie keine Fehler in der Positionsbestimmung. Nur werden alle Fehler bei der Standortbestimmung durch eine schlechte“ Satellitengeometrie und somit ” durch schlechte DOP-Werte vervielfacht. Je höher die DOP-Werte sind, desto schlechter ist die Satellitengeometrie und dementsprechend größer werden die Fehler in der Positionsbestimmung [2]. Zusätzlich zur Position der Satelliten zum GPS-Empfänger spielen auch die Umlaufbahnen der Satelliten eine Rolle bei der Positionsbestimmung, denn obwohl die GPS-Satelliten sich in sehr präzisen Umlaufbahnen befinden kommt es zu leichten Schwankungen durch Gravitationskräfte. So beeinflussen Sonne und Mond die Bahnen geringfügig. Die exakten Bahndaten werden jedoch regelmäßig kontrolliert und auch korrigiert und in den Ephemeridendaten zu den Empfängern gesandt. Dadurch bleibt der für die Positionsbestimmung resultierende Fehler mit ca. 2 Metern sehr gering [2]. 2.2.5 Uhrengenauigkeit Eine weitere Fehlerquelle ist, trotz der Synchronisierung der Uhr während der Positionsbestimmung auf die Zeit der Satelliten, die verbleibende Ungenauigkeit der Empfänger-Uhr. Die verbleibende Uhrenungenauigkeit der Satelliten macht einen Fehler von ca. 2 Metern aus. Rundungs- und Re” chenfehler“ der Empfänger bewirken etwa einen 1 Meter Ungenauigkeit [3]. In den Satelliten des Navigationssystems kann man relativistische Effekte nachweisen, die ein starkes Indiz für die Relativitätstheorie sind. Der größte Effekt wird dabei von der speziellen Relativitätstheorie vorhergesagt, nach der eine Uhr, die sich in einem Satelliten mit einer hohen Geschwindigkeit um die Erde bewegt, langsamer geht als eine unbewegt Uhr. Dieser Effekt macht immerhin einen Zeitfehler von etwa 7,2 Mikrosekunden (1 Mikrosekunde = 10−6 Sekunden) pro Tag aus und ist mit den Atomuhren der GPS-Satelliten leicht messbar [3]. Die allgemeine Relativitätstheorie sagt nun aber zudem, dass die Zeit umso langsamer vergeht, je stärker das Gravitationsfeld ist, dem man ausgesetzt ist. Dieser Effekt führt dazu, dass ein Beobachter auf der Erde die Uhr des Satelliten, welcher einem geringeren Erdgravitationsfeld ausgesetzt ist, als der Beobachter, als zu schnell empfindet. Und dieser Effekt ist etwa sechsmal so groß wie die durch die Geschwindigkeit hervorgerufene Zeitdilatation [3]. Addiert man die beiden Effekte so scheinen die Uhren in den Navigationssatelliten insgesamt schneller zu laufen als die Uhren auf der Erde. Die Zeitverschiebung zum Beobachter auf der Erde wäre etwa 38 Mikrosekun- KAPITEL 2. THEORETISCHE GRUNDLAGEN 14 den pro Tag und würde zu einem Gesamtfehler von etwa 10 Kilometern pro Tag führen. Um dem Fehler entgegenzuwirken wurde einfach die Taktfrequenz der Atomuhren in den Satelliten von ursprünglich 10.23 MHz auf 10.229999995453 Mhz umgestellt. Aufgrund der Annahme bei der Positionsbestimmung, dass die Atomuhren noch immer mit einer Taktfrequenz von 10.23 MHz arbeiten, werden die relativistischen Effekte kompensiert [3]. 2.3 NMEA Übertragungsprotokoll Zur Vereinheitlichung des Datenformats bei der Übertragung von Positionsdaten hat die National Marine Electronics Association (NMEA) das NMEA0183 Format definiert welches bei vielen externen GPS-Empfängern verwendet wird um die Positionsdaten an andere Geräte zu übermitteln. Neben dem NMEA-0183 Format gibt es auch noch die Formate NMEA-0180 und NMEA-0182, allerdings werden diese nicht mehr verwendet und sind daher nicht von Bedeutung. Seit 2000 ist die Weiterentwicklung des NMEA-0183 Standards, das NMEA-2000 Format, im Einsatz. Trotz der größeren Übertragungsgeschwindigkeit und der Verwendung des CAN-Bussystems konnte sich der NMEA-2000 Standard, aufgrund der großen Anzahl an Geräten die noch immer NMEA-0183 unterstützen, noch nicht durchsetzen [3]. Bei der Übertragung ist prinzipiell nur ein Sender (talker ) und ein beziehungsweise mehrere Empfänger (listener ) vorgesehen. Sollen die Daten verschiedener Sender gleichzeitig ausgelesen werden, so muss ein Multiplexer die parallel eintreffenden Datenströme in einen seriellen Datenstrom umwandeln, welcher dann ausgelesen werden kann [3]. Die Datenübertragung verläuft in kleinen Dateneinheiten, den so genannten sentences, welche maximal 80 Zeichen lang sein dürfen. Die Daten der sentences werden im ASCII-Format (American Standard Code for Information Interchange) übertragen und können aus allen druckbaren Zeichen, sowie Carriage-Return (CR) und Line-Feed (LF), bestehen [3]. Jede Dateneinheit beginnt mit dem Zeichen $ und einer zwei Zeichen langen Senderkennung (zum Beispiel: GP für GPS-Empfänger oder LC für Loran-C Empfänger, einem älteren Positionsbestimmungssystem). Drei weitere Zeichen stellen die Kennung der Dateneinheit dar. Anschließend folgt eine Reihe von Datensätzen, welche jeweils mit einem Komma voneinander getrennt werden. Abschließend wird der Dateneinheit noch eine Prüfsumme hinzugefügt und mit einem CR/LF abgeschlossen. Ist ein Datensatz in einer Dateneinheit zwar vorgesehen aber nicht verfügbar, so wird er weggelassen, das dazugehörige Komma zur Trennung der Datensätze wird aber ohne Leerzeichen beibehalten. Durch ihre Position in einer Dateneinheit werden die Datensätze deshalb genau definiert [3]. Kapitel 3 Prototypen 3.1 Spielprinzip Grundsätzlich war geplant, dass sich die zu entwickelnden Prototypen im Spielprinzip unterscheiden sollten. Da es sich in beiden Fällen allerdings um ein positionsabhängiges Spiel handeln sollte, stellte sich schnell heraus, dass diese Idee nicht wirklich umsetzbar ist. Wie der Name schon sagt, wirkt sich bei positionsabhängigen Spielen die reale Position des Spielers auf das Spielgeschehen aus. Daraus folgt, dass jede Standortänderung direkt ins Spiel einfließt, was wiederum zur Folge hat, dass man ständig auf der Suche nach einer neuen Position ist um das gewünschte Spielziel zu erreichen. In jedem positionsabhängigen Spiel besteht daher das Spielprinzip zu einem Teil daraus, einen Standort zu finden um das Spielziel zu erreichen. Je nach Rahmenhandlung des Spiels nimmt die Positionsfindung einen mehr oder minder starken Anteil des Spielprinzips in Anspruch. So ist es zum Beispiel bei einer Schnitzeljagd unabdingbar die einzelnen Zwischenstationen zu erreichen um das Spiel zu beenden. Der Spieler befasst sich während der gesamten Spielzeit nur mit dem Problem der Positionsfindung und kann in keiner anderen Weise als durch eine Standortänderung auf das Spielgeschehen einwirken. Ein anderer Ansatz wäre, dem Spieler noch zusätzliche Möglichkeiten zur Interaktion im Spiel zu gewähren um dem Aspekt der Positionsfindung eine eher untergeordnete Rolle zuzuordnen. Aus den oben genannten Überlegungen wurde schnell klar, dass beide Prototypen in ihrem Spielprinzip zwar ähnlich sind, die Positionsfindung aber eine unterschiedliche Relevanz beim Erreichen des Spielziels einnehmen sollte. Die erste Spielidee die umgesetzt werden sollte, ist eine Portierung des Kinderspiels Topfschlagen“. Bei diesem Spiel werden dem Spieler die Augen ” verbunden. Anschließend muss der Spieler blind einen Topf finden den seine Mitspieler vorher an einer zufälligen Position im Raum verkehrt auf den 15 KAPITEL 3. PROTOTYPEN 16 Boden gestellt haben. Die Mitspieler geben dabei Hinweise wie warm“ oder ” kalt“ wenn sich der Spieler dem Topf nähert oder sich von diesem entfernt. ” In der mobilen Version des Spiels wird am Endgerät per Zufall eine Position in einem selbst gewählten Spielgebiet bestimmt. Der Spieler muss nun versuchen diese Position so schnell wie möglich zu finden. Die Annäherung an den gesuchten Punkt wird dabei am Display, wie in der klassischen Variante, mit den Begriffen warm“ und kalt“ dargestellt. ” ” Der zweite Prototyp ist ebenfalls eine mobile Umsetzung des Spiels Crossgolf. Crossgolf ist eine Variante des herkömmlichen Golfes, es wird allerdings nicht auf einem Golfplatz sondern in jeder erdenklichen Umgebung gespielt. Hat man sich ein Ziel ausgesucht, genügen ein Schläger und ein Golfball und man versucht das ausgewählte Ziel mit möglichst wenig Schlägen zu erreichen. Ein fixes Regelwerk gibt es bei diesem Spiel nicht, weshalb sich seit der Erfindung des Crossgolf schon einige Varianten gebildet haben. Die mobile Version der Spielidee ist eine Mischung aus Crossgolf und gewöhnlichem Golf. Auf dem Display sieht der Spieler dabei einen virtuellen Golfplatz und muss versuchen den Ball mit möglichst wenigen Schlägen einzulochen. Erschwert wird das Spiel dadurch, dass die Position des Abschlags, des Lochs und die des Balls auf die reale Umgebung, in der sich der Spieler zum Zeitpunkt des Spiels befindet, übertragen werden. Nachdem der Spieler den Ball virtuell abgeschlagen hat, muss er bevor er weiterspielen kann zuerst zur realen Position des neuen Abschlags gelangen. Die Schwierigkeit besteht nun darin neben den virtuellen Hindernissen wie Wasser, Wälder oder Sandgruben auch die reale Umgebung in das Spiel mit einzuplanen. 3.2 Technische Anforderungen Eine der Überlegungen bei der Realisierung der beiden Prototypen war es, eine möglichst große Anzahl an Mobiltelefonen zu unterstützen. Aufgrund der Vorgaben durch die Spiele selbst (Kartengrößen, die Verwendung der Bluetooth-Schnittstelle, etc.) ergaben sich folgende technische Anforderungen. 3.2.1 Displaygröße Wie im Abschnitt 3.8 noch beschrieben wird, sind die Spielfelder beim Mobile Crossgolf als Bilddateien gespeichert. Bei den Bildern war die Vorgabe, dass sie eine maximale Breite von 176 Pixel nicht überschreiten dürfen. Die Höhe der Bilder, und somit die Länge des Spielfelds war nicht vorgegeben. Ist die Auflösung des Displays größer als die des Spielfeldes, wird das Bild horizontal und vertikal zentriert dargestellt und ein grauer Rahmen um das Spielfeld gezeichnet. Nachdem das Spielfeld zwar vertikal aber nicht horizontal scrollbar ist, muss das Display des Mobiltelefons mindestens eine Breite KAPITEL 3. PROTOTYPEN 17 Abbildung 3.1: Spielfelddarstellung auf unterschiedlichen Displays (v.l.n.r.: 132×176 Pixel, 176×220 Pixel, 240×320 Pixel). von 176 Pixeln haben um die komplette Breite des Spielfelds darstellen zu können (Abb. 3.1). Da für den Mobile Potbanging Prototypen nur zwei Textzeilen anzuzeigen sind, ergaben sich keine weiteren Einschränkungen bezüglich der Displaygröße. Für die Tests wurden neben den spielrelevanten Informationen allerdings auch noch Zusatzinformationen zur Positionsbestimmung angezeigt, weshalb sich eine minimale Display-Höhe von 220 Pixeln bewährt hat. 3.2.2 Bluetooth Wie schon im Abschitt 1.2 erwähnt wurde, wird für die Ermittlung der Positionsdaten ein externer GPS-Empfänger verwendet, da nur wenige Mobiltelefone einen integrierten GPS-Empfänger besitzen. Kabelgebundene Empfänger besitzen meistens einen PS/2-Schnittstelle oder einen USB-Anschluss um die Positionsdaten auszulesen. Da keine der beiden Schnittstellen bei einem Mobiltelefon üblicherweise ausgeführt sind, fiel die Wahl auf einen kabellosen GPS-Empfänger, bei dem man die Daten über eine Bluetooth-Schnittstelle auslesen kann. Um in einer J2ME-Anwendung Zugriff auf die Bluetooth-Schnittstelle zu bekommen, muss das Mobiltelefon den Java Specification Request 82 (JSR82) unterstützen, welcher eine Programmierschnittstelle zur Kommunikation mit Bluetooth-Geräten implementiert. 3.2.3 Dateiverwaltung Ein Problem war die Datenerfassung während der Testphase. Nachdem die Spiele nur im Freien getestet werden können, da man eine direkte Sicht- KAPITEL 3. PROTOTYPEN 18 verbindung zwischen dem GPS-Empfänger und den Satelliten benötigt, ist es unmöglich eine Testumgebung mit Videoaufzeichnung aufzubauen. Auch das Abfilmen der Mobiltelefonbildschirme ist aufgrund der kleinen Displays und der Reflexionen wieder verworfen worden. Die Darstellungen am Mobiltelefon direkt als Video oder Einzelbildfolge abzuspeichern ist aufgrund der großen Datenmenge ebenfalls nicht möglich, zusätzlich würden die Daten zur Position des Spielers verloren gehen, da diese dem Spieler nicht direkt sichtbar gemacht werden. Um die Tests trotzdem reproduzierbar zu machen, werden für jeden Testlauf alle eingehenden Positionsdaten und Benutzerinteraktionen in einer eigenen Datei abgespeichert. Über die Verwendung von Zeitstempeln kann die genaue Abfolge eines Testlaufs nachgebildet werden. Wie schon bei der Bluetooth-Schnittstelle (siehe 3.2.2) ist auch den Zugriff auf das Dateisystem eines Mobiltelefons über einen Java Specification Request (JSR-75) geregelt. Durch das File Connection Optional Package (FCOP) welches im JSR-75 implementiert ist, wird es möglich von einer J2ME-Anwendung direkt auf das Dateisystem des Mobiltelefons zuzugreifen um dort Dateien zu erstellen oder auszulesen. Nach Beendigung der Testläufe wurden die archivierten Dateien vom Mobiltelefon auf den Computer übertragen und anschließend ausgewertet. 3.2.4 J2ME-Laufzeitumgebung Die Basis zur Entwicklung eine Java-Applikation auf einem mobilen Endgerät bildet die Java 2 Micro Edition. Innerhalb der J2ME beschreiben Konfigurationen und Profile die verschiedenen Verwendungsgebiete der J2ME. Die Connection Limited Device Configuration (CLDC) beschreibt die Mindestanforderungen der Hardware mobiler Endgeräte und ist in den Versionen 1.0 und 1.1 verfügbar. Der größte Unterschied zwischen CLDC 1.0 und CLDC 1.1 besteht in der Verwendung von Fließkommazahlen, welche nur in der CLDC 1.1 verfügbar sind. Das Mobile Information Device Profile (MIDP) bietet die Funktionalität zur Ansteuerung des Displays und der Eingabemöglichkeiten (Touchscreen oder Tastatur) auf einem Mobiltelefon und liegt in den Versionen 1.0 und 2.0 vor (siehe [7, Kap. 2]). Da die Positionsdaten als Fließkommazahlen übertragen werden, wurde der Einfachheit halber auf eine Umrechnung in Festkommazahlen verzichtet. Damit die Testapplikationen auf Mobiltelefonen lauffähig sind, müssen diese mit der CLDC 1.1 und mit dem MIDP 2.0 kompatibel sein. Es besteht zwar kein direkter Zusammenhang zwischen Konfiguration und Profil, jedoch unterstützen fast alle CLDC 1.1 konformen Mobiltelefone das MIDP 2.0. 3.2.5 Testumgebung Unter Berücksichtigung der technischen Anforderungen und aufgrund der Verfügbarkeit fiel die Wahl auf folgendes Mobiltelefon (Abb. 3.2): KAPITEL 3. PROTOTYPEN 19 Abbildung 3.2: Sony Ericsson v630i. Quelle: http://www.telyou.ro/. Sony Ericsson v630i • Display Auflösung Art Farben 176x220 Pixel TFT 262.144 • Java-Umgebung Version Speicher Programmgröße JSR CLDC 1.1, MIDP 2.0 dynamisch keine Angaben JSR-82 JSR-75 • Konnektivität Bluetooth Infrarot Serielle Schnittstelle USB ja nein nein ja, mit speziellem Kabel und Treiber Für die Positionsbestimmung wurde der BT-328 Bluetooth-GPS-Empfänger (Abb. 3.3) von Navilock verwendet. Dieser zeichnet sich durch die lange Betriebszeit, von 16 Stunden nach Vollladung im Dauerbetriebsmodus, und den geringen Kosten aus. Navilock BT-328 • Generelle Spezifikation KAPITEL 3. PROTOTYPEN 20 Abbildung 3.3: Navilock BT-328 Bluetooth GPS-Empfänger. Quelle: http://www.pdashop-bg.com/. – Chipsatz SiRF Star GSC2 – Empfindlichkeit -155dBm – Frequenz L1, 1575.42MHz – C/A Code 1.023 MHz Chiprate – Kanäle 12 Satelliten max. gleichzeitig empfangbar • Genauigkeit – Position Horizontal 10 Meter 2D RMS und 5 Meter 2D RMS – Geschwindigkeit 0.1m/sec 95 – Zeit 1us taktweise zur GPS Zeit • Erfassungszeit – Neuerfassung 0,1 Sek., durchschnittlich – Heißstart 8 Sek, durchschnittlich – Warmstart 38 Sek., durchschnittlich – Kaltstart 42 Sek., durchschnittlich • Protokolle – Baudrate 4.800 - 38.400 bps – Ausgabe Protokoll NMEA 0183 V2.2, GGA, GSA, GSV, RMC, GLL, op. VTG 3.3 Basisapplikation Bei beiden Spielen werden die Positionsdaten mittels eines externen GPSEmpfängers ermittelt und über eine Bluetooth-Schnittstelle an die Applikation weitergereicht. Um diese Funktionalität nicht für beide Prototypen neu KAPITEL 3. PROTOTYPEN 21 implementieren zu müssen, wurde zuerst eine Basisapplikation entwickelt, die sich um den Verbindungsaufbau mit dem GPS-Empfänger kümmert und die Positionsdaten in einem vorgegebenem Format abspeichert um sie anschließend den eigentlichen Spielen zur Verfügung stellen zu können. Abgeleitet von der Applikationsbezeichnung Applet der Java 2 Platform, Standard Edition (J2SE) werden Anwendungen für das MIDP MIDlets genannt. Ebenso wie Applets besitzen auch MIDlets einen vordefinierten Lebenszyklus, dessen Methoden zum Starten, Beenden und Pausieren der Applikation implementiert werden müssen. Für die Gestaltung der Benutzeroberfläche stellt die LCDUI-Bibliothek (Lowest Common Denominator User Interface) Funktionen zur Darstellung und zur Abfrage der Benutzereingaben zu Verfügung. Das High-LevelAPI bietet dem Benutzer einfache Interfacebausteine wie Formulare, Listen, Menüs und Textfenster zur Verwendung in der Applikation, allerdings hat der Benutzer nur wenig Einfluss auf die visuelle Gestaltung der Bausteine da diese automatisch an das Look and Feel des Endgeräts angepasst werden. Aus diesem Grund werden Spiele in der Regel mit Hilfe der Low-Level-API programmiert, da mit ihr eine pixelgenaue Darstellung am Display möglich ist. Ebenso ist in der Low-Level-API eine direkte Auswertung von Benutzereingaben, wie dem Drücken und Loslassen einer speziellen Taste möglich, wobei hingegen in der High-Level-API nur indirekt, also über Veränderungen in Formularen oder Textfenstern, auf die Eingaben zugegriffen werden können. Für die Spielvariante Topfschlagen“ würden die Möglichkeiten ” der High-Level-API zur Gestaltung der Benutzeroberfläche zwar ausreichen, doch beim Spiel CrossGolf“ werden komplexere Grafikelemente zur Darstel” lung von Wind, Schläger und Spielfeld benötigt, welche nur die Low-LevelAPI aufweist. Damit man auf dem Display zeichnen kann, stellt die Low-Level-API die Klasse Canvas zur Verfügung. Canvas ist eine abstrakte Basisklasse, welche von der Klasse Displayable abgeleitet ist. Grundsätzlich kann immer nur ein Displayable-Objekt am Bildschirm angezeigt werden, hat man zum Beispiel mehrere MIDlets parallel laufen so wird immer nur das MIDlet grafisch dargestellt, welches im Moment aktiv ist. Alle anderen MIDlets befinden sich zu diesem Zeitpunkt, wie schon oben beschrieben, in einem Pause-Zustand und werden nicht gezeichnet. Für das Zeichnen der Benutzeroberfläche ist in der Canvas-Klasse die Methode paint(Graphics g) vorgesehen. Das an die paint ()-Methode übergebene Graphics-Objekt beschreibt den Bildausschnitt auf dem gezeichnet werden soll und muss nicht zwingend mit der Displaygröße identisch sein. Des Weiteren sind in dem Graphics-Objekt Methoden zum Zeichnen von Text, Linien und Kreisen implementiert. Um die Eingaben von Tastatur und berührungssensitiven Bildschirm (Touchscreen) auswerten zu können, gibt es in der Canvas-Klasse verschiedene Methoden die je nach Zustand der Eingabe aufgerufen werden. keyPressed(int keyCode) wird aufgerufen, sobald eine Taste gedrückt wurde. KAPITEL 3. PROTOTYPEN 22 Abbildung 3.4: Aufbau der Basisapplikation mit Bluetooth-Unterstützung. Der Parameter keyCode gibt an um welche Taste es sich handelt. Der keyCode ist für Zahlentasten, Stern, Raute und den Pfeiltasten bei allen Mobiltelefonen gleich, nur die Softkeys sind geräteabhängig. Neben der keyPressed(int keyCode)-Methode gibt es auch eine keyReleased(int keyCode)-Methode die aufgerufen wird, wenn eine Taste losgelassen wird. Wird eine Taste über einen längeren Zeitraum gedrückt (geräteabhängig, in der Regel allerdings 2-3 Sekunden), so wird die Methode keyRepeated(int keyCode) aufgerufen. Verfügt das Mobiltelefon über einen Touchscreen so stellt auch hierfür die Canvas-Klasse geeignete Methoden zur Auswertung der Interaktion zu Verfügung. pointerPressed(int x, int y) wird aufgerufen wenn auf den Touchscreen getippt wird. Die Variablen x und y geben dabei die Position am Bildschirm an, die angetippt wurde. Nach dem Tippen wird die Methode pointerReleased(int x, int y) aufgerufen. Um ein Drag and Drop-Event (Ziehen und Loslassen) auswerten zu können, gibt es die Methode pointerDragged(int x, int y), welche dann aufgerufen wird, wenn ein Ziehvorgang gestartet wurde. Die Position x, y gibt dabei den Startpunkt des Drag and Drop-Events an. In Abbildung 3.4 ist nun der gesamte Aufbau der Basisapplikation inklusive Bluetooth-Unterstützung ersichtlich. Beim Starten des MIDlets Blue KAPITEL 3. PROTOTYPEN 23 \-tooth wird das Canvas initialisiert und als aktives Darstellungsobjekt festgelegt, wie in folgendem Programmcode ersichtlich ist: protected void startApp () throws MIDletStateChangeException { this . canvas = new BluetoothCanvas ( true ) ; this . canvas . start () ; this . display . setCurrent ( canvas ) ; } canvas.start() startet die eigentliche Spielschleife, welche in einem ei- genen Thread läuft. In der Spielschleife werden pro Durchlauf alle spielrelevanten Daten aktualisiert und anschließend die Anzeige neu gezeichnet. Damit beide Spiele im Vollbildmodus arbeiten muss die Methode canvas .setFullScreenMode(true) einmalig aufgerufen werden. Im ersten Prototypen wurde die Methode setFullScreenMode(true) im Konstruktor der canvas-Klasse aufgerufen. Das führte dazu, dass bei mehreren Mobiltelefonen des Herstellers Nokia nicht die komplette Bildschirmgröße zur Verfügung stand – graue Balken waren sichtbar. Nach einigen Tests konnte der Fehler behoben werden, indem die Methode setFullScreenMode(true) beim erstmaligen Aufruf der paint()-Methode ausgeführt wurde. Wird die Methode setFullScreenMode(true) mehrfach, also bei jedem Aufruf der paint ()-Methode, ausgeführt, so führt das zu einem Programmabsturz weshalb folgende Lösung implementiert wurde: protected void paint ( Graphics g ) { if ( this . isInitialized ) { // ... Aktualisierung der Anzeige } else { setFullScreenMode ( true ) ; initialize ( getWidth () , getHeight () ) ; } } private void initialize ( int width , int height ) { this . isInitialized = true ; // ... Initialisierung der Canvas - Klasse } 3.3.1 Bluetooth-Unterstützung Bei mobilen Endgeräten mit Java-Unterstützung bietet die Java-API JSR-82 Funktionen zum Aufbau einer Bluetooth-Verbindung. Damit also ein Spiel überhaupt auf einem Mobiltelefon gespielt werden kann, ist es zwingend erforderlich, dass das gewünschte Telefon auch die JSR-82 implementiert hat. KAPITEL 3. PROTOTYPEN 24 Abbildung 3.5: Beobachter-Entwurfsmuster. Um eine Verbindung mit einem Bluetooth-Gerät aufbauen zu können, muss zunächst überprüft werden ob das gewünschte Gerät in Reichweite ist. Dazu bietet die JSR-82 einen so genannten DiscoveryAgent, welcher die Suche nach Bluetooth-Geräten und deren Dienste unterstützt. Damit die Applikation nicht durch die Suche blockiert wird, teilt der DiscoveryListener der Anwendung mit wenn ein Gerät oder ein Dienst gefunden wurde. Über die Funktion startInquiry(int accessCode, DiscoveryListener listener) des DiscoveryAgent wird eine neue Suche nach Geräten getriggert. Der accessCode beschreibt dabei den Sichtbarkeitsmodus der Geräte die gefunden werden sollen, dabei unterscheidet man zwischen nicht sichtbaren, allgemein sichtbaren (General Unlimited Inquiry Access Code - GIAC) und für spezielle Anfragen (Limited Dedicated Inquiry Access Code - LIAC) sichtbare Geräte. Ebenfalls an die Funktion startInquiry wird der Listener übergeben, welcher benachrichtigt wird, wenn ein Gerät gefunden wurde. Die Callback-Funktion deviceDiscovered(RemoteDevice device, DeviceClass devClass) im DeviceListener wird aufgerufen, sobald ein Bluetooth-Gerät gefunden wurde. In device ist das gefundene Gerät gespeichert, sowie dessen Name und Bluetooth-Addresse. Die Variable devClass gibt an um welche Geräte-Klasse es sich handelt und welche Services dieses Gerät unterstützt. Ist die GeräteSuche abgeschlossen oder abgebrochen worden, so wird die Callback-Funktion inquiryCompleted(int discType) im Listener aufgerufen. Die Variable discType gibt dabei an ob die Suche erfolgreich abgeschlossen, durch einen Fehler oder durch den Anwender abgebrochen wurde. Sowohl die Gerätesuche als auch die Dienstsuche wurde in der Klasse KAPITEL 3. PROTOTYPEN 25 BluetoothDiscovery ausprogrammiert. Mit dem Aufruf der Methode doDeviceDiscovery() wird eine neue Gerätesuche gestartet, für eine Dienstsuche muss die Methode doServiceSearch(RemoteDevice device) gestartet werden. Ein Problem bei der Realisierung der Gerätesuche war die Benachrichtigung der Klasse BluetoothCanvas sobald eine Suche beendet war, da sowohl die Gerätesuche als auch die Dienstsuche eine unbestimmte Zeit in Anspruch nehmen. Aus diesem Grund wurde ein Observer-Pattern (BeobachterEntwurfsmuster) implementiert (Siehe Abb. 3.5). Bei diesem Entwurfsmuster können sich ein oder mehrere Beobachter (Observer ) bei einem Objekt (Observable) registrieren. Jede Änderung des Objekts wird an die angemeldeten Beobachter weitergeleitet, damit diese darauf reagieren können. Im Fall der beiden Prototypen registriert sich die Klasse BluetoothCanvas bei der Klasse BluetoothDiscovery als Beobachter. Nachdem die Gerätesuche beendet und in der Klasse BluetoothDiscovery die Callbackfunktion inquiryCompleted(int discType) aufgerufen wurde, wird den Beobachtern mitgeteilt, dass die Gerätesuche beendet ist. In der Regel sollte man anschließend eine Dienstsuche bei den gefundenen Bluetooth-Geräten durchführen um zu ermitteln ob ein Gerät den gewünschten Dienst unterstützt. Da in diesem speziellen Fall allerdings die Bluetooth-Adresse des GPS-Moduls bekannt ist wird auf eine Dienstsuche verzichtet und nur überprüft ob sich das gewünschte GPS-Gerät in Reichweite befindet. Dazu wird die bekannte Bluetooth-Adresse mit den Adressen in Reichweite befindlicher Geräte verglichen. Bei einer Übereinstimmung befindet sich das GPS-Gerät in unmittelbarer Nähe und eine Verbindung kann aufgebaut werden. Da das Programm sich nicht sofort beenden soll wenn das GPS-Gerät nicht sofort gefunden werden kann, wird automatisch eine neue Geräte-Suche gestartet bis der GPS-Empfänger gefunden wird. Zur besseren Veranschaulichung dient folgender Programmcodeausschnitt: public class BluetoothCanvas extends Canvas implements Observer { // ... public void notify ( Observable o , Object arg ) { if ( o instanceof B l u e t o o t h D i s c o v e r y ) { if ((( String ) arg ) . equals ( " deviceSearch " ) ) { // ... Vergleich der gefundenen Geräte mit dem Referenzempfänger } if ( this . isDeviceFound ) { // ... Starten der B l u e t o o t h v e r b i n d u n g KAPITEL 3. PROTOTYPEN 26 } else { this . discovery . doDeviceDiscovery () ; } } } // ... } public class B l u e t o o t h D i s c o v e r y extends Observable implements DiscoveryListener { // ... public void inquiryCompleted ( int discType ) { // ... Auswertung der Gerätesuche setChanged () ; notifyObservers ( " deviceSearch " ) ; } // ... } Die Kommunikation mit dem GPS-Modul basiert auf dem Radio Frequency Communication (RFCOMM) Protokoll welches eine serielle Verbindung mit dem GPS-Empfänger simuliert. Wie Daten zwischen zwei oder mehr Bluetooth-Geräten übermittelt wird, ist in dem verwendeten Profil festgelegt. Das, in der Basisapplikation verwendete, Serial Port Profile (SPP) legt fest, wie eine emulierte serielle Verbindung zustande kommt. Die Daten werden dabei über das zuvor erwähnte RFCOMM-Protokoll übermittelt und liegen in dem zuvor genannten NMEA-0183 Format vor (siehe 2.3), da es sich bei dem angeschlossenen Gerät um einen GPS-Empfänger handelt. Der Programmcode für den Verbindungsaufbau zwischen Bluetooth-GPSEmpfänger und der J2ME-Applikation wurde in die Klasse BluetoothConnection ausgelagert. Der einzige Paramter für die Erstellung einer Bluetoothverbindung ist die Bluetoothadresse des GPS-Empfängers, welche als String an den Konstruktor übergeben werden muss. Aus dem Protokoll, der Bluetoothadresse und einer Portnummer wird im Konstruktor eine URL (Uniform Resource Locator, engl. einheitlicher Quellenanzeiger“) ” für den Verbindungsaufbau mit dem Empfänger erstellt: public class B l u e t o o t h C o n n e c t i o n { // ... KAPITEL 3. PROTOTYPEN 27 private String btAddress = null ; public B l u e t o o t h C o n n e c t i o n ( String btAddress ) { this . btAddress = " btspp :// " + btAddress + " :1 " ; } // ... } Die Methode connect() öffnet eine StreamConnection mit dem GPSEmpfänger und über einen InputStreamReader können die Daten, welche der Empfänger sendet, byteweise ausgelesen werden. StreamConnection und InputStreamReader sind Komponenten des Generic Connection Framework, welches eine protokollunabhängige Schnittstelle für den Verbindungsaufbau zwischen zwei Geräten bietet. Ist eine Verbindung aufgebaut kann über die Funktion public synchronized int read () throws IOException { return this . btStreamReader . read () ; } Daten aus dem Stream ausgelesen werden. Über den Aufruf von discon\nect() wird der InputStreamReader und die StreamConnection geschlossen und die Verbindung somit beendet. 3.3.2 GPS-Anbindung Nachdem die Bluetoothverbindung aufgebaut wurde, müssen die vom GPSEmpfänger gesendeten Daten eingelesen und weiterverarbeitet werden. Wie in den Spezifikationen des Navilock BT-328 GPS-Empfängers ersichtlich ist (siehe 3.2.5), wird neben dem RMC-Datensatz auch die Datensätze GGA, GSA, GSV, GLL und VTG übertragen. Da für die Spieleprototypen nur der RMC-Datensatz von Bedeutung ist, müssen die restlichen Datensätze aus dem empfangenem Datenstrom gefiltert werden. Anschließend wird der RMC-Datensatz in seine Komponenten zerteilt und in einem Zwischenspeicher gespeichert, bis dieser von den Spielen ausgelesen wird. Da der Empfang und die Auswertung der empfangenen GPS-Daten unabhängig von den Spielabläufen ist, wird ein eigener Thread ausschließlich für die GPS-Verwaltung gestartet. Durch Variation der Dauer in welcher der GPS-Thread blockiert ist kann man indirekt auf die Anzahl der empfangenen Datensätze pro Minute Einfluss nehmen und ist somit für die Auswertung der Spielbarkeit beider Prototypen von großer Bedeutung. Die Klasse GPS dient als Schnittstelle zwischen den eigentlichen Spielen und der GPS-Verwaltung. Über die Funktion getRecord() können die Spiele den aktuellen RMC-Datensatz auslesen. Bevor allerdings ein Datensatz vorliegt muss die Methode start() aufgerufen werden, welche den GPS-Thread KAPITEL 3. PROTOTYPEN 28 initialisiert und eine Verbindung mit dem Bluetooth-GPS-Empfänger aufbaut. Bei jedem Durchlauf des Threads wird zuerst der Datenstrom empfangen, bis ein Zeilenumbruch übertragen wird. Da der Zeilenumbruch im Anschluss an das Ende eines NMEA-Datensatzes übertragen wird, allerdings nicht zum Datensatz dazugehört, wird dieser aus dem erstellten Datenstring wieder entfernt: String output = new String () ; int input ; while (( input = this . connection . read () ) != LINE_DELIMITER ) { output += ( char ) input ; } output = output . substring (1 , output . length () - 1) ; Mit einem Parser wird der Datenstring anschließend in die einzelnen Komponenten (Längengrad, Breitengrad, Uhrzeit,...) unterteilt, welche anschließend als ein vollständiger Datensatz in den Zwischenspeicher abgelegt werden. Da jede Komponente eine fixe Position im Datenstring hat und das Komma als Standard zur Trennung der Komponenten verwendet wird, kann der Datenstring schnell in die Komponenten zerteilt werden. Bevor der Datenstring allerdings aufgeteilt wird, muss sichergestellt sein, dass es sich bei dem String um einen RMC-Datensatz handelt. Ein Flag im RMC-Datensatz beschreibt zusätzlich den Status des GPS-Empfängers. Ist das Flag auf A“ ” gesetzt, so handelt es sich dabei um einen Datensatz mit korrekten Daten. V“ weist auf einen möglichen Fehler in den empfangen Daten hin, aus die” sem Grund wird vom Parser nur ein korrekter Datensatz an die GPS-Klasse zurückgeliefert. Wurde ein falscher Datensatz empfangen oder sind die Daten nicht korrekt, wird null ausgegeben. Der folgende Codeabschnitt liefert einen Ausschnitt des Parsers und zeigt das Aufteilen des Datenstrings in die ersten Komponenten und die Auswertung der Korrektheit der Daten: public static GPSRecord parse ( String record ) { if ( record . startsWith ( " GPRMC " ) == true ) { String currentValue = record ; int nextTokenIndex = currentValue . indexOf ( DELIMETER ); currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Date time of fix nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String dateTimeOfFix = currentValue . substring (0 , nextTokenIndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // ... KAPITEL 3. PROTOTYPEN 29 if ( warning . equals ( " A " ) == true ) { GPSRecord pos = new GPSRecord ( record , longitude , lattitude , 0 , longitudeDouble , latitudeDouble , longitudeRad , latitudeRad , speed ) ; return pos ; } return null ; } else { return null ; } } 3.4 Controller Abbildung 3.6: Controller Interface. Um den spielrelevanten Programmcode von der Basisapplikation zu trennen wurde die abstrakte Klasse Controller eingeführt (siehe Abb. 3.6). Der Controller speichert die wichtigsten Daten die für ein positionsabhängiges Spiel nötig sind, wie eine Instanz der GPS-Klasse, den letzten GPS-Datensatz sowie die Größe des Displays, ab. Eine abstrakte Methode update() wird zur Verfügung gestellt, in der die Prototypen die spieleigenen Daten aktualisieren können. Um den aktuellen Spielfortschritt am Mobiltelefon darstellen zu können, stellt der Controller eine paint()-Methode bereit in der die Benutzeroberfläche der Spiele gezeichnet werden kann. Ohne Kenntnis der eigentlichen Spiellogik leitet die Klasse BluetoothCanvas Benutzerinteraktionen an den Controller weiter und ruft die update()- und paint()-Methode des Controller in periodischen Abständen auf, wie folgender Programmausschnitt beweist: public void keyPressed ( int keyCode ) { if ( this . isDeviceFound ) { Logger . log ( " * key pressed * " + keyCode , true ) ; KAPITEL 3. PROTOTYPEN 30 this . controller . keyPressed ( keyCode , this ) ; } } Aufgrund des Controllers ist es somit beim Kompilieren des MIDlets möglich schnell zwischen den Prototypen umzuschalten, indem man je nach Bedarf die Zeilen auskommentiert. Um einen neuen positionabhängigen Spieleprototypen zu entwickeln muss einfach nur eine neue Controllerklasse implementiert werden, welche von der abstrakten Klasse Controller abgeleitet ist. Anschließend muss zusätzliche noch eine Zeile in der notify()-Methode der BluetoothCanvas-Klasse hinzugefügt werden, in der ein Objekt der neuen Controllerklasse erstellt wird. // this . controller = new P o t B a n g i n g C o n t r o l l e r ( this . width , this . height , this . gps ) ; this . controller = new C r o s s G o l f C o n t r o l l e r ( this . width , this . height , this . gps ) ; 3.5 Logger Abbildung 3.7: Logger Singleton. Wie schon im Abschnitt 3.2.3 beschrieben wurde, müssen die Spieldaten in einer externen Datei abgelegt um dann im Anschluss an den Testlauf ausgewertet zu werden. Während des Spiels wird dafür wie schon bei der Bluetooth-Anbindung eine Verbindung über das Generic Connection Framework erstellt. Sämtliche Zugriffe auf das Dateisystem laufen dabei über das Interface FileConnection ab, das von der Klasse StreamConnection abgeleitet ist. Neben den Methoden zum Öffnen eines Input- oder OutputStream besitzt das Interface FileConnection zusätzlich Funktionen zum Auflisten, Erstellen und Löschen von Dateien und Verzeichnissen. Die Laufwerksbezeichnung auf Mobiltelefonen ist nicht wie bei einem Microsoft Betriebssystem einheitlich ein Buchstabe, sondern kann je nach Hersteller auch ein längerer Name sein. Die Bezeichnungen der Dateisystemwurzeln eines Mobiltelefons kann KAPITEL 3. PROTOTYPEN 31 mit Hilfe der Methode listRoots() der Klasse FileSystemRegistry ausgelesen werden. Da von allen Stellen im Programm auf den Logger zugegriffen werden muss, ist dieser nach dem Singleton Entwurfsmuster ausgeführt worden. Es verhindert, dass von einer Klasse mehr als ein Objekt erzeugt werden kann, darüber hinaus ist die Klasse üblicherweise global verfügbar. Nicht nur aufgrund der globalen Verfügbarkeit wurde die Klasse Logger als Singleton implementiert, sondern auch wegen der Verbindung mit dem Dateisystem, um nicht bei jeder Tasteneingabe oder bei jedem Empfang eines GPSDatensatzes einen neuen OutputStream öffnen zu müssen. public static void init ( String msg ) { if ( instance == null ) { instance = new Logger ( msg ) ; } } Schon im Konstruktor des MIDlets Bluetooth wird von der Klasse Logger über den Aufruf der init(String msg)-Methode die einzige Instanz erstellt und initialisiert (siehe obigen Codeauschnitt). An die Initialisierungsmethode wird ein String übergeben. Dieser String repräsentiert den Titel der Applikation und wird als erster Eintrag in den Kopf der Log-Datei geschrieben: # PotBanging # # Mon Feb 26 16:27:28 GMT +01:00 2007 # # Profile .......... MIDP -1.0 MIDP -2.0 # Configuration .... CLDC -1.1 # # FileConnection ...1.0 # ### Um zu sehen ob das aktuell verwendete Mobiltelefon das Anforderungsprofil erfüllt, werden neben dem Titel Informationen über das Profil und die Konfiguration des mobilen Endgeräts in die Datei geschrieben. Ob das File Connection Optional Package der JSR-75 unterstützt wird, wird mit der Funktion System.getProperty("microedition.io.file.FileConnection. version") überprüft, die den String "1.0" zurückliefert sofern das Package auf dem Mobiltelefon vorhanden ist. Über den Aufruf von System.getProperty ("bluetooth.api.version") sollte es zusätzlich möglich sein die BluetoothUnterstützung des Mobiltelefons abzufragen. Aus unbekannten Gründen lieferte die Funktion allerdings immer null zurück, obwohl alle Testgeräte die JSR-82 unterstützen. Da die Information nicht aussagekräftig war wurde sie wieder aus dem Kopf der Log-Datei entfernt. Der Datumseintrag im Header dient zur Katalogisierung der Testdurchläufe. Anhand des Datums und der KAPITEL 3. PROTOTYPEN 32 Uhrzeit kann man zusätzlich auf das Wetterverhältnis während des Testlaufs Rückschlüsse ziehen, sofern der Ort des Testdurchlaufs bekannt ist. Bevor die Daten allerdings in die Datei geschrieben werden können, muss zuerst eine FileConnection über den Aufruf von Connector.open(URL) hergestellt werden. Die URL setzt sich wie bei einer Bluetooth-Verbindung aus dem Protokoll, dem Zielpfad und einer Zieldatei zusammen. Das es sich um eine FileConnection handelt, geht aus dem Protokoll-Abschnitt ("file:///" ) der URL hervor. Bei der Namensgebung der Datei gab es Schwierigkeiten, da bei einem fixen Dateinamen die Protokolldatei bei jedem Testlauf überschrieben wurde. Eine Überprüfung aller existierenden Log-Dateien und die Verwendung einer fortlaufenden Nummerierung der Dateien mittels Suffix wäre zwar möglich, jedoch wurde im Hinblick auf die Komplexität darauf verzichtet. Die Lösung des Problems war die Verwendung der Date.getTime ()-Funktion, welche die Anzahl der Millisekunden seit Mitternacht des ersten Januar 1970 (GMT) zurückliefert. Da die Zeit ständig verstreicht und nicht zwei MIDlets genau zeitgleich auf einem Mobiltelefon gestartet werden können, ist es sehr unwahrscheinlich mit einem Testlauf die Log-Datei eines vorigen Testlaufes zu überschreiben. Erst durch ein manuelles Zurücksetzen der Systemuhr am Mobiltelefon wäre es möglich Daten eines älteren Testlaufes zu überschreiben, wobei man dann mit ziemlicher Wahrscheinlichkeit von einem provozierten Fehler ausgehen kann. Um die relevaten Spieldaten in der Log-Datei abzuspeichern stellt die Klasse Logger zwei Methoden zur Verfügung. Die Methode log(String msg) fügt den an die Prozedur übergebenen String msg direkt ans Ende der LogDatei an. Durch jeden neuen Aufruf der Methode wird eine weitere Zeile in der Log-Datei geschrieben. Da allerdings kein Zeitstempel mitgespeichert wird, ist eine zeitgleiche Wiedergabe des Testlaufs nicht möglich. Aus diesem Grund wurde folgende log()-Methode implementiert: public static void log ( String msg , boolean timestamp ) { String bytes = " " ; if ( timestamp ) { Calendar currentTime = Calendar . getInstance () ; bytes += currentTime . get ( Calendar . HOUR_OF_DAY ) ; bytes += " : " ; bytes += currentTime . get ( Calendar . MINUTE ) ; bytes += " : " ; bytes += currentTime . get ( Calendar . SECOND ) ; bytes += " . " ; bytes += currentTime . get ( Calendar . MILLISECOND ) ; bytes += " " ; } bytes += msg + " \ n " ; try { os . write ( bytes . getBytes () ) ; KAPITEL 3. PROTOTYPEN 33 } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " log :: couldnt write data " ) ; } } In der oben dokumentierten Methode ist es möglich neben den Daten zusätzlich noch den Zeitpunkt abzuspeichern, an dem der Datensatz in die Datei geschrieben wurde. Da die Anzahl der Millisekunden seit Mitternacht des ersten Januar 1970 (GMT) als Zeitstempel nicht sehr aussagekräftig ist, wurde das Calendar-Objekt des java.util-Packages verwendet, welches die aktuelle Uhrzeit im Format hh:mm:ss:ms zurückliefert. An das Ende des Datensatzes wird abschließend automatisch noch ein Zeilenumbruch angehängt, damit sichergestellt ist, dass nicht mehrere Datensätze in einer Zeile stehen. 3.6 ErrorMessage Abbildung 3.8: ErrorMessage Singleton. Zu Testzwecken wurden die Prototypen in den frühen Versionen auf dem vom Wireless Toolkit zur Verfügung gestellten Emulator getestet. Da der Emulator eine Softwareumgebung ist, welche auf einem Computer ausgeführt wird, zeigt dieser bei der Abarbeitung des Programmcodes ein anderes Verhalten als auf einem realen Endgerät. Ein wesentlicher Unterschied bei Taktfrequenz und Speicher zwischen Mobiltelefon und Computer kann dazu führen, dass das MIDlet zwar im Emulator ohne Fehler ausgeführt wird, auf dem Mobiltelefon aber aufgrund von Speichermangel abstürzt. Zusätzlich ist zu erwähnen, dass der Bluetooth-GPS-Empfänger nur im Freien richtige Daten liefert, weshalb der Emulator am Computer nur mit falschen“ GPS-Daten arbeitete. Da zur Zeit nur der Hersteller SonyEricc” son die Technik bereitstellt, um direkt auf dem Mobiltelefon die Applikation in einzelnen Schritten abzuarbeiten und so nach Fehlern zu suchen, wurde eine Klasse ErrorMessage implementiert, die Programmfehler oder -abstürze direkte am Display des Mobiltelefons anzeigen. Da die Fehlermeldungen zu- KAPITEL 3. PROTOTYPEN 34 sätzlich noch in der Log-Datei mitprotokolliert werden, kann die Fehlerquelle im Quellcode auf einige Zeilen genau bestimmt werden. Als Grundlage für die ErrorMessage-Klasse dient die Klasse Alert der LCDUI-Bibliothek. Alert ist ebenso wie Canvas ein Displayable-Objekt, welches sich jedoch das Displayable-Objekt, das am Display angezeigt wurde, merkt und nach einer definierten Anzeigedauer wieder auf das letzte Display -able-Objekt zurückschaltet. Da immer nur eine Fehlermeldung am Bildschirm angezeigt werden kann, wurde die Klasse ErrorMessage wie schon die KlasseLogger nach dem Singleton Entwurfsmuster implementiert. Im Konstruktor der MIDlet-Klasse Blue -tooth wird das ErrorMessage-Objekt erzeugt und initialisiert, dabei wird das Display-Objekt übergeben, damit die Fehlermeldung über die Methode dis-play.setCurrent() zur Anzeige gebracht werden kann. Die Anzeigedauer der Fehlermeldung am Bildschirm wurde über den Aufruf der Methode setTimeout(int ms) im Konstruktor der Klasse ErrorMessage auf drei Sekunden festgelegt. public static void showError ( String message ) { if ( instance == null ) { instance = new ErrorMessage () ; } instance . setString ( message ) ; display . setCurrent ( instance ) ; Logger . log ( " * error * " + message , true ) ; } Über den Methodeaufruf showError(String message) kann die Fehlermeldung zur Anzeige gebracht werden. Dabei wird zuerst überprüft ob schon eine Instanz der Klasse ErrorMessage erstellt wurde. Anschließend wird die Fehlermeldung mit dem Aufruf von setString(String message) als Text des Alert-Objekts gesetzt und anschließend mit display.setCurrent() angezeigt. Gleichzeitig wird die Fehlermeldung in der Protokoll-Datei eingetragen um sie nach dem Test auswerten zu können. 3.7 Potbanging Controller Der PotbangigController ist eine abgeleitete Klasse der abstrakten Klasse Controller und beinhaltet die Logik des ersten Spieleprototypen. Wie schon im Abschnitt 3.1 beschrieben, handelt es sich bei dem ersten Spiel um eine Portierung des Kinderspiels Topfschlagen“ auf das Mobiltelefon. Per Zufall ” wird dabei eine Position in einem selbst gewählten Radius bestimmt, welche der Spieler so schnell wie möglich finden muss. Statt einer akustischen Rückmeldung, wie nahe man sich der zu findenden Position ist, wird eine optisches Feedback am Bildschirm erzeugt. Dazu muss das Spielfeld zuerst in zehn Sektoren eingeteilt werden, die radial um KAPITEL 3. PROTOTYPEN 35 Abbildung 3.9: Spielfeldeinteilung in Sektoren. den zu findenden Punkt angeordnet sind. Ausgehen von der Distanz d zwischen Startpunkt und Ziel wird der Radius der Sektoren immer halbiert, das bedeutet, dass sich der Zielsektor in einem Radius von d /16 Länge um den Zielpunkt befindet (siehe Abb. 3.9). Der aktuelle Sektor wird dabei am Bildschirm durch eine Hintergrundfarbe dargestellt, wie sie in der Abbildung 3.9) zu sehen ist. Zusätzlich wird der zugehörige Sektorenname mittig am Bildschirm dargestellt. Um den Testbetrieb zu Vereinfachen wird neben der schon beschriebenen Bildschirmdarstellung, weitere Informationen zum aktuellen Spiel angezeigt. Sobald man den Debugmodus einschaltet, werden in der linken oberen Ecke des Displays die GPS-Koordinaten der Startposition ausgegeben. Darunter befindet sich die Anzeige der GPS-Daten zum zufällig berechneten Zielpunkt. Ein Trennstrich, trennt die statischen Zusatzinformationen von den Anzeigen, die sich in jedem Updatezyklus verändern. Dazu zählen die GPSKoordinaten der aktuellen Position und der momentane Abstand zwischen Zielpunkt und aktueller Position. KAPITEL 3. PROTOTYPEN 3.7.1 36 Tastenbelegung Je nachdem in welchem Teil des Spieles man sich befindet, haben die Tasten eine unterschiedliche Belegung. In der folgenden Aufzählung findet man die Tatenbelegung sortiert nach den einzelnen Spielzuständen. Während das Spiel Topfschlagen“ gespielt wird ist eigentlich keine Benutzereingabe nötig, ” da die Interaktion zwischen Spiel und Spieler nur auf einer Positionsänderung des Spielers basiert. Trotzdem kann es möglich sein, dass man während des Spieles eine Taste drücken muss um den automatischen Bildschirmschoner, welcher sich bei vielen Mobiltelefonen nach ein paar Minuten ohne Benutzereingabe einschaltet, wieder zu deaktivieren. • Menü Pfeiltaste oben Pfeiltaste unten Pfeiltaste links Pfeiltaste rechts Steuerkreuz mitte • Spiel Rautentaste . Sterntaste . . Umschalten zwischen Testmodus und normalem Modus. Umschalten zwischen Testmodus und normalem Modus. Die Taste muss mindestens zwei Sekunden gedrückt bleiben. • Debugmodus Rautentaste . Sterntaste . . Steuerkreuz mitte . 3.7.2 Markiert das vorige Menüelement aus. Markiert das nächste Menüelement aus. Markiert das vorige Menüelement aus. Markiert das nächste Menüelement aus. Wählt das markierte Menüelement aus. Umschalten zwischen Testmodus und normalem Modus. Umschalten zwischen Testmodus und normalem Modus. Die Taste muss mindestens zwei Sekunden gedrückt bleiben. Setzen der aktuellen Position auf die Zielposition. Bewirkt ein sofortiges Spielende. Spielablauf Der in der Abbildung 3.10 dargestellte Spielablauf wird in den folgenden Abschnitten anhand von Codesegmenten und einer textueller Beschreibung genauer erläutert. Die Schleife zwischen Positionsabfrage und dem Vergleich der aktuellen Position mit der Zielposition wird solange durchlaufen, bis der Spieler sich anhand der Auswertung der GPS-Position im Zielsektor befindet. Auswahl des Spielfeldes Damit das Spiel gestartet werden kann, muss vorher festgelegt werden, wie weit sich das Spielfeld erstreckt. Je größer das Spielfeld gewählt wird, desto KAPITEL 3. PROTOTYPEN 37 Abbildung 3.10: Ablaufdiagramm für das Spiel Topfschlagen. weiter ist der Startpunkt vom Ziel entfernt. Da der Zielsektor, wie schon im Abschnittg 3.7 erwähnt wurde, von der Distanz zwischen Start- und Endpunkt abhängig ist, wird es mit zunehmender Spielfeldgröße leichter den Zielsektor zu finden. Aufgrund der Genauigkeitsfehler von einigen Metern beim GPS-Positionsbestimmungsverfahren muss das Spielfeld eine bestimmte Größe aufweisen, damit das Spiel überhaupt spielbar ist. Zur Auswahl stehen deshalb 10, 30, 50, 80 und 100 Meter für die Distanz zwischen KAPITEL 3. PROTOTYPEN 38 Startpunkt und Ziel. Da in der Low-Level-API keine vorgefertigten Komponenten zur Erstellung von Menüs und Menüelementen gibt, wurde eine eigene Klasse Menu und eine Klasse Button implementiert um das Menü zu visualisieren. Die Klasse Button stellt alle Funktionen zum Zeichnen eines Buttons zur Verfügung und speichert des Weiteren den aktuellen Status des Buttons, der angibt ob der Button aktuell selektiert ist oder nicht. Zusätzlich wird neben dem Text des Buttons auch ein zugehöriger Wert gespeichert. Der Wert steht im Spielfeldmenü für die Distanz zwischen Start und Endpunkt und muss somit nicht aus dem Buttontext ausgelesen werden. Um den Button unabhängig von einem Menü verwenden zu können wurde folgender Konstruktor implementiert: public Button ( String text , double value , int left , int top , int width , int height ) { this . text = text ; this . value = value ; this . left = left ; this . top = top ; this . width = width ; this . height = height ; this . textX = this . left + ( this . width / 2) ; this . textY = this . top + ( this . height / 2) ; } left und top definiert den linken, oberen Eckpunkt des Buttons für die Platzierung am Bildschirm. width und height beschreiben die Dimensionen des Bedienelements in Pixel. Für das generische Menü wird die Größe der Buttons anhand der Anzahl der Bedienelemente im Menü und aufgrund der Display-Größe automatisch berechnet. Über die Funktion addButton() der Klasse Menu kann ein neuer Button dem Menü hinzugefügt werden. Mit jedem zusätzlichen Button-Objekt muss die Größe aller Menüelemente mit Hilfe der Methode recalculateDimensions() neu berechnet werden. Über die Methoden selectUp() und selectDown() der Klasse Menu kann der Spieler durch das Menü navigieren. Nachdem der Benutzer eine Auswahl getroffen hat, kann der Wert des selektierten Buttons anhand der Funktion getSelectedValue() ausgelesen werden. Mit dem Wert des selektierten Buttons hat man auch die Distanz zwischen Start- und Endpunkt und in der Prozedur setTargetSectors() werden die Größen der Spielfeldsektoren berechnet und in dem double-Array targetSectors abgespeichert. Intialisierung Nachdem eine Bluetooth-Verbindung zwischen der Applikation und dem GPS-Empfänger aufgebaut ist, und die Spielfeldgröße sowie die Sektoren KAPITEL 3. PROTOTYPEN 39 ermittelt wurden, kann das eigentliche Spiel starten. Damit allerdings sichergestellt ist, dass der GPS-Empfänger auch schon Datensätze sendet, wird das Spiel zunächst für sechs Sekunden angehalten. In dieser Zeit werden die ersten GPS-Daten vom Empfänger in den Zwischenspeicher abgelegt. Da diese Daten für die Ermittlung des Startpunktes herangezogen werden, sollte sich der Benutzer nicht bewegen damit der Startpunkt möglichst genau festgelegt wird. Am Ende der Initialisierung wird der Startpunkt herangezogen um die Zielposition zu berechnen. Das Ziel liegt auf einem beliebigen Punkt eines Kreises mit der gewählten Spielfeldgröße als Radius und der Startposition als Mittelpunkt. Für die Berechnung wird dabei angenommen, dass die Erde eine Kugel mit einem Radius von 6378 Kilometern ist. Aufgrund der kleinen Spielfeldgröße von maximal hundert Metern ist der Fehler bei der Annahme, das des sich bei der Erde um eine Kugel handelt, vernachlässigbar. public GPSRecord getRandomPoint ( double distance ) { Random randomGen = new Random () ; randomGen . setSeed ( System . currentTimeMillis () ) ; double random = randomGen . nextDouble () ; double x = distance * Math . cos ( random ) ; double y = distance * Math . sin ( random ) ; int randomInt = randomGen . nextInt (4) ; switch ( randomInt ) { case 1: x *= -1.0 f ; break ; case 2: x *= -1.0 f ; y *= -1.0 f ; break ; case 3: y *= -1.0 f ; break ; } double radLon = ( x / RADIUS * Math . cos ( this . mLatitudeRad ) ) + this . mLongitudeRad ; double radLat = ( y / RADIUS ) + this . mLatitudeRad ; return new GPSRecord ( radLon , radLat ) ; } In der Funktion getRandomPoint(double distance) der Klasse GPSRecord wird eine zufällige Zielposition erstellt indem zunächst über die Winkelfunktionen ein Punkt mit den Koordinaten x und y berechnet, der auf einem Kreissegment mit dem Radius der Distanz liegt. Da der Punkt nicht nur im ersten Quadranten liegen soll, wird über einen Zufallsgenerator bestimmt in welchem Quadranten der Punkt liegt und die Koordinaten x und y dementsprechend angepasst. Abschließend muss der Punkt von der Ebene auf eine KAPITEL 3. PROTOTYPEN 40 Kugel mit dem Erdradius projiziert werden, wobei das Zentrum des ebenen Koordinatensystems auf die GPS-Position des Startwerts gelegt wird. Spielschleife Nachdem das Ziel berechnet wurde, beginnt der erste Durchlauf der Spielschleife. Dazu wird zunächst die aktuelle Position des Spielers aus dem GPSRecordBuffer der GPS-Klasse ausgelesen. Anschließend wird die Entfernung von Start- und Endpunkt berechnet, indem die Lattitude- und LogitudeWerte auf ein ebenes Koordinatensystem projiziert werden. Der Ursprung des ebenen Koordinatensystems wird auf die projizierte Position der aktuellen GPS-Position gelegt und anschließend wird unter Verwendung des Satzes von Pythagoras die Distanz zwischen den beiden Punkten berechnet. Damit der folgende Programmcode für alle GPS-Anwendungen verwendbar ist, wurde er in der Klasse GPSRecord hinzugefügt (siehe folgender Programmausschnitt): public double getDistanceTo ( GPSRecord r ) { double x = RADIUS * ( r . mLongitudeRad - this . mLongitudeRad ) * Math . cos ( this . mLatitudeRad ) ; double y = RADIUS * ( r . mLatitudeRad - this . mLatitudeRad ); return Math . sqrt (( x * x + y * y ) ) ; } Ist die Distanz zwischen aktueller Position und Zielpunkt ermittelt, wird diese mit den Werten des targetSectors-Arrays verglichen. Je nach Zielsektor wird für die Benutzeroberfläche anschließend ein String abgespeichert, der angibt in welchem Sektor (Heiß, Warm, Kalt, Kälter, etc.) man sich gerade befindet. Da sich die Farben der Farbpalette für den Hintergrund nicht ändern, wurde das Farbarray in der Klasse Colors abgelegt. Der Einfachheit halber haben die zusammengehörigen Farben und Sektoren denselben Array-Index. Wenn die Distanz zwischen aktueller Position und Zielpunkt kleiner oder gleich einem Sechzehntel der Distanz zwischen Startpunkt und Endpunkt ist, so wurde die Siegesbedingung erfüllt und die Spielschleife wird verlassen. Als optisches Feedback wird dem Spieler für seinen Erfolg gratuliert und die vergangene Spielzeit angezeigt. 3.8 Crossgolf Controller Wie auch der PotbangigController ist der CrossgolfController eine abgeleitete Klasse der abstrakten Klasse Controller und beinhaltet die Logik des zweiten Spieleprototypen. Der zweite Prototyp setzt das Spiel Crossgolf, eine Abwandlung des klassischen Golfens, auf ein Mobiltelefon um. Bei Crossgolf KAPITEL 3. PROTOTYPEN 41 versucht man mit möglichst wenig Abschlägen ein vorher definiertes Ziel zu erreichen. Crossgolf wird allerdings nicht auf einem normalen Golfplatz gespielt, sondern an jedem erdenklichen Ort wie zum Beispiel in urbaner Umgebung oder auf einem verlassenen Industriegebiet. Abbildung 3.11: Darstellung des CrossGolf-Prototyps auf einem Emulator. Bei dem mobilen Spiel wird versucht eine Verbindung zwischen dem realen Crossgolf und einer klassischen Golfsimulation herzustellen. Während der Spieler versucht auf dem virtuellen Golfplatz den Ball mit möglichst wenig Schlägen einzulochen, muss er in der realen Umgebung versuchen den virtuell abgeschlagen Golfball nach jedem Schuss wieder zu finden. Die Schwierigkeit besteht nun darin neben den virtuellen Hindernissen wie Wasser, Wäldern oder Sandgruben auch die reale Umgebung in das Spiel mit einfließen zu lassen. Im Gegensatz zum ersten Prototypen spielt die Positionsbestimmung eine untergeordnete Rolle, da die Suche“ nach dem Ball nur einen Teil ” des Spieles darstellt. Wie in Abbildung 3.11 ersichtlich ist, ist das Benutzerinterface das zweiten Prototypen komplexer, da Informationen über den gewählten Golfschläger, den Abschlagwinkel, den Wetterverhältnissen und die Schlagkraftanzeige dargestellt werden müssen. 3.8.1 Tastenbelegung Auch bei diesem Controller ist die Tastenbelegung abhängig vom aktuellen Spielstatus. Im Gegensatz zum Spiel Topfschlagen“, ist beim CrossGolf” Prototypen eine Eingabe über Tasten notwendig, damit der Ball vom virtuellen Spielfeld abgeschlagen werden kann. In der folgenden Aufzählung findet man die Tatenbelegung sortiert nach den einzelnen Spielzuständen. KAPITEL 3. PROTOTYPEN 42 • Menü Pfeiltaste oben Pfeiltaste unten Pfeiltaste links Pfeiltaste rechts Steuerkreuz mitte Markiert das vorige Menüelement aus. Markiert das nächste Menüelement aus. Markiert das vorige Menüelement aus. Markiert das nächste Menüelement aus. Wählt das markierte Menüelement aus. • Spiel Pfeiltaste oben Pfeiltaste unten Pfeiltaste links . Pfeiltaste rechts . Steuerkreuz mitte Rautentaste . Sterntaste . . Wählt den vorigen Golfschläger aus. Wählt den nächsten Golfschläger aus. Dreht die Abschlagrichtung um fünf Grad nach links. Dreht die Abschlagrichtung um fünf Grad nach rechts. Startet bzw. beendet den virtuellen Abschlag. Umschalten zwischen Testmodus und normalem Modus. Umschalten zwischen Testmodus und normalem Modus. Die Taste muss mindestens zwei Sekundengedrückt bleiben. • Debugmodus Pfeiltaste oben Pfeiltaste unten Pfeiltaste links . Pfeiltaste rechts . Steuerkreuz mitte Rautentaste . Sterntaste . . Steuerkreuz mitte . Wählt den vorigen Golfschläger aus. Wählt den nächsten Golfschläger aus. Dreht die Abschlagrichtung um fünf Grad nach links. Dreht die Abschlagrichtung um fünf Grad nach rechts. Startet bzw. beendet den virtuellen Abschlag. Umschalten zwischen Testmodus und normalem Modus. Umschalten zwischen Testmodus und normalem Modus. Die Taste muss mindestens zwei Sekunden gedrückt bleiben. Setzen der aktuellen Position auf die Zielposition. Bewirkt ein sofortiges Spielende. 3.8.2 Spielablauf Wie man in der Abbildung 3.12 erkennen kann, ist das Spiel komplexer als der Topfschlag“-Prototyp. Aus diesem Grund wurde eine Klasse GameState ” implementiert, die zur Laufzeit angibt in welcher Spielphase sich der Spieler befindet. Das Spiel durchläuft dabei immer die Phasen in einer vorgegebenen Reihenfolge. In der Phase SET kann der Spieler den Abschlagwinkel ändern und die Schlägerauswahl treffen. Sobald der Benutzer das Steuerkreuz in der KAPITEL 3. PROTOTYPEN 43 Abbildung 3.12: Ablaufdiagramm für das Spiel CrossGolf. Mitte drückt wechselt das Spiel in die DRIVE-Phase, der Spieler kann keine Schlägerauswahl mehr treffen, und ein Fortschrittsanzeiger wird dargestellt, mit dem die Wucht des Schlages beeinflusst werden kann. In der Phase FLY kann der Spieler nicht mit dem MIDlet interagieren und die Flugbahn des Golfballs wird in einem kurzen Video dargestellt. lstinlineCALC beschreibt die Phase in der die Kollisionberechnungen durchgeführt werden und der Ball gegebenenfalls wieder an den letzten Abschlag zurückbefördert wird. Ist der Ball fehlerlos gelandet, so startet die Phase MOVE in der nun der Spieler wieder versuchen muss die Ballposition in der realen Umgebung zu finden. Wurde der Ball in das Loch eingelocht so startet die letzte Phase END und dem Spieler wird eine Zusammenfassung des Spiels am Display angezeigt. Auswahl des Spielfeldes Als Spielfeldgröße dient bei dem zweiten Prototyp der Abstand vom ersten Abschlag bis zum Loch. Wie schon beim ersten Prototypen wurde hier KAPITEL 3. PROTOTYPEN 44 ebenfalls die Klassen Menu und Button zum Erstellen eines Menüs verwendet. Schwankte die Auswahl der Spielfeldgröße bei dem ersten Prototyp noch zwischen 10 und 100 Meter, so variiert beim zweiten Spiel die Auswahl des Spielfelds zwischen 100 und 1000 Meter. Das Spielfeld wurde deshalb so groß dimensioniert, da der Spieler mehrere Schläge benötig, bis er den Ball in das virtuelle Loch einlochen kann. Würde er den Ball mit zum Beispiel fünf Schlägen einlochen und wären das Spielfeld 10 Meter groß, so wäre im schlimmsten Fall nur zwei Meter zwischen alter Spielerposition und zu findender Ballposition, was bedeuten würde, dass der Fehler bei Positionsbestimmungsverfahren größer ist als der positionsabhängige Spielabschnitt und dadurch würde das Spiel unspielbar werden. Der Wind wurde so implementiert, dass er sich mit der Spielfeldgröße skaliert. Maximal um 20% der Spielfeldgröße kann die Windkomponente den Ball von seiner ursprünglichen Landeposition abbringen. Einzig die Schlägerauswahl kann diesen Wert noch verändern, da mit unterschiedlichen Schlägern der Ball unterschiedlich vom Wind beeinflusst wird. Bei der Verwendung von Schlägern die den Ball höher und weiter schießen als andere wirkt sich auch der Wind stärker aus. Intialisierung Bei der Initialisierung wird der Startpunkt (Abschlag) und die Zielposition (Loch) herangezogen und anschließend die reale Spielfeldgröße darauf projiziert. Die Variable factor gibt an, aus wie vielen Metern eine Pixelseitenlänge auf dem virtuellen Golfplatz bestehen würde: this . holeDistanceReal = this . crossMenu . getSelectedValue () ; this . holeDistanceGame = this . tee . y - this . hole . y ; this . factor = this . holeDistanceGame / this . holeDistanceReal ; Spielschleife Nachdem das Spiel initialisiert wurde, beginnt die Phase SET in der der Spieler den Abschlagwinkel ändern und die Schlägerauswahl treffen kann. Durch Drücken des Steuerkreuzes in der Mitte wird automatisch zur DRIVEPhase gewechselt und der Spieler kann keine Schlägerauswahl mehr treffen. Ein Fortschrittsanzeiger zeigt die Schlagkraft an, mit welcher der kommende Schlag ausgeführt wird. Um das Spiel herausfordernder zu gestalten, wird die Schlagkraft bei jedem Durchlauf der update()-Methode von 0 erhöht, bis sie das Maximum erreicht hat. Ist das Maximum erreicht beginnt die Anzeige beim nächsten Durchlauf wieder bei 0. Aus diesem Grund besteht ein Risiko zu Warten bis die Anzeige ein Maximum erreicht hat, da unter Umständen nicht der KAPITEL 3. PROTOTYPEN 45 richtige Zeitpunkt erwischt wird und der Ball anstatt mit maximaler Kraft nur mit geringer Kraft abgeschossen wird. Wurde ein Ball abgeschlagen, so werden in der Methode updateCurrentPoint() zuerst die Vektoren Wind und Schlag miteinander addiert und anschließend mit den Koordinaten des Abschlags summiert. Aus der Summe der Vektoren ergibt sich der Punkt auf dem der Ball landet. Während der Ball fliegt ist die Phase FLY aktiv. Ist der Golfball mit einem Putter geschlagen worden, so besteht die Möglichkeit, dass der Ball in seiner Bewegung zum Zielpunkt über das Loch rollt und somit hineinfällt. if ((( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . name . equals ( " Putter " ) ) { for ( int j = -1; j <= 1; j ++) { for ( int i = -1; i < 1; i ++) { if ( equalsColor ( getPixel (( int ) this . ball . x + i , ( int ) this . ball . y + j , this . raw ) , Colors . crossgolf [ G_HOLE ]) ) { this . ball . x = this . hole . x ; this . ball . y = this . hole . y ; // ... this . gs . setState ( GameState . END ) ; } } } Wie man in obigem Codestück erkennen kann, wird nicht nur die aktuelle Position sondern auch auf den Nachbarfeldern geprüft, ob es sich um ein Loch handelt. Anhand eine Farbpalette mit definierten Farben für jeden Spieluntergrund und dem Array raw, in welchem das Spielfeld als Bild abgespeichert ist, wird ein Farbvergleich zwischen der Referenzfarbe des Loches und der Farbe der aktuellen Position im Bild gemacht. Stimmen die Farben überein, so wird der Ball auf die genaue Position des Loches gesetzt und die Anwendung beendet. Sobald der Golfball sein Ziel erreicht hat, wechselt die Phase auf CALC . In der Phase CALC wird eine einfache Kollisionserkennung durchgeführt. Befindet sich der Ball außerhalb des Spielfeldes oder im Wasser, so wird der Golfball auf seine letzte Position zurückgesetzt und ein Strafschlag wird gewertet. double dX = this . gpsRecord . getDistanceX ( this . lastBallPos ) ; double dY = this . gpsRecord . getDistanceY ( this . lastBallPos ) ; this . player . x = this . centerOld . x + dX * factor ; this . player . y = this . centerOld . y + dY * factor ; if (( this . player . x <= this . ball . x + 4) KAPITEL 3. PROTOTYPEN 46 && ( this . player . x >= this . ball . x - 4) && ( this . player . y <= this . ball . y + 4) && ( this . player . y >= this . ball . y - 4) ) { this . calcText = " " ; this . meter . reset () ; this . gs . nextState () ; } Nachdem der Ball geflogen ist und nicht zurückgesetzt werden musste, startet die Phase MOVE den positionsabhängigen Teil des Spieles. Zuerst wird der x- und y-Abstand zwischen der letzte Position des Golfballs und dem aktuellen GPS-Datensatz ermittelt wie in obigem Programmcode zu sehen ist. Anschließend werden die Bildschirmkoordinaten des Spielers neu berechnet. Ist der Golfball von der Repräsentation des Spielers maximal vier Pixel entfernt, so wird vom Spiel davon ausgegangen, dass der Golfball gefunden wurde. Kapitel 4 Schlusswort In diesem abschließenden Kapitel wird auf die Eindrücke bei den Testläufen der Prototypen näher eingegangen und die Vor- und Nachteile der Spiele aufgezeigt. Grundsätzlich waren die Resonanzen bei den Tests durchwegs positiv obwohl die Positionsbestimmung immer wieder Probleme mit sich brachte. Der erste Test wurde mit dem Potbanging Prototypen in Wien auf den Erholungsgebiet Wienerberg durchgeführt. Das Erholungsgebiet Wienerberg wurde deshalb ausgewählt, da sich in der näheren Umgebung keine hohen Häuser befinden und das Gebiet relativ weitläufig ist, sodass auch ein Spiel mit größerem Spielfeld getestet werden kann. Bei der Entwicklung des Spiels wurden immer die Debug-Informationen (unter anderem auch die Distanz zum Zielpunkt) zusätzlich am Bildschirm angezeigt. Erst für den ersten Test wurden diese Informationen ausgeblendet, wodurch die Spieler nur mehr wussten in welchem Spielfeldsektor sie sich befinden, nicht allerdings ob sie sich vom Zielpunkt entfernen oder auf ihn zu bewegen. Durch die fehlende Information verdoppelte sich die Spielzeit bei einem Spielfeld von dreißig Metern von zehn Minuten auf mindestens zwanzig Minuten. Einige Spieler brachen den Test sogar ab mit der Begründung, dass sie keinerlei Orientierung mehr haben und nicht einmal mehr ansatzweise wussten in welche Richtung sie sich bewegen sollten. Aus diesem Grund wurde der Prototyp kurzfristig modifiziert. Zusätzlich zu der Information über den Sektor in dem sich die Spieler befinden, wurde aus den letzten beiden Positionsdaten ein Richtungsvektor erzeugt aus dem ermittelt wurde, ob und wie sehr sich der Spieler auf den Zielpunkt zu bewegt. Diese Information wurde für die Spieler mittels einer weiteren Textzeile am Display zugänglich gemacht. Bei einem erneuten Test hatten die Spieler dann weniger Probleme das Ziel zu erreichen und auch die Spieldauer verkürzte sich wieder auf circa zehn bis fünfzehn Minuten. Ein weiteres Problem war die Update-Geschwindigkeit der Positionsdaten. Bewegte sich ein Spieler schnell und blieb dann abrupt stehen, so lie- 47 KAPITEL 4. SCHLUSSWORT 48 ferte der GPS-Empfänger für circa drei bis fünf Sekunden Daten weiter als würde er die Position des Spielers aufgrund seiner vorangehenden Bewegung vorausberechnen. Erst nach ein paar Sekunden lieferte dann auch der Bluetooth-GPS-Empfänger keine Daten mehr und die Position des Spielers am Display wurde nicht mehr verändert. Dieser Fehler fiel zwar bei Spielfeldgrößen ab fünfzig Metern nicht mehr ins Gewicht, allerdings machte er das Spiel bei einer Spielfeldgröße von zehn Metern dadurch fast unspielbar. Bei einem größeren Spielfeld sind die Sektoren dementsprechend größer und durch ein plötzliches Stehen bleiben wurde ein Sektor in den wenigsten Fällen passiert. Im Gegensatz dazu sind die Sektoren bei einer Spielfeldgröße von zehn Metern ziemlich klein, durch einen schnellen Stopp konnte es also passieren, dass man sich plötzlich wieder vom Zielpunkt entfernt, obwohl man sich darauf zu bewegt hat und durch ein Stehen bleiben eigentlich verhindern wollte, dass man sich wieder vom Ziel entfernt. Trotz Veränderung der Dauer des Sleep-Befehls im Thread für die Positionsbestimmung konnte der Fehler nicht behoben werden. Allerdings konnten bei den Spielern zwei unterschiedliche Reaktionen auf diesen Fehler wahrgenommen werden. Ein Großteil der Spieler bewegte sich aufgrund dessen wesentlich langsamer und machte am Anfang weniger Stopps. Erst als sie in die Nähe des Zielpunktes kamen wurden kurze Bewegungspausen eingelegt. Durch das langsame Spiel wurde auch die Spieldauer wieder um einige Minuten verlängert. Einige Spieler versuchten sich den Fehler zunutze zu machen und begannen sich, sobald sie in der Nähe des Zielpunkts waren, schnell um die eigene Achse zu drehen oder versuchten schnell im Kreis zu laufen. Durch die schnelle Drehung wurden von mehreren Positionen im Umkreis Daten erfasst, wodurch der Zielpunkt gefunden werden konnte, obwohl die Spieler eigentlich keine Positionsänderung vornahmen. Dieser Nachlaufeffekt“ hatte zusätzlich zur Folge, dass eine Position ” manchmal nicht wieder erreicht werden konnte. Machte man zehn Schritte in eine bestimmte Richtung und bewegte man sich mit einer anderen Geschwindigkeit wieder zum Ausgangspunkt zurück, so konnte man erkennen, dass die neuen Positionsdaten nicht mit den Positionsdaten der ersten Messung übereinstimmten. Je größer der Geschwindigkeitsunterschied zwischen den beiden Bewegungen war, desto größer war auch der Unterschied in den ermittelten Positionen. Wie schon vorher erwähnt worden ist, konnte der Nachlaufeffekt“ nicht unterdrückt werden, allerdings verliert er durch die ” Wahl der Spielfeldgröße an Bedeutung. Je größer das Spielfeld gewählt wird, desto weniger macht sich der Nachlaufeffekt“ im Spiel bemerkbar. ” Nachdem der PotBanging-Prototyp getestet wurde, wurden auch noch Tests mit dem CrossGolf-Prototyp gemacht. Das erste Problem ergab sich daraus, dass der Spieler beim Start des Spieles nicht weiß, in welche Richtung der erste Abschlag gemacht wird. Da in dem empfangenen NMEA-Datensatz keine Information zur Ausrichtung des Spielers vorhanden ist, breitet sich KAPITEL 4. SCHLUSSWORT 49 das Spielfeld immer nach Norden aus. In einem ersten Versuch wurde die Ausrichtung des Spielfelds per Zufall bestimmt, allerdings führte das nur zur Verwirrung unter den Spielern. Hatten die Spieler nämlich nach dem ersten Testlauf ein ungefähres Gefühl dafür entwickelt, in welche Richtung gespielt wird, konnten sie bei einem zweiten Versuch die Umgebung besser in das Spiel einplanen. Eine mögliche Erweiterung des Spieles wäre eine zusätzliche Initialisierungsroutine bei der der Spieler sich in die gewünschte Spielrichtung begibt. Aufgrund der empfangenen Positionsdaten könnte so die Ausrichtung des Spielers ermittelt werden und das Spielfeld könnte in die Bewegungsrichtung des Spielers gedreht werden. Auch bei diesem Prototypen wurde der Einfluss des Nachlaufeffekts“ ” bemerkt, allerdings war der Fehler aufgrund der großen Spielfläche vernachlässigbar. Ein Problem war die zu kleine Anzeige am Mobiltelefon. Der Golfball wurde als 2x2 Pixel großes Element während der Ballsuche dargestellt und die aktuelle Position wurde ebenfalls als 2x2 Pixel großer Block visualisiert. Wurde die Hintergrundbeleuchtung aufgrund von Inaktivität in der Benutzereingabe abgeschaltet, so war für die Spieler nicht mehr erkennbar in welche Richtung sie eigentlich gehen mussten. Nachdem die Suche nach dem Ball, und somit auch die Positionsbestimmung, nur einen kleinen Teil des Gesamtspiels ausmacht, gab es beim zweiten Prototypen weniger Probleme bei dem Spielablauf und die Tester meinten, dass das Spiel durchsichtiger und verständlicher als der PotBangingPrototyp ist. Grundsätzlich war gedacht, dass beide Spiele sowohl in der Stadt als auch auf einer freien Fläche gespielt werden können. Trotzdem wurde festgestellt, dass vor allem der zweite Prototyp in dicht besiedeltem Gebiet unspielbarer ist, da der virtuelle Spiel meistens in ein Haus geschossen wurde und die Position des Spielballs somit nie erreicht werden konnte. Auch das Wetter hatte einen Einfluss auf den Spielfluss, da bei sehr stark bewölktem Himmel die Positionsfindung mehrfach aussetzte und keine Positionsdaten geliefert wurden. Dasselbe Problem trat bei den Testläufen in Alt Erlaa, einer dicht besiedelten Wohnhausanlage im 23. Wiener Gemeindebezirk, auf. Zwischen 2 Wohnblocks setzte ebenfalls kurzfristig die Positionsbestimmung aus. Auch die Schwankungen in der Position waren deutlich größer bei schlechtem Wetter oder dicht besiedeltem Gebiet. Da der $GPGSA-Datensatz aus dem NMEA-Format nicht geparst und ausgelesen wurde, kann im Nachhinein leider keine belegbare Angabe über die Satellitengeometrie gemacht werden, allerdings sind die Schwankungen in den Positionsdaten ein Zeichen dafür, dass die DOP-Werte größer waren als auf der freien Fläche am Erholungsgebiet Wienerberg. KAPITEL 4. SCHLUSSWORT 4.1 50 Fazit Abschließend sei noch einmal erwähnt, dass die Spiele durchweg auf positive Resonanz gestoßen sind. Vor allem der leichte Einstieg in die Spiele und die kurze Initialisierung der Spieler machen die Spiele attraktiv. Anstatt viele technische Geräte mit sich herumtragen zu müssen, werden bei den Prototypen nur ein Mobiltelefon und ein Bluetooth-GPS-Adapter benötig. Aufgrund der Nachlaufzeit“ und des geringen Updateintervalls die die Po” sitionsbestimmung mittels GPS wahrscheinlich nicht für die Umsetzung von schnelleren Spielen wie zum Beispiel Fußball oder Rugby eingesetzt werden können. Ein weiteres Problem ist der große Fehler bei der Positionsbestimmung. Zwar ist es für die Prototypen von geringerer Bedeutung, ob die Position exakt ermittelt wurde, da die Daten in beiden Fällen ja nur als Referenz für weitere Messungen dient, allerdings sind die Schwankungen in der Position im Spiel teilweise sichtbar. Je genauer die GPS-Module arbeiten und je kürzer die Update-Intervalle werden, desto leichter werden positionsabhängige Spiele für den Spieler durchschaubar. Bei großen Updateintervallen oder ungenauen Positionsbestimmungen kann der Spieler dem Spielverlauf nicht mehr folgen - jede Bewegung des Spielers muss auch direkt auf den Spielverlauf, für den Benutzer logisch nachvollziehbar, auswirken. Auch aufgrund der Spieleraussagen kann man darauf schließen, dass Spiele mit einem höheren, technischen Aufwand wie zum Beispiel dem Aufbau eines WLAN-Positionsbestimmungsverfahren aufgrund ihrer Komplexität im Moment noch nicht für eine Person zugänglich sind. Je geringer der Aufwand für den Spieler vor dem Spielstart ist, desto eher wird ein Spiel auch angenommen. Anhang A Sourcecode Listing A.1: Bar.java package util . ui ; import javax . microedition . lcdui . Graphics ; public class Bar implements Renderable { private int left = 0; private int top = 0; private int width = 0; private int height = 0; private double min = 0.0; private double max = 1.0; private double value = 0.0; private double barHeight = 0.0; public Bar ( double min , double max , double value , int left , int top , int width , int height ) { this . left = left ; this . top = top ; this . width = width ; this . height = height ; this . min = min ; this . max = max ; this . value = value ; } public void paint ( Graphics g ) { g . setColor ( Colors . grey ) ; g . fillRect ( this . left , this . top , this . width , this . height ) ; g . setColor ( Colors . blue_bright ) ; g . fillRect ( this . left , this . top , this . width , ( int ) this . barHeight ) ; g . setColor ( Colors . black ) ; g . drawRect ( this . left , this . top , this . width , this . height ) ; } public void inc ( double value ) { this . value += value ; if ( this . value > this . max ) { this . value = this . min ; } this . barHeight = (( this . height / ( this . max - this . min ) ) * ( this . value - this . min ) ) ; } public void dec ( double value ) { this . value -= value ; if ( this . value < this . min ) { this . value = this . max ; } this . barHeight = (( this . height / ( this . max - this . min ) ) * ( this . value - this . min ) ) ; } 51 ANHANG A. SOURCECODE public double getValue () { return this . value ; } public void reset () { this . value = this . min ; } } Listing A.2: Bluetooth.java package bluetooth ; import javax . microedition . lcdui . Display ; import javax . microedition . midlet . MIDlet ; import javax . microedition . midlet . M I D l e t S t a t e C h a n g e E x c e p t i o n ; import log . Logger ; import util . error . ErrorMessage ; public class Bluetooth extends MIDlet { private Display display = null ; private BluetoothCanvas canvas = null ; public Bluetooth () { super () ; this . display = Display . getDisplay ( this ) ; ErrorMessage . init ( this . display ) ; Logger . init ( " PotBanging " ) ; } protected void destroyApp ( boolean unconditional ) throws M I D l e t S t a t e C h a n g e E x c e p t i o n { Logger . endLog () ; } protected void pauseApp () { this . display . setCurrent ( null ) ; } protected void startApp () throws M I D l e t S t a t e C h a n g e E x c e p t i o n { this . canvas = new BluetoothCanvas ( true ) ; this . canvas . start () ; this . display . setCurrent ( canvas ) ; } } Listing A.3: BluetoothCanvas.java package bluetooth ; import gps . GPS ; import java . io . IOException ; import java . util . Enumeration ; import java . util . Vector ; import import import import javax . bluetooth . RemoteDevice ; javax . microedition . lcdui . Canvas ; javax . microedition . lcdui . Font ; javax . microedition . lcdui . Graphics ; import crossgolf . C r o s s G o l f C o n t r o l l e r ; import log . Logger ; import potbanging . P o t B a n g i n g C o n t r o l l e r ; import import import import import import util . error . ErrorMessage ; util . game . Controller ; util . game . GameState ; util . observer . Observable ; util . observer . Observer ; util . ui . Colors ; public class BluetoothCanvas extends Canvas implements Runnable , Observer { private static final int BREAK = 500; 52 ANHANG A. SOURCECODE private static final int PLAY = 50; private Font font = Font . getFont ( Font . FACE_MONOSPACE , Font . STYL E_PLAIN , Font . SIZE_SMALL ) ; private Thread engine = null ; private int width = 0; private int height = 0; private boolean isFullScreen = false ; private boolean isInitialized = false ; private boolean isDeviceFound = false ; private Controller controller = null ; private GPS gps = null ; private B l u e t o o t h D i s c o v e r y discovery = null ; private B l u e t o o t h C o n n e c t i o n connection = null ; private String bluetoothName = " BT - GPS -336 F8D " ; private String bluetoothAddress = null ; public BluetoothCanvas ( boolean fullScreen ) { super () ; this . isFullScreen = fullScreen ; } public void run () { Thread currentThread = Thread . currentThread () ; while ( currentThread == this . engine ) { repaint () ; try { if ( this . controller instanceof C r o s s G o l f C o n t r o l l e r ) { if ((( C r o s s G o l f C o n t r o l l e r ) this . controller ) . gs . getState () == GameState . MOVE ) { Thread . sleep ( BREAK ) ; } else { Thread . sleep ( PLAY ) ; } } else { Thread . sleep ( BREAK ) ; } } catch ( I n t e r r u p t e d E x c e p t i o n e ) { // ErrorHandling ErrorMessage . showError ( " thread :: sleep error " ) ; } } } public void start () { if ( this . engine == null ) { this . engine = new Thread ( this ) ; this . engine . start () ; } } public void stop () { this . gps . stop () ; this . engine = null ; } public void notify ( Observable o , Object arg ) { if ( o instanceof B l u e t o o t h D i s c o v e r y ) { if ((( String ) arg ) . equals ( " deviceSearch " ) ) { Vector devices = this . discovery . getDevices () ; Enumeration enumDevices = devices . elements () ; while ( enumDevices . hasMoreElements () ) { RemoteDevice device = ( RemoteDevice ) enumDevices . nextEl ement () ; try { if ( device . getFriendlyName ( false ) . equals ( this . bluetoothName ) ) { this . bluetoothAddress = device . g e t B l u e t o o t h A d d r e s s () ; Logger . log ( " * search * " + " end " , true ) ; this . isDeviceFound = true ; break ; } } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " bt :: couldnt get device name " ) ; 53 ANHANG A. SOURCECODE } } if ( this . isDeviceFound ) { this . connection = new B l u e t o o t h C o n n e c t i o n ( this . bluetoothAddress ) ; this . gps = new GPS ( this . connection ) ; // this . controller = new P o t B a n g i n g C o n t r o l l e r ( this . width , this . height , // this . gps ) ; this . controller = new C r o s s G o l f C o n t r o l l e r ( this . width , this . height , this . gps ) ; this . gps . start () ; } else { this . discovery . d o D e v i c e D i s c o v e r y () ; } } } } protected void paint ( Graphics g ) { if ( this . isInitialized ) { if (! this . isDeviceFound ) { g . setColor ( Colors . grey ) ; g . fillRect (0 , 0 , this . width , this . height ) ; g . setFont ( this . font ) ; g . setColor ( Colors . white ) ; g . drawString ( " searching for devices " , this . width / 2 , this . height / 2 , Graphics . BASELINE | Graphics . HCENTER ) ; } else { this . controller . update () ; this . controller . paint ( g ) ; } } else { s e t F u l l S c r e e n M o d e ( this . isFullScreen ) ; initialize ( getWidth () , getHeight () ) ; } } private void initialize ( int width , int height ) { this . width = width ; this . height = height ; this . isInitialized = true ; this . discovery = new B l u e t o o t h D i s c o v e r y () ; this . discovery . addObserver ( this ) ; this . discovery . d o D e v i c e D i s c o v e r y () ; Logger . log ( " * search * " + " start " , true ) ; } public void keyPressed ( int keyCode ) { if ( this . isDeviceFound ) { Logger . log ( " * key pressed * " + keyCode , true ) ; this . controller . keyPressed ( keyCode , this ) ; } } public void keyReleased ( int keyCode ) { if ( this . isDeviceFound ) { Logger . log ( " * key released * " + keyCode , true ) ; this . controller . keyReleased ( keyCode , this ) ; } } public void keyRepeated ( int keyCode ) { if ( this . isDeviceFound ) { Logger . log ( " * key repeated * " + keyCode , true ) ; this . controller . keyRepeated ( keyCode , this ) ; } } public void pointerPressed ( int x , int y ) { if ( this . isDeviceFound ) { Logger . log ( " * pointer pressed * " + x + " , " + y , true ) ; this . controller . pointerPressed (x , y ) ; } } public void pointerReleased ( int x , int y ) { if ( this . isDeviceFound ) { Logger . log ( " * pointer released * " + x + " , " + y , true ) ; this . controller . pointerReleased (x , y ) ; } } public void pointerDragged ( int x , int y ) { if ( this . isDeviceFound ) { Logger . log ( " * pointer dragged * " + x + " , " + y , true ) ; 54 ANHANG A. SOURCECODE this . controller . pointerDragged (x , y ) ; } } } Listing A.4: BluetoothConnection.java package bluetooth ; import java . io . IOException ; import java . io . I n p u t S t r e a m R e a d e r ; import javax . microedition . io . Connector ; import javax . microedition . io . StreamConnection ; import util . error . ErrorMessage ; public class B l u e t o o t h C o n n e c t i o n { private StreamConnection btConection = null ; private I n p u t S t r e a m R e a d e r btStreamReader = null ; private String btAddress = null ; public B l u e t o o t h C o n n e c t i o n ( String btAddress ) { this . btAddress = " btspp :// " + btAddress + " :1 " ; } public synchronized void connect () throws IOException { if (! isConnected () ) { this . btConection = (( StreamConnection ) Connector . open ( this . btAddress , Connector . READ ) ) ; this . btStreamReader = new I n p u t S t r e a m R e a d e r ( this . btConection . openInputStream () ) ; } } public synchronized void disconnect () { try { if ( this . btStreamReader != null ) { this . btStreamReader . close () ; } if ( this . btConection != null ) { this . btConection . close () ; } } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " bt :: error during disconnect " ) ; } this . btStreamReader = null ; this . btConection = null ; } public synchronized boolean isConnected () { if (( this . btConection != null ) && ( this . btStreamReader != null ) ) { return true ; } return false ; } public synchronized int read () throws IOException { return this . btStreamReader . read () ; } } Listing A.5: BluetoothDiscovery.java package bluetooth ; import java . io . IOException ; import java . util . Vector ; import import import import import import import import javax . bluetooth . B l u e t o o t h S t a t e E x c e p t i o n ; javax . bluetooth . DeviceClass ; javax . bluetooth . DiscoveryAgent ; javax . bluetooth . D i s c o v e r y L i s t e n e r ; javax . bluetooth . LocalDevice ; javax . bluetooth . RemoteDevice ; javax . bluetooth . ServiceRecord ; javax . bluetooth . UUID ; 55 ANHANG A. SOURCECODE import log . Logger ; import util . error . ErrorMessage ; import util . observer . Observable ; public class B l u e t o o t h D i s c o v e r y extends Observable implements D i s c o v e r y L i s t e n e r { private LocalDevice local = null ; private DiscoveryAgent agent = null ; private Vector devices = null ; private ServiceRecord [] services = null ; public boolean i s D e v i c e S e a r c h C o m p l e t e d = false ; public boolean i s S e r v i c e S e a r c h C o m p l e t e d = false ; public B l u e t o o t h D i s c o v e r y () { } public void deviceDiscovered ( RemoteDevice remoteDevice , DeviceClass deviceClass ) { if (! this . devices . contains ( remoteDevice ) ) { this . devices . addElement ( remoteDevice ) ; try { Logger . log ( " * device_found__ " + remoteDevice . getFriendlyName ( false ) + " * " , true ) ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " bt :: couldnt get device name " ) ; } } } public void inquiryCompleted ( int discType ) { switch ( discType ) { case D i s c o v e r y L i s t e n e r . I N Q U I R Y _ C O M P L E T E D : // Inquiry completed normally if ( this . devices . size () > 0) { this . i s D e v i c e S e a r c h C o m p l e t e d = true ; } else { // ErrorHandling ErrorMessage . showError ( " bt :: no devices found " ) ; } break ; case D i s c o v e r y L i s t e n e r . INQUIRY_ERROR : // Error during inquiry ErrorMessage . showError ( " bt :: error during inqury " ) ; break ; case D i s c o v e r y L i s t e n e r . I N Q U I R Y _ T E R M I N A T E D : // Inquiry terminated by agent ErrorMessage . showError ( " bt :: inquiry terminated by agent " ) ; break ; } setChanged () ; notifyObservers ( " deviceSearch " ) ; } public void s e r v i c e S e a r c h C o m p l e t e d ( int transactionID , int responseCode ) { this . i s S e r v i c e S e a r c h C o m p l e t e d = true ; switch ( responseCode ) { case D i s c o v e r y L i s t e n e r . S E R V I C E _ S E A R C H _ C O M P L E T E D : // Service search completed normally this . i s S e r v i c e S e a r c h C o m p l e t e d = true ; break ; case D i s c o v e r y L i s t e n e r . S E R V I C E _ S E A R C H _ D E V I C E _ N O T _ R E A C H A B L E : // Searchable device not reachable break ; case D i s c o v e r y L i s t e n e r . S E R V I C E _ S E A R C H _ E R R O R : // Some error occured during service search break ; case D i s c o v e r y L i s t e n e r . S E R V I C E _ S E A R C H _ N O _ R E C O R D S : // Searchable device provides no services break ; case D i s c o v e r y L i s t e n e r . S E R V I C E _ S E A R C H _ T E R M I N A T E D : // Service search terminated by user break ; } setChanged () ; notifyObservers ( " serviceSearch " ) ; } public void s e r v i c e s D i s c o v e r e d ( int transactionID , 56 ANHANG A. SOURCECODE ServiceRecord [] serviceRecord ) { this . services = serviceRecord ; } public void d o D e v i c e D i s c o v e r y () { try { this . local = LocalDevice . getLocalDevice () ; } catch ( B l u e t o o t h S t a t e E x c e p t i o n e ) { // ErrorHandling ErrorMessage . showError ( " bt :: couldnt get local device " ) ; } this . agent = this . local . g e t D i s c o v e r y A g e n t () ; this . devices = new Vector () ; this . i s D e v i c e S e a r c h C o m p l e t e d = false ; try { if (! agent . startInquiry ( DiscoveryAgent . GIAC , this ) ) { // ErrorHandling ErrorMessage . showError ( " bt :: couldnt get agent " ) ; } } catch ( B l u e t o o t h S t a t e E x c e p t i o n e ) { // ErrorHandling ErrorMessage . showError ( " bt :: couldnt search devices " ) ; } } public void doServiceSearch ( RemoteDevice device ) { // S e r v i c e R e c o r d H a n d l e (0 x0000 ) // S e r v i c e C l a s s I D L i s t (0 x0001 ) // S e r v i c e R e c o r d S t a t e (0 x0002 ) // ServiceID (0 x0003 ) // P r o t o c o l D e s c r i p t o r L i s t (0 x0004 ) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ServiceName (0 x100 ) // S e r v i c e D e s c r i p t i o n (0 x101 ) // ProviderName (0 x102 ) int [] attributes = { 0 x100 , 0 x101 , 0 x102 }; UUID [] uuids = new UUID [1]; uuids [0] = new UUID (0 x1002 ) ; this . i s S e r v i c e S e a r c h C o m p l e t e d = false ; try { this . agent . searchServices ( attributes , uuids , device , this ) ; } catch ( B l u e t o o t h S t a t e E x c e p t i o n e ) { // ErrorHandling ErrorMessage . showError ( " bt :: couldnt search services " ) ; } } public Vector getDevices () { return this . devices ; } public ServiceRecord [] getServices () { return this . services ; } } Listing A.6: Button.java package util . ui ; import javax . microedition . lcdui . Font ; import javax . microedition . lcdui . Graphics ; public class Button implements Renderable { private Font font = Font . getFont ( Font . FACE_MONOSPACE , Font . STYL E_PLAIN , Font . SIZE_SMALL ) ; private Font boldFont = Font . getFont ( Font . FACE_MONOSPACE , Font . STYLE_BOLD , Font . SIZE_SMALL ) ; private boolean isSelected = false ; private String text = null ; private double value = 0.0; private int left = 0; private int top = 0; private int width = 0; private int height = 0; 57 ANHANG A. SOURCECODE private int textX = 0; private int textY = 0; public Button ( String text , double value ) { this . text = text ; this . value = value ; } public Button ( String text , double value , int left , int top , int width , int height ) { this . text = text ; this . value = value ; this . left = left ; this . top = top ; this . width = width ; this . height = height ; this . textX = this . left + ( this . width / 2) ; this . textY = this . top + ( this . height / 2) ; } public void paint ( Graphics g ) { if ( this . isSelected ) { g . setColor ( Colors . green_bright ) ; } else { g . setColor ( Colors . green_dark ) ; } g . fillRoundRect ( this . left , this . top , this . width , this . height , 15 , 15) ; g . setColor ( Colors . black ) ; g . drawRoundRect ( this . left , this . top , this . width , this . height , 15 , 15) ; if ( this . isSelected ) { g . setFont ( this . boldFont ) ; g . drawString ( this . text , this . textX , this . textY + ( this . boldFont . getHeight () / 2) , Graphics . HCENTER | Graphics . BOTTOM ) ; } else { g . setFont ( this . font ) ; g . drawString ( this . text , this . textX , this . textY + ( this . font . getHeight () / 2) , Graphics . HCENTER | Graphics . BOTT OM ) ; } } public void setDimension ( int left , int top , int width , int height ) { this . left = left ; this . top = top ; this . width = width ; this . height = height ; this . textX = this . left + ( this . width / 2) ; this . textY = this . top + ( this . height / 2) ; } public void unselect () { this . isSelected = false ; } public void select () { this . isSelected = true ; } public void toggleSelect () { this . isSelected = ! this . isSelected ; } public boolean isSelected () { return this . isSelected ; } public double getValue () { return this . value ; } } Listing A.7: Colors.java package util . ui ; public class Colors { public static final int white = ( int ) 0 x00FFFFFF ; public static final int black = ( int ) 0 x00000000 ; 58 ANHANG A. SOURCECODE public static final int grey = ( int ) 0 x007D7D7D ; public static final int red_bright = ( int ) 0 x00FF0000 ; public static final int red_dark = ( int ) 0 x00FF7D7D ; public static final int green_bright = ( int ) 0 x0000FF00 ; public static final int green_dark = ( int ) 0 x007DFF7D ; public static final int blue_bright = ( int ) 0 x000000FF ; public static final int blue_dark = ( int ) 0 x007D7DFF ; public static final int [] potbanging = new int [] { ( int ) 0 x00FF0000 , ( int ) 0 x00CC0000 , ( int ) 0 x00990000 , ( int ) 0 x00660000 , ( int ) 0 x007d0068 , ( int ) 0 x0068007d , ( int ) 0 x00000066 , ( int ) 0 x00000099 , ( int ) 0 x000000CC , ( int ) 0 x000000FF }; public static final int [] crossgolf = new int [] { ( int ) 0 x00FF0000 , // Tee 0 ( int ) 0 x00000000 , // Hole 1 ( int ) 0 x0000c800 , // Fairway 2 ( int ) 0 x00009600 , // Rough 3 ( int ) 0 x0000ff00 , // Green 4 ( int ) 0 x00ffff00 , // Bunker 5 ( int ) 0 x006365ff // Water 6 }; } Listing A.8: Controller.java package util . game ; import gps . GPS ; import gps . GPSRecord ; import javax . microedition . lcdui . Canvas ; import javax . microedition . lcdui . Font ; import util . ui . Renderable ; public abstract class Controller implements Renderable { protected Font font = Font . getFont ( Font . FACE_MONOSPACE , Font . STYL E_PLAIN , Font . SIZE_SMALL ) ; protected int width = 0; protected int height = 0; protected GPS gps = null ; protected GPSRecord gpsRecord = null ; protected boolean isDebugMode = true ; public Controller ( int width , int height , GPS gps ) { this . width = width ; this . height = height ; this . gps = gps ; } public abstract void update () ; public abstract void keyPressed ( int keyCode , Canvas c ) ; public abstract void keyReleased ( int keyCode , Canvas c ) ; public abstract void keyRepeated ( int keyCode , Canvas c ) ; public abstract void pointerPressed ( int x , int y ) ; public abstract void pointerReleased ( int x , int y ) ; public abstract void pointerDragged ( int x , int y ) ; } Listing A.9: CrossGolfController.java package crossgolf ; 59 ANHANG A. SOURCECODE import java . io . IOException ; import java . util . Random ; import java . util . Vector ; import gps . GPS ; import gps . GPSRecord ; import import import import javax . microedition . lcdui . Canvas ; javax . microedition . lcdui . Font ; javax . microedition . lcdui . Graphics ; javax . microedition . lcdui . Image ; import log . Logger ; import import import import import import import import util . error . ErrorMessage ; util . game . Controller ; util . game . GameState ; util . game . Golfclub ; util . game . Point ; util . ui . Bar ; util . ui . Colors ; util . ui . Menu ; public class C r o s s G o l f C o n t r o l l e r extends Controller { public static final int G_TEE = 0; public static final int G_HOLE = 1; public static final int G_FAIRWAY = 2; public static final int G_ROUGH = 3; public static final int G_GREEN = 4; public static final int G_BUNKER = 5; public static final int G_WATER = 6; public GameState gs = null ; private static final int INIT_TIME = 6000; private Font bigFont = Font . getFont ( Font . FACE_MONOSPACE , Font . S TYLE_BOLD , Font . SIZE_LARGE ) ; private boolean isInitialized = false ; private boolean showMenu = true ; private boolean hasEnded = false ; private Menu crossMenu = null ; private long hours = 0; private long minutes = 0; private long seconds = 0; private long startTime = 0; private long endTime = 0; private long currentInitTime = 0; private long endInitTime = 0; private Bar meter = null ; private Image map = null ; private int [] raw = null ; private int imgW = 0; private int imgH = 0; private double factor ; private double holeDistanceGame = 0.0; private double holeDistanceReal = 0.0; private double directionRad = 0.0; 60 ANHANG A. SOURCECODE private int direction = 0; private int dirX = 0; private int dirY = 0; private int windX = 0; private int windY = 0; private double diffX = 0; private double diffY = 0; private double windSpeed = 0.0; private long windTimer = 0; private int clubIndex = 0; private Vector clubs = null ; private GPSRecord newBallPos = null ; private GPSRecord lastBallPos = null ; private Point center = null ; private Point centerOld = null ; private Point target = null ; private Point tee = null ; private Point ball = null ; private Point hole = null ; private Point player = null ; private String shotsText = " " ; private String timeText = " " ; private String calcText = " " ; private boolean calcDisp = false ; private boolean calcEnd = false ; private int calcTime = 0; private int shots = 0; public C r o s s G o l f C o n t r o l l e r ( int width , int height , GPS gps ) { super ( width , height , gps ) ; this . crossMenu = new Menu ( " Select Hole Distance : " , width , height ) ; this . crossMenu . addButton ( " 100 Meter " , 100.0) ; this . crossMenu . addButton ( " 300 Meter " , 300.0) ; this . crossMenu . addButton ( " 500 Meter " , 500.0) ; this . crossMenu . addButton ( " 1000 Meter " , 1000.0) ; Logger . log ( " * menu * " + " start " , true ) ; try { this . map = Image . createImage ( " / res / map1 . gif " ) ; this . imgW = this . map . getWidth () ; this . imgH = this . map . getHeight () ; this . raw = new int [ this . imgW * this . imgH ]; this . map . getRGB ( this . raw , 0 , this . imgW , 0 , 0 , this . imgW , this . imgH ) ; for ( int j = 0; j < this . imgH ; j ++) { for ( int i = 0; i < this . imgW ; i ++) { if ( equalsColor ( Colors . crossgolf [ G_TEE ] , getPixel (i , j , this . raw ) ) ) { this . tee = new Point (i , j ) ; this . center = new Point (i , j ) ; this . centerOld = new Point (i , j ) ; this . ball = new Point (i , j ) ; this . player = new Point (i , j ) ; } else if ( equalsColor ( Colors . crossgolf [ G_HOLE ] , getPixel (i , j , this . raw ) ) ) { this . hole = new Point (i , j ) ; } } } 61 ANHANG A. SOURCECODE this . gs = new GameState () ; this . clubs = new Vector () ; this . clubs . addElement ( new Golfclub ( " Wood " , 65 , 120) ) ; this . clubs . addElement ( new Golfclub ( " Iron " , 45 , 100) ) ; this . clubs . addElement ( new Golfclub ( " Wedge " , 20 , 50) ) ; this . clubs . addElement ( new Golfclub ( " Putter " , 10 , 0) ) ; this . meter = new Bar (0.0 , 100.0 , 0.0 , 4 , 4 , 10 , 100) ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " img :: couldnt load image " ) ; } } public void update () { if (! this . hasEnded ) { if (! this . showMenu ) { if ( this . isInitialized ) { updatePosition () ; updateDistance () ; updateWind () ; if ( this . gs . getState () == GameState . DRIVE ) { this . meter . inc (5.0) ; } else if ( this . gs . getState () == GameState . FLY ) { if ( this . ball . x != this . target . x ) { this . ball . x += this . diffX ; } if ( this . ball . y != this . target . y ) { this . ball . y += this . diffY ; } if (( this . ball . x <= this . target . x + 1) && ( this . ball . x >= this . target . x - 1) && ( this . ball . y <= this . target . y + 1) && ( this . ball . y >= this . target . y - 1) ) { this . ball . x = this . target . x ; this . ball . y = this . target . y ; this . centerOld . x = this . center . x ; this . centerOld . y = this . center . y ; this . center . x = this . target . x ; this . center . y = this . target . y ; this . gs . nextState () ; } if ((( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . name . equals ( " Putter " ) ) { for ( int j = -1; j <= 1; j ++) { for ( int i = -1; i < 1; i ++) { if ( equalsColor ( getPixel (( int ) this . ball . x + i , ( int ) this . ball . y + j , this . raw ) , Colors . crossgolf [ G_HOLE ]) ) { this . ball . x = this . hole . x ; this . ball . y = this . hole . y ; this . endTime = System . c u r r e n t T i m e M i l l i s () ; long time = this . endTime - this . startTime ; this . hours = time / 3600000; this . minutes = ( time - this . hours * 3600000) / 60000; this . seconds = ( time - ( this . hours * 3600000 + this . minutes * 60000) ) / 1000; this . calcText = " gratulations " ; this . timeText = " time needed - " + this . hours + " : " + this . minutes + " : " + this . seconds ; this . shotsText = " shots needed - " + this . shots ; Logger . log ( " * end * " + time , true ) ; this . gs . setState ( GameState . END ) ; } } } } } else if ( this . gs . getState () == GameState . CALC ) { if ( this . calcDisp ) { if ( this . calcTime <= 0) { if ( this . calcEnd ) { this . calcText = " " ; resetBall () ; } else { this . calcText = " move to ball " ; this . player . x = this . centerOld . x ; this . player . y = this . centerOld . y ; g e t N e w B a l l P o s i t i o n () ; this . gs . nextState () ; } this . calcDisp = false ; this . calcEnd = false ; } else { this . calcTime - -; } 62 ANHANG A. SOURCECODE } else { if (( this . ball . x < 0) || ( this . ball . x > this . imgW ) || ( this . ball . y < 0) || ( this . ball . y > this . imgH ) ) { this . calcText = " out of bounds " ; this . calcEnd = true ; } else if ( equalsColor ( getPixel (( int ) this . ball .x , ( int ) this . ball .y , this . raw ) , Colors . crossgolf [ G_WATER ]) ) { this . calcText = " water " ; this . calcEnd = true ; } else if ( equalsColor ( getPixel (( int ) this . ball .x , ( int ) this . ball .y , this . raw ) , Colors . crossgolf [ G_BUNKER ]) ) { this . calcText = " bunker " ; } else { this . calcText = " nice shot " ; } this . calcDisp = true ; this . calcTime = 50; } } else if ( this . gs . getState () == GameState . MOVE ) { double dX = this . gpsRecord . getDistanceX ( this . lastBallPos ) ; double dY = this . gpsRecord . getDistanceY ( this . lastBallPos ) ; this . player . x = this . centerOld . x + dX * factor ; this . player . y = this . centerOld . y + dY * factor ; if (( this . player . x <= this . ball . x + 4) && ( this . player . x >= this . ball . x - 4) && ( this . player . y <= this . ball . y + 4) && ( this . player . y >= this . ball . y - 4) ) { this . calcText = " " ; this . meter . reset () ; this . gs . nextState () ; } } } else { this . currentInitTime = System . c u r r e n t T i m e M i l l i s () ; if ( this . currentInitTime >= this . endInitTime ) { this . startTime = this . currentInitTime ; Logger . log ( " * initialize * " + " end " , true ) ; this . isInitialized = true ; } else { GPSRecord temp = this . gps . getRecord () ; if ( temp != null ) { this . gpsRecord = temp ; Logger . log ( " * gps * " + this . gpsRecord . getRawString () , true ) ; } } } } } } public void paint ( Graphics g ) { if ( this . showMenu ) { this . crossMenu . paint ( g ) ; } else { if ( this . isInitialized ) { g . setColor ( Colors . grey ) ; g . fillRect (0 , 0 , this . width , this . height ) ; if ( this . gs . getState () == GameState . MOVE ) { g . drawImage ( this . map , ( this . imgW / 2) , ( int ) ( - this . player . y + ( this . height / 2) ) , Graphics . HCENTER | Graphics . TOP ) ; } else if ( this . gs . getState () != GameState . FLY ) { g . drawImage ( this . map , ( this . imgW / 2) , ( int ) ( - this . center . y + ( this . height / 2) ) , Graphics . HCENTER | Graphics . TOP ) ; } else { g . drawImage ( this . map , ( this . imgW / 2) , ( int ) ( - this . ball . y + ( this . height / 2) ) , Graphics . HCENTER | Graphics . TOP ) ; } if (( this . gs . getState () != GameState . END ) && ( this . gs . getState () != GameState . MOVE ) ) { this . meter . paint ( g ) ; } drawDirection ( g ) ; drawInfo ( g ) ; drawWind ( g ) ; drawTarget ( g ) ; drawBall ( g ) ; drawPlayer ( g ) ; if ( this . isDebugMode ) { if ( this . gs . getState () == GameState . MOVE ) { g . setColor ( Colors . white ) ; g . setFont ( this . font ) ; 63 ANHANG A. SOURCECODE g . drawString ( " tp : " + this . newBallPos . toString () , 2 , this . height - (0 * this . font . getHeight () ) , Graphics . LEFT | Graphics . BOTTOM ) ; g . drawString ( " sp : " + this . lastBallPos . toString () , 2 , this . height - (1 * this . font . getHeight () ) , Graphics . LEFT | Graphics . BOTTOM ) ; g . drawString ( " - - - - - - - - -" , 2 , this . height - (2 * this . font . getHeight () ) , Graphics . LEFT | Graphics . BOTTOM ) ; g . drawString ( " cp : " + this . gpsRecord . toString () , 2 , this . height - (3 * this . font . getHeight () ) , Graphics . LEFT | Graphics . BOTTOM ) ; g . drawString ( " cd : " + this . gpsRecord . getDistanceTo ( this . newBallPos ) + " Meter " , 2 , this . height - (4 * this . font . getHeight () ) , Graphics . LEFT | Graphics . BOTTOM ) ; } } } else { g . setColor ( Colors . grey ) ; g . fillRect (0 , 0 , this . width , this . height ) ; g . setFont ( this . font ) ; g . setColor ( Colors . white ) ; g . drawString ( " stand still for a moment " , this . width / 2 , this . height / 2 - ( this . font . getHeight () + 2) , Graphics . HCENTER | Graphics . BASELINE ) ; g . drawString ( " the game is initializing " , this . width / 2 , this . height / 2 + ( this . font . getHeight () + 2) , Graphics . HCENTER | Graphics . BASELINE ) ; } } } public void resetBall () { this . shots ++; this . center . x = this . centerOld . x ; this . center . y = this . centerOld . y ; this . ball . x = this . center . x ; this . ball . y = this . center . y ; this . gs . setState ( GameState . SET ) ; } public void g e t N e w B a l l P o s i t i o n () { this . lastBallPos = this . gpsRecord ; double diffX = this . centerOld . x - this . center . x ; double diffY = this . centerOld . y - this . center . y ; double realDiffX = diffX / this . factor ; double realDiffY = diffY / this . factor ; this . newBallPos = this . lastBallPos . getNewPoint ( realDiffX , realDiffY ) ; } public void updatePosition () { GPSRecord temp = this . gps . getRecord () ; if ( temp != null ) { this . gpsRecord = temp ; Logger . log ( " * gps * " + this . gpsRecord . getRawString () , true ) ; } } public void updateDistance () { if ( this . gs . getState () == GameState . SET ) { this . directionRad = this . direction * ( Math . PI / 180.0) ; this . dirX = ( int ) ((( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . maxDistance * ( this . holeDistanceReal / 100.0) * factor * Math . cos ( this . directionRad ) ) ; this . dirY = ( int ) ((( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . maxDistance * ( this . holeDistanceReal / 100.0) * factor * Math . sin ( this . directionRad ) ) ; } } public void updateWind () { if ( this . gs . getState () == GameState . SET ) { if ( this . windTimer == 0) { Random randomGen = new Random () ; randomGen . setSeed ( System . c u r r e n t T i m e M i l l i s () ) ; int random = randomGen . nextInt (360) ; double dir = random * ( Math . PI / 180.0) ; this . windSpeed = ( int ) ( randomGen . nextInt (20) * ( this . holeDistanceReal / 100.0) ) ; this . windX = ( int ) ( this . windSpeed * Math . cos ( dir ) ) ; this . windY = ( int ) ( this . windSpeed * Math . sin ( dir ) ) ; this . windTimer = 50; } else { this . windTimer - -; } } } public void u p d a t e C u r r e n t P o i n t () { 64 ANHANG A. SOURCECODE int tempX = ( int ) ((( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . maxDistance * this . meter . getValue () / 100.0 * ( this . holeDistanceReal / 100.0) * factor * Math . cos ( this . directionRad ) ) ; int tempY = ( int ) ((( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . maxDistance * this . meter . getValue () / 100.0 * ( this . holeDistanceReal / 100.0) * factor * Math . sin ( this . directionRad ) ) ; int tempWX = ( int ) ( - this . windX * (( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . maxWind / 100.0) ; int tempWY = ( int ) ( - this . windY * (( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . maxWind / 100.0) ; this . meter . reset () ; this . target = new Point (( int ) ( this . center . x + tempX + tempWX ) , ( int ) ( this . center . y - tempY + tempWY ) ) ; this . diffX = ( this . target . x - this . ball . x ) / 20.0; this . diffY = ( this . target . y - this . ball . y ) / 20.0; } public void drawPlayer ( Graphics g ) { g . setColor ( Colors . blue_bright ) ; if ( this . gs . getState () == GameState . MOVE ) { g . fillRect (( int ) this . player .x , ( this . height / 2) , 2 , 2) ; } } public void drawDirection ( Graphics g ) { if ( this . gs . getState () == GameState . SET ) { g . setColor ( Colors . black ) ; g . setStrokeStyle ( Graphics . DOTTED ) ; g . drawLine (( int ) this . center .x , ( this . height / 2) , ( int ) ( this . dirX + this . center . x ) , ( int ) ( - this . dirY + ( this . height / 2) ) ) ; } } public void drawInfo ( Graphics g ) { g . setFont ( this . bigFont ) ; g . setColor ( Colors . white ) ; g . drawString ( this . calcText , this . width / 2 , 20 + (0 * this . bigFont . getHeight () ) , Graphics . HCENTER | Graphics . TOP ) ; g . drawString ( this . timeText , this . width / 2 , 20 + (1 * this . bigFont . getHeight () ) , Graphics . HCENTER | Graphics . TOP ) ; g . drawString ( this . shotsText , this . width / 2 , 20 + (2 * this . bigFont . getHeight () ) , Graphics . HCENTER | Graphics . TOP ) ; if (( this . gs . getState () != GameState . END ) && ( this . gs . getState () != GameState . MOVE ) ) { g . setFont ( this . font ) ; g . drawString ( " Club : " + (( Golfclub ) this . clubs . elementAt ( this . clubIndex ) ) . name , 2 , this . height - 2 - (0 * ( this . font . getHeight () ) ) , Graphics . LEFT | Graphics . BOTTOM ) ; g . drawString ( this . direction + " ◦ " , this . width - 2 , this . height - 2 - (0 * this . font . getHeight () ) , Graphics . RIGHT | Graphics . BOTTOM ) ; } } public void drawWind ( Graphics g ) { if (( this . gs . getState () != GameState . END ) && ( this . gs . getState () != GameState . MOVE ) ) { g . setFont ( this . font ) ; g . setColor ( Colors . white ) ; g . drawString ( " Wind : " + this . windSpeed + " m / s " , this . width - 2 , 2 , Graphics . RIGHT | Graphics . TOP ) ; g . setStrokeStyle ( Graphics . DOTTED ) ; g . drawLine ( this . width - 14 - 5 , this . font . getHeight () + 14 , this . width - 14 + 5 , this . font . getHeight () + 14) ; g . drawLine ( this . width - 14 , this . font . getHeight () + 14 - 5 , this . width - 14 , this . font . getHeight () + 14 + 5) ; g . setStrokeStyle ( Graphics . SOLID ) ; g . drawLine ( this . width - 14 , this . font . getHeight () + 14 , this . width - 14 - this . windX , this . font . getHeight () + 14 - this . windY ) ; } } public void drawBall ( Graphics g ) { g . setColor ( Colors . white ) ; if ( this . gs . getState () == GameState . MOVE ) { g . fillRect (( int ) this . ball .x , ( int ) ( this . ball . y - this . player . y + ( this . height / 2) ) , 2 , 2) ; } else if ( this . gs . getState () != GameState . FLY ) { g . fillRect (( int ) this . ball .x , 65 ANHANG A. SOURCECODE ( int ) ( this . ball . y - this . center . y + ( this . height / 2) ) , 2 , 2) ; } else { g . fillRect (( int ) this . ball .x , ( this . height / 2) , 2 , 2) ; } } public void drawTarget ( Graphics g ) { if ( this . target != null ) { g . setColor ( Colors . blue_bright ) ; if ( this . gs . getState () == GameState . DRIVE ) { g . fillRect (( int ) this . target .x , ( int ) ( this . target . y - this . center . y + ( this . height / 2) ) , 2 , 2) ; } else if ( this . gs . getState () == GameState . FLY ) { g . fillRect (( int ) this . target .x , ( int ) ( this . target . y - this . ball . y + ( this . height / 2) ) , 2 , 2) ; } } } public int getPixel ( int x , int y , int [] raw ) { return raw [ y * this . imgW + x ]; } public static boolean equalsColor ( int colorA , int colorB ) { int redA = ( colorA & 0 x00ff0000 ) >> 16; int greenA = ( colorA & 0 x0000ff00 ) >> 8; int blueA = colorA & 0 x000000ff ; int redB = ( colorB & 0 x00ff0000 ) >> 16; int greenB = ( colorB & 0 x0000ff00 ) >> 8; int blueB = colorB & 0 x000000ff ; if (( redA == redB ) && ( greenA == greenB ) && ( blueA == blueB ) ) { return true ; } return false ; } public void keyPressed ( int keyCode , Canvas c ) { switch ( keyCode ) { case Canvas . KEY_POUND : this . isDebugMode = ! this . isDebugMode ; break ; default : switch ( c . getGameAction ( keyCode ) ) { case Canvas . UP : if ( this . showMenu ) { this . crossMenu . selectUp () ; } else { if ( gs . getState () == GameState . SET ) { this . clubIndex - -; if ( this . clubIndex == -1) { this . clubIndex = this . clubs . size () - 1; } updateDistance () ; } } break ; case Canvas . DOWN : if ( this . showMenu ) { this . crossMenu . selectDown () ; } else { if ( gs . getState () == GameState . SET ) { this . clubIndex ++; if ( this . clubIndex == this . clubs . size () ) { this . clubIndex = 0; } updateDistance () ; } } break ; case Canvas . LEFT : if ( this . showMenu ) { this . crossMenu . selectUp () ; } else { if ( gs . getState () == GameState . SET ) { this . direction += 5; if ( this . direction == 360) { this . direction = 0; } updateDistance () ; } } break ; case Canvas . RIGHT : if ( this . showMenu ) { 66 ANHANG A. SOURCECODE this . crossMenu . selectDown () ; } else { if ( gs . getState () == GameState . SET ) { this . direction -= 5; if ( this . direction == -5) { this . direction = 360 - 5; } updateDistance () ; } } break ; case Canvas . FIRE : if ( this . showMenu ) { this . holeDistanceReal = this . crossMenu . getSelectedValue () ; this . holeDistanceGame = this . tee . y - this . hole . y ; this . factor = this . holeDistanceGame / this . holeDistanceReal ; this . endInitTime = System . c u r r e n t T i m e M i l l i s () ; this . endInitTime += INIT_TIME ; Logger . log ( " * menu * " + " end " , true ) ; Logger . log ( " * initialize * " + " start " , true ) ; this . showMenu = false ; } else { if ( gs . getState () == GameState . SET ) { gs . nextState () ; } else if ( gs . getState () == GameState . DRIVE ) { u p d a t e C u r r e n t P o i n t () ; this . shots ++; gs . nextState () ; } } break ; } break ; } } public void keyReleased ( int keyCode , Canvas c ) { switch ( keyCode ) { default : break ; } } public void keyRepeated ( int keyCode , Canvas c ) { switch ( keyCode ) { case Canvas . KEY_STAR : this . isDebugMode = ! this . isDebugMode ; break ; default : break ; } } public void pointerDragged ( int x , int y ) { } public void pointerPressed ( int x , int y ) { } public void pointerReleased ( int x , int y ) { } } Listing A.10: ErrorMessage.java package util . error ; import import import import javax . microedition . lcdui . Alert ; javax . microedition . lcdui . AlertType ; javax . microedition . lcdui . Display ; javax . microedition . lcdui . Displayable ; import log . Logger ; public class ErrorMessage extends Alert { private static ErrorMessage instance = null ; private static Display display ; private ErrorMessage () { super ( " Error " ) ; setType ( AlertType . ERROR ) ; 67 ANHANG A. SOURCECODE setTimeout (3000) ; setImage ( null ) ; } public static void init ( Display d ) { display = d ; } public static void showError ( String message ) { if ( instance == null ) { instance = new ErrorMessage () ; } instance . setString ( message ) ; display . setCurrent ( instance ) ; Logger . log ( " * error * " + message , true ) ; } public static void showError ( String message , Displayable next ) { if ( instance == null ) { instance = new ErrorMessage () ; } instance . setString ( message ) ; display . setCurrent ( instance , next ) ; Logger . log ( " error : " + message , true ) ; } } Listing A.11: GameState.java package util . game ; import log . Logger ; public class GameState { public static final int SET = 0; public static final int DRIVE = 1; public static final int FLY = 2; public static final int CALC = 3; public static final int MOVE = 4; public static final int END = 5; private boolean endGame = false ; private int currentState = SET ; public GameState () { } public void nextState () { if (! this . endGame ) { this . currentState = ( this . currentState + 1) ; if ( this . currentState > MOVE ) { this . currentState = SET ; } Logger . log ( " * gamestate * " + this . currentState , true ) ; } } public void setState ( int state ) { if (! this . endGame ) { this . currentState = state ; if ( this . currentState == END ) { this . endGame = true ; } Logger . log ( " * gamestate * " + this . currentState , true ) ; } } public int getState () { return this . currentState ; } } Listing A.12: Golfclub.java package util . game ; 68 ANHANG A. SOURCECODE public class Golfclub { public String name = null ; public double maxDistance = 0.0; public double maxWind = 0.0; public Golfclub ( String name , double maxDistance , double maxWind ) { this . name = name ; this . maxDistance = maxDistance ; this . maxWind = maxWind ; } } Listing A.13: GPS.java package gps ; import java . io . IOException ; import util . error . ErrorMessage ; import bluetooth . B l u e t o o t h C o n n e c t i o n ; public class GPS implements Runnable { private static final int BREAK = 2000; private static final int LINE_DELIMITER = 13; private B l u e t o o t h C o n n e c t i o n connection = null ; private GPSRecordBuffer buffer = null ; private Thread engine = null ; public GPS ( B l u e t o o t h C o n n e c t i o n connection ) { this . connection = connection ; this . buffer = new GPSRecordBuffer () ; } public void run () { Thread currentThread = Thread . currentThread () ; while ( currentThread == this . engine ) { try { String output = new String () ; int input ; while (( input = this . connection . read () ) != LINE_DELIMITER ) { output += ( char ) input ; } output = output . substring (1 , output . length () - 1) ; GPSRecord record = GPSParser . parse ( output ) ; if ( record != null ) { buffer . putRecord ( record ) ; } } catch ( IOException ie ) { try { Thread . sleep ( BREAK ) ; } catch ( I n t e r r u p t e d E x c e p t i o n e ) { ErrorMessage . showError ( " thread :: sleep error " ) ; } ErrorMessage . showError ( " gps :: parsing error " ) ; } } } public void start () { if ( this . engine == null ) { try { this . connection . connect () ; } catch ( IOException e ) { ErrorMessage . showError ( " gps :: connection error " ) ; } this . engine = new Thread ( this ) ; this . engine . start () ; } } public void stop () { this . connection . disconnect () ; this . engine = null ; } public GPSRecord getRecord () { 69 ANHANG A. SOURCECODE return buffer . getRecord () ; } } Listing A.14: GPSParser.java package gps ; public class GPSParser { private static final String DELIMETER = " ," ; public GPSParser () { } public static GPSRecord parse ( String record ) { if ( record . startsWith ( " $GPRMC " ) == true ) { String currentValue = record ; int nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Date time of fix nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String dateTimeOfFix = currentValue . substring (0 , nextTo kenIndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Warning nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String warning = currentValue . substring (0 , nextTokenInd ex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Lattitude nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String lattitude = currentValue . substring (0 , nextTokenI ndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Lattitude direction nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String l a t t i t u d e D i r e c t i o n = currentValue . substring (0 , n extTokenIndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Longitude nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String longitude = currentValue . substring (0 , nextTokenI ndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Longitude direction nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String l o n g i t u d e D i r e c t i o n = currentValue . substring (0 , n extTokenIndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Ground speed nextTokenIndex = currentValue . indexOf ( DELIMETER ) ; String groundSpeed = currentValue . substring (0 , nextToke nIndex ) ; currentValue = currentValue . substring ( nextTokenIndex + 1) ; // Course String courseMadeGood = currentValue ; double longitudeDouble = 0.0; double latitudeDouble = 0.0; double longitudeRad = 0.0; double latitudeRad = 0.0; double speed = 0.0; if (( longitude . length () > 0) && ( lattitude . length () > 0) ) { longitudeDouble = parseDegValue ( longitude , false ) ; if ( l o n g i t u d e D i r e c t i o n . equals ( " E " ) == false ) { longitudeDouble = - longitudeDouble ; } longitudeRad = parseRadValue ( longitudeDouble ) ; latitudeDouble = parseDegValue ( lattitude , true ) ; if ( l a t t i t u d e D i r e c t i o n . equals ( " N " ) == false ) { latitudeDouble = - latitudeDouble ; } latitudeRad = parseRadValue ( latitudeDouble ) ; speed = Double . parseDouble ( groundSpeed ) ; } if ( warning . equals ( " A " ) == true ) { GPSRecord pos = new GPSRecord ( record , longitude , lattitude , 0 , longitudeDouble , latitudeDouble , longitudeRad , latitud eRad , speed ) ; return pos ; } return null ; } else { return null ; } } private static double parseDegValue ( String valueString , boolean isLongitude ) { int degreeInteger = 0; double minutes = 0.0; if ( isLongitude ) { 70 ANHANG A. SOURCECODE degreeInteger = Integer . parseInt ( valueString . substrin g (0 , 2) ) ; minutes = Double . parseDouble ( valueString . substring (2) ) ; } else { degreeInteger = Integer . parseInt ( valueString . substrin g (0 , 3) ) ; minutes = Double . parseDouble ( valueString . substring (3) ) ; } double degreeDecimals = minutes / 60.0; double degrees = degreeInteger + degreeDecimals ; return degrees ; } private static double parseRadValue ( double valueDeg ) { return ( valueDeg * ( Math . PI / 180.0 f ) ) ; } } Listing A.15: GPSRecord.java package gps ; import java . util . Calendar ; import java . util . Date ; import java . util . Random ; public class GPSRecord { private static final int RADIUS = 6378000; private String mRawData ; private String mLongitudeString ; private double mLongitude ; private double mLongitudeRad ; private String mLatitudeString ; private double mLatitude ; private double mLatitudeRad ; private double mSpeed ; private int mElevation ; private Date mPositionDate ; public GPSRecord ( double longitudeRad , double latitudeRad ) { this . mRawData = " not messured , " + longitudeRad + " , " + latitudeRad ; this . mLongitudeString = " not messured " ; this . mLatitudeString = " not messured " ; this . mElevation = 0; Calendar cal = Calendar . getInstance () ; this . mPositionDate = cal . getTime () ; this . mLongitude = longitudeRad / ( Math . PI / 180.0 f ) ; this . mLongitudeRad = longitudeRad ; this . mLatitude = latitudeRad / ( Math . PI / 180.0 f ) ; this . mLatitudeRad = latitudeRad ; } public GPSRecord ( String rawData , String longitude , String latitude , int elevation , double longitudeDouble , double latitudeDouble , double longitudeRad , double latitudeRad , double speed ) { this . mRawData = rawData ; this . mLongitudeString = longitude ; this . mLatitudeString = latitude ; this . mElevation = elevation ; Calendar cal = Calendar . getInstance () ; this . mPositionDate = cal . getTime () ; this . mLongitude = longitudeDouble ; this . mLongitudeRad = longitudeRad ; this . mLatitude = latitudeDouble ; this . mLatitudeRad = latitudeRad ; } public boolean equals ( GPSRecord position ) { if (( this . mLongitudeString . equals ( position . mLongitudeString ) == true ) && ( this . mLatitudeString . equals ( position . mLatitudeString ) == true ) ) { return true ; } else { return false ; } } 71 ANHANG A. SOURCECODE public String getRawString () { return this . mRawData ; } public Date getDate () { return this . mPositionDate ; } public double getLongitude () { return this . mLongitude ; } public double getLatitude () { return this . mLatitude ; } public double getSpeed () { return this . mSpeed ; } public double getDistanceTo ( GPSRecord r ) { double x = RADIUS * ( r . mLongitudeRad - this . mLongitudeRad ) * Math . cos ( this . mLatitudeRad ) ; double y = RADIUS * ( r . mLatitudeRad - this . mLatitudeRad ) ; return Math . sqrt (( x * x + y * y ) ) ; } public double getDistanceX ( GPSRecord r ) { return RADIUS * ( r . mLongitudeRad - this . mLongitudeRad ) * Math . cos ( this . mLatitudeRad ) ; } public double getDistanceY ( GPSRecord r ) { return RADIUS * ( r . mLatitudeRad - this . mLatitudeRad ) ; } public GPSRecord getRandomPoint ( double distance ) { Random randomGen = new Random () ; randomGen . setSeed ( System . c u r r e n t T i m e M i l l i s () ) ; double random = randomGen . nextDouble () ; double x = distance * Math . cos ( random ) ; double y = distance * Math . sin ( random ) ; int randomInt = randomGen . nextInt (4) ; switch ( randomInt ) { case 1: x *= -1.0 f ; break ; case 2: x *= -1.0 f ; y *= -1.0 f ; break ; case 3: y *= -1.0 f ; break ; } double radLon = ( x / RADIUS * Math . cos ( this . mLatitudeRad ) ) + this . mLongitudeRad ; double radLat = ( y / RADIUS ) + this . mLatitudeRad ; return new GPSRecord ( radLon , radLat ) ; } public GPSRecord getNewPoint ( double dX , double dY ) { double radLon = ( dX / RADIUS * Math . cos ( this . mLatitudeRad ) ) + this . mLongitudeRad ; double radLat = ( dY / RADIUS ) + this . mLatitudeRad ; return new GPSRecord ( radLon , radLat ) ; } public String toString () { String result ; if ( this . mLongitudeString . length () > 0) { result = this . mLongitudeRad + " , " + this . mLatitudeRad + " , " + this . mElevation ; } else { result = " Unknown " ; } return result ; } } Listing A.16: GPSRecordBuffer.java 72 ANHANG A. SOURCECODE package gps ; import log . Logger ; public class GPSRecordBuffer { private GPSRecord record = null ; public GPSRecordBuffer () { } public synchronized GPSRecord getRecord () { return this . record ; } public synchronized void putRecord ( GPSRecord record ) { this . record = record ; } } Listing A.17: Logger.java package log ; import import import import java . io . IOException ; java . io . OutputStream ; java . util . Calendar ; java . util . Date ; import javax . microedition . io . Connector ; import javax . microedition . io . file . FileConnection ; import util . error . ErrorMessage ; public class Logger { private static Logger instance = null ; private static FileConnection con = null ; private static OutputStream os = null ; private static String conUrl = null ; private static Date logDate = null ; private Logger ( String msg ) { Calendar cal = Calendar . getInstance () ; logDate = cal . getTime () ; conUrl = " file :/// e :/ " + logDate . getTime () + " . gps " ; try { con = ( FileConnection ) Connector . open ( conUrl ) ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " log :: couldnt open connection " ) ; } if (! con . exists () ) { try { con . create () ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " log :: couldnt create file " ) ; } } try { os = con . openOutputStream () ; } catch ( IOException e ) { // Errorhandling ErrorMessage . showError ( " log :: couldnt open stream " ) ; } log ( " # " + msg ) ; log ( " # " ) ; log ( " # " + logDate . toString () ) ; log ( " # " ) ; log ( " # Profile .......... " + System . getProperty ( " microedition . profiles " ) ) ; log ( " # Configuration .... " + System . getProperty ( " microedition . configuration " ) ) ; log ( " # " ) ; log ( " # FileConnection ... " + System . getProperty ( " microedition . io . file . FileConnection . version " ) ) ; log ( " # " ) ; log ( " ### " ) ; log ( " " ) ; } 73 ANHANG A. SOURCECODE public static void init ( String msg ) { if ( instance == null ) { instance = new Logger ( msg ) ; } } public static void log ( String msg ) { String bytes = msg + " \ n " ; try { os . write ( bytes . getBytes () ) ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " log :: couldnt write data " ) ; } } public static void log ( String msg , boolean timestamp ) { String bytes = " " ; if ( timestamp ) { Calendar currentTime = Calendar . getInstance () ; bytes += currentTime . get ( Calendar . HOUR_OF_DAY ) ; bytes += " : " ; bytes += currentTime . get ( Calendar . MINUTE ) ; bytes += " : " ; bytes += currentTime . get ( Calendar . SECOND ) ; bytes += " . " ; bytes += currentTime . get ( Calendar . MILLISECOND ) ; bytes += " " ; } bytes += msg + " \ n " ; try { os . write ( bytes . getBytes () ) ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " log :: couldnt write data " ) ; } } public static void endLog () { try { os . close () ; con . close () ; } catch ( IOException e ) { // ErrorHandling ErrorMessage . showError ( " log :: couldnt close connection " ) ; } os = null ; con = null ; } } Listing A.18: Menu.java package util . ui ; import java . util . Vector ; import javax . microedition . lcdui . Font ; import javax . microedition . lcdui . Graphics ; public class Menu implements Renderable { private Font font = Font . getFont ( Font . FACE_MONOSPACE , Font . STYL E_BOLD , Font . SIZE_SMALL ) ; private Vector buttons = null ; private String text = null ; private int width = 0; private int height = 0; private int selectedIndex = 0; public Menu ( String text , int width , int height ) { this . text = text ; this . width = width ; this . height = height ; this . buttons = new Vector () ; } public void paint ( Graphics g ) { g . setColor ( Colors . grey ) ; 74 ANHANG A. SOURCECODE g . fillRect (0 , 0 , this . width , this . height ) ; g . setColor ( Colors . white ) ; g . setFont ( this . font ) ; g . drawString ( this . text , 5 , 5 , Graphics . LEFT | Graphics . TOP ) ; for ( int i = 0; i < this . buttons . size () ; i ++) { (( Button ) this . buttons . elementAt ( i ) ) . paint ( g ) ; } } public void addButton ( Button b ) { this . buttons . addElement ( b ) ; r e c a l c u l a t e D i m e n s i o n s () ; } public void addButton ( String text , double value ) { Button b = new Button ( text , value ) ; addButton ( b ) ; } public void selectUp () { this . selectedIndex - -; if ( this . selectedIndex < 0) { this . selectedIndex = this . buttons . size () - 1; } select () ; } public void selectDown () { this . selectedIndex ++; if ( this . selectedIndex == this . buttons . size () ) { this . selectedIndex = 0; } select () ; } public double getSelectedValue () { return (( Button ) this . buttons . elementAt ( this . selectedIndex ) ) . getValue () ; } private void select () { unselectAll () ; (( Button ) this . buttons . elementAt ( this . selectedIndex ) ) . select () ; } private void unselectAll () { for ( int i = 0; i < this . buttons . size () ; i ++) { (( Button ) this . buttons . elementAt ( i ) ) . unselect () ; } } private void r e c a l c u l a t e D i m e n s i o n s () { int buttonSize = ( this . height - 40) / this . buttons . size () ; for ( int i = 0; i < this . buttons . size () ; i ++) { (( Button ) this . buttons . elementAt ( i ) ) . setDimension (5 , 30 + ( i * buttonSize ) , this . width - 10 , buttonSize - 4) ; } select () ; } } Listing A.19: Observable.java package util . observer ; import java . util . Enumeration ; import java . util . Vector ; public class Observable { private boolean hasChanged = false ; private Vector observers = null ; public Observable () { this . observers = new Vector () ; } public synchronized void addObserver ( Observer o ) { if ( o == null ) { throw new N u l l P o i n t e r E x c e p t i o n () ; } if (! this . observers . contains ( o ) ) { this . observers . addElement ( o ) ; } 75 ANHANG A. SOURCECODE } public synchronized void deleteObserver ( Observer o ) { this . observers . removeElement ( o ) ; } public void notifyObservers () { notifyObservers ( null ) ; } public void notifyObservers ( Object arg ) { Enumeration enumObservers ; synchronized ( this ) { if (! this . hasChanged ) { return ; } enumObservers = this . observers . elements () ; clearChanged () ; } while ( enumObservers . hasMoreElements () ) { (( Observer ) enumObservers . nextElement () ) . notify ( this , arg ) ; } } public synchronized void deleteObservers () { this . observers . r e m o v e A l l E l e m e n t s () ; } protected synchronized void setChanged () { this . hasChanged = true ; } protected synchronized void clearChanged () { this . hasChanged = false ; } public synchronized boolean hasChanged () { return this . hasChanged ; } public synchronized int nrOfObservers () { return this . observers . size () ; } } Listing A.20: Observer.java package util . observer ; public interface Observer { void notify ( Observable o , Object arg ) ; } Listing A.21: Point.java package util . game ; public class Point { public double x = 0.0; public double y = 0.0; public Point () { } public Point ( double x , double y ) { this . x = x ; this . y = y ; } } Listing A.22: PotBangingController.java package potbanging ; import gps . GPS ; import gps . GPSRecord ; 76 ANHANG A. SOURCECODE import javax . microedition . lcdui . Canvas ; import javax . microedition . lcdui . Font ; import javax . microedition . lcdui . Graphics ; import log . Logger ; import util . game . Controller ; import util . ui . Colors ; import util . ui . Menu ; public class P o t B a n g i n g C o n t r o l l e r extends Controller { private static final int INIT_TIME = 6000; private Font bigFont = Font . getFont ( Font . FACE_MONOSPACE , Font . S TYLE_BOLD , Font . SIZE_LARGE ) ; private boolean isInitialized = false ; private boolean showMenu = true ; private boolean hasEnded = false ; private Menu potMenu = null ; private long hours = 0; private long minutes = 0; private long seconds = 0; private long startTime = 0; private long endTime = 0; private long currentInitTime = 0; private long endInitTime = 0; private double [] targetSectors = new double [10]; private double targetDistance = 0.0; private double currentDistance = 0.0; private String currentText = " Test " ; private int currentIndex = 0; private GPSRecord targetPoint = null ; private GPSRecord startPoint = null ; public P o t B a n g i n g C o n t r o l l e r ( int width , int height , GPS gps ) { super ( width , height , gps ) ; this . potMenu = new Menu ( " Select Target Distance : " , width , height ) ; this . potMenu . addButton ( " 10 Meter " , 10.0) ; this . potMenu . addButton ( " 30 Meter " , 30.0) ; this . potMenu . addButton ( " 50 Meter " , 50.0) ; this . potMenu . addButton ( " 80 Meter " , 80.0) ; this . potMenu . addButton ( " 100 Meter " , 100.0) ; Logger . log ( " * menu * " + " start " , true ) ; } public void update () { if (! this . hasEnded ) { if (! this . showMenu ) { if ( this . isInitialized ) { GPSRecord temp = this . gps . getRecord () ; if ( temp != null ) { this . gpsRecord = temp ; Logger . log ( " * gps * " + this . gpsRecord . getRawString () , true ) ; this . currentDistance = this . targetPoint . getDistanceTo ( this . gpsRecord ) ; if ( this . currentDistance <= this . targetSectors [8]) { if ( this . currentDistance <= this . targetSectors [7]) { if ( this . currentDistance <= this . targetSectors [6]) { if ( this . currentDistance <= this . targetSectors [5]) { if ( this . currentDistance <= this . targetSectors [4]) { if ( this . currentDistance <= this . targetSectors [3]) { if ( this . currentDistance <= this . targetSectors [2]) { if ( this . currentDistance <= this . targetSectors [1]) { if ( this . currentDistance <= this . targetSectors [0]) { this . currentText = " Heiss " ; this . currentIndex = 0; 77 ANHANG A. SOURCECODE this . endTime = System . c u r r e n t T i m e M i l l i s () ; long time = this . endTime - this . startTime ; this . hours = time / 3600000; this . minutes = ( time - this . hours * 3600000) / 60000; this . seconds = ( time - ( this . hours * 3600000 + this . minutes * 60000) ) / 1000; this . hasEnded = true ; } else { this . currentText = " Sehr Warm " ; this . currentIndex = 1; } } else { this . currentText = " Noch Wärmer " ; this . currentIndex = 2; } } else { this . currentText = " Wärmer " ; this . currentIndex = 3; } } else { this . currentText = " Warm " ; this . currentIndex = 4; } } else { this . currentText = " Kalt " ; this . currentIndex = 5; } } else { this . currentText = " Kälter " ; this . currentIndex = 6; } } else { this . currentText = " Noch Kälter " ; this . currentIndex = 7; } } else { this . currentText = " Sehr Kalt " ; this . currentIndex = 8; } } else { this . currentText = " Eiskalt " ; this . currentIndex = 9; } Logger . log ( " * distance * " + this . currentText , true ) ; if ( this . hasEnded ) { long time = this . endTime - this . startTime ; Logger . log ( " * end * " + time , true ) ; } } } else { this . currentInitTime = System . c u r r e n t T i m e M i l l i s () ; if ( this . currentInitTime >= this . endInitTime ) { this . startTime = this . currentInitTime ; this . startPoint = this . gpsRecord ; this . targetPoint = this . startPoint . getRandomPoint ( this . targetDistance ) ; this . isInitialized = true ; Logger . log ( " * initialize * " + " end " , true ) ; Logger . log ( " * target * " + " start " , true ) ; Logger . log ( " * target * " + this . targetPoint . toString () , true ) ; Logger . log ( " * target * " + " end " , true ) ; } else { GPSRecord temp = this . gps . getRecord () ; if ( temp != null ) { this . gpsRecord = temp ; Logger . log ( " * gps * " + this . gpsRecord . getRawString () , true ) ; } } } } } } public void paint ( Graphics g ) { if ( this . showMenu ) { this . potMenu . paint ( g ) ; } else { if ( this . isInitialized ) { g . setColor ( Colors . potbanging [ this . currentIndex ]) ; g . fillRect (0 , 0 , this . width , this . height ) ; if ( this . isDebugMode ) { g . setColor ( Colors . white ) ; g . setFont ( this . font ) ; g . drawString ( " tp : " + this . targetPoint . toString () , 2 , 78 79 ANHANG A. SOURCECODE 2 + (0 * this . font . getHeight () ) , Graphics . LEFT | Graphics . TOP ) ; g . drawString ( " sp : " + this . startPoint . toString () , 2 , 2 + (1 * this . font . getHeight () ) , Graphics . LEFT | Graphics . TOP ) ; g . drawString ( " - - - - - - - - -" , 2 , 2 + (2 * this . font . getHeight () ) , Graphics . LEFT | Graphics . TOP ) ; g . drawString ( " cp : " + this . gpsRecord . toString () , 2 , 2 + (3 * this . font . getHeight () ) , Graphics . LEFT | Graphics . TOP ) ; g . drawString ( " cd : " + this . currentDistance + " Meter " , 2 , 2 + (4 * this . font . getHeight () ) , Graphics . LEFT | Graphics . TOP ) ; } g . setColor ( Colors . black ) ; g . setFont ( this . bigFont ) ; g . drawString ( this . currentText , ( this . width / 2) , ( this . height / 2) , Graphics . BASELINE | Graphics . HCENTER ) ; if ( this . hasEnded ) { g . setColor ( Colors . white ) ; g . setFont ( this . font ) ; g . drawString ( " gratulations " , ( this . width / 2) , ( this . height / 2) + ( this . bigFont . getHeight () + 1 * this . font . getHeight () + 5) , Graphics . BASELINE | Graphics . HCENTER ) ; g . drawString ( " you solved the game " , ( this . width / 2) , ( this . height / 2) + ( this . bigFont . getHeight () + 2 * this . font . getHeight () + 5) , Graphics . BASELINE | Graphics . HCENTER ) ; g . drawString ( this . hours + " : " + this . minutes + " : " + this . seconds , ( this . width / 2) , ( this . height / 2) + ( this . bigFont . getHeight () + 3 * this . font . getHeight () + 5) , Graphics . BASELINE | Graphics . HCENTER ) ; } } else { g . setColor ( Colors . grey ) ; g . fillRect (0 , 0 , this . width , this . height ) ; g . setFont ( this . font ) ; g . setColor ( Colors . white ) ; g . drawString ( " stand still for a moment " , this . width / 2 , this . height / 2 - ( this . font . getHeight () + 2) , Graphics . HCENTER | Graphics . BASELINE ) ; g . drawString ( " the game is initializing " , this . width / 2 , this . height / 2 + ( this . font . getHeight () + 2) , Graphics . HCENTER | Graphics . BASELINE ) ; } } } private void setTargetSectors () { this . targetSectors [4] = this . targetDistance ; this . targetSectors [3] = this . targetSectors [4] this . targetSectors [2] = this . targetSectors [3] this . targetSectors [1] = this . targetSectors [2] this . targetSectors [0] = this . targetSectors [1] this . targetSectors [5] = this . targetSectors [4] this . targetSectors [6] = this . targetSectors [4] this . targetSectors [7] = this . targetSectors [4] this . targetSectors [8] = this . targetSectors [4] this . targetSectors [9] = this . targetSectors [4] } public void keyPressed ( int keyCode , Canvas c ) { switch ( keyCode ) { case Canvas . KEY_POUND : this . isDebugMode = ! this . isDebugMode ; break ; default : switch ( c . getGameAction ( keyCode ) ) { case Canvas . UP : if ( this . showMenu ) { this . potMenu . selectUp () ; } break ; case Canvas . DOWN : if ( this . showMenu ) { this . potMenu . selectDown () ; } break ; case Canvas . LEFT : if ( this . showMenu ) { this . potMenu . selectUp () ; } break ; case Canvas . RIGHT : if ( this . showMenu ) { this . potMenu . selectDown () ; } break ; / / / / + + + + + 2; 2; 2; 2; this . targetSectors [0]; this . targetSectors [1]; this . targetSectors [2]; this . targetSectors [3]; this . targetSectors [4]; ANHANG A. SOURCECODE case Canvas . FIRE : if ( this . showMenu ) { this . targetDistance = this . potMenu . getSelectedValue () ; setTargetSectors () ; this . endInitTime = System . c u r r e n t T i m e M i l l i s () ; this . endInitTime += INIT_TIME ; Logger . log ( " * menu * " + " end " , true ) ; Logger . log ( " * initialize * " + " start " , true ) ; this . showMenu = false ; } else { if ( this . isDebugMode ) { this . gpsRecord = this . targetPoint ; this . currentDistance = this . targetPoint . getDistanceTo ( this . gpsRecord ) ; this . currentText = " Heiss " ; this . currentIndex = 0; this . endTime = System . c u r r e n t T i m e M i l l i s () ; long time = this . endTime - this . startTime ; this . hours = time / 3600000; this . minutes = ( time - this . hours * 3600000) / 60000; this . seconds = ( time - ( this . hours * 3600000 + this . minutes * 60000) ) / 1000; this . hasEnded = true ; } } break ; } break ; } } public void keyReleased ( int keyCode , Canvas c ) { switch ( keyCode ) { default : break ; } } public void keyRepeated ( int keyCode , Canvas c ) { switch ( keyCode ) { case Canvas . KEY_STAR : this . isDebugMode = ! this . isDebugMode ; break ; default : break ; } } public void pointerPressed ( int x , int y ) { } public void pointerReleased ( int x , int y ) { } public void pointerDragged ( int x , int y ) { } } Listing A.23: Renderable.java package util . ui ; import javax . microedition . lcdui . Graphics ; public interface Renderable { public void paint ( Graphics g ) ; } 80 Anhang B Inhalt der CD-ROM File System: Joliet Mode: Single-Session B.1 Diplomarbeit Pfad: / DaBa.dvi . . . . . . . . DaBa.pdf . . . . . . . DaBa.ps . . . . . . . . B.2 Diplomarbeit (DVI-File) Diplomarbeit (PDF-File) Diplomarbeit (PostScript-File) LaTeX-Dateien Pfad: / DaBa.tex . . . . . . . . Hauptdokument kurzfassung.tex . . . . . Kurzfassung abstract.tex . . . . . . . Abstract einleitung.tex . . . . . . Kapitel 1 theoretischegrundlagen.tex Kapitel 2 prototypen.tex . . . . . Kapitel 3 schlusswort.tex . . . . . Kapitel 4 anhang code.tex . . . . Anhang A (Sourcecode) anhang latex.tex . . . . Anhang B (Inhalt CD-ROM) messbox.tex . . . . . . . Messbox zur Druckkontrolle literatur.bib . . . . . . . Literatur-Datenbank (BibTeX-File) 81 ANHANG B. INHALT DER CD-ROM B.3 Style-Dateien Pfad: / hagenberg.sty . . . . . . B.4 Style-File für Diplomarbeiten Dokumentation Pfad: /literatur/ example.pdf . . . . . . . B.5 Beispiel Sonstiges Pfad: / code/ . . . . . . . . . . images/ . . . . . . . . . Sourcecode Bilder und Graphiken 82 Literaturverzeichnis [1] Benford, S., R. Anastasi, M. Flintham, A. Drozd, A. Crabtree, C. Greenhalgh, N. Tandavanitj, M. Adams und J. Row-Farr: Coping with uncertainty in a location-based game. IEEE Pervasive Computing, 2:34–41, 2003. [2] Corvallis, M.: Introduction to the Global Positioning System for GIS and TRAVERSE . URL, http://www.cmtinc.com/gpsbook/index.htm, Juni 1996. Kopie auf CD-ROM (cmtic.pdf). [3] Heeskens, H. und H. Trautmann: GPS und seine Anwendungen. URL, http://www.rz.rwth-aachen.de/mata/downloads/seminar dv/ 2003 04/GPS.pdf, November 2003. Kopie auf CD-ROM (GPS.pdf). [4] Peloschek, R.: Location-Aware Games. URL, http://stud4.tuwien.ac. at/˜e0125012/download/Location-AwareGames-DieWeltalsSpielbrett.pdf, Juni 2006. Kopie auf CD-ROM (Location-AwareGames.pdf). [5] Roth, J.: Mobile Computing. dpunkt.verlag, Heidelberg, 2002. [6] Rothacher, M. und B. Zebhauser: Einführung in GPS . URL, http://tau.fesg.tu-muenchen.de/˜iapg/web/veroeffentlichung/ Kopie auf CD-ROM schriftenreihe/iapg fesg rpt 08.pdf, Mai 2000. (iapgfesgrpt08.pdf). [7] Schmatz, K.-D.: Java 2 Micro Edition. dpunkt.verlag, Heidelberg, 2004. 83 Messbox zur Druckkontrolle — Druckgröße kontrollieren! — Breite = 100 mm Höhe = 50 mm — Diese Seite nach dem Druck entfernen! — 84