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

Documentos relacionados