Donnerstag, 6. Juni 2013

Wherigo ohne Zonen

Voraussetzung: "Der Einstieg in lua“
Im heutigen Tutorial geht es darum einen Wherigo ohne Zonen zu programmieren. Dies kann hilfreich sein, wenn die benötigte WIG-Lösung viele Zonen bräuchte bzw. die Zonen nicht sichtbar sein sollen.
Die kann bei einem schachbrettartigen Spielfeld sinnvoll sein, z.B ein Memory-Spiel oder dem beim Projekt-Eck verfügbaren Minesweeper-Spiel. Ansonsten hätte wir selbst bei einem 4x4 Memory 16 aktive Felder. Mit dem Oregon undenkbar.

Ermittlung der aktuellen "Zone"


Abbildung 1: Bei der Ermittlung wo der Spieler gerade steht wird über alle Felder iteriert
Das Zauberwort heißt hier (mal wieder) Player.ObjectLocation. Damit kann festgestellt werden, wo sich der Spieler gerade befindet. Danach wird "lediglich" über alle vorher definierten Felder (z.B das Feld A2 auf unserem Schachbrett, Definition folgt weiter unten im Tutorial) iteriert und geprüft, ob der Abstand zum Feld einen definierten Mindestabstand distance entspricht. Dieses Attribut haben wir global festgelegt, z.B distance = 5 (Meter).

Abbildung 2: Die Klasse Field enthält alle benötigten Werte: Name, Index und vorallem die Location
Ein Feld wird dabei durch die Klasse Field definiert. Sie enthält neben Namen und Index(sinnvoll für Verwendung von Arrays) die Eigenschaft Location, die aus der GPS-Koordinate des Mittelpunktes besteht. Freunde des OnProximity Event (siehe Zone als Kreis) kennen diese Logik bereits. Der Radius der virtuellen Zone wird durch die obige Variable distance definiert.
Sobald das erste Feld diese Bedingung erfüllt (distance sollte passend zum Abstand zwischen den Feldern so gewählt sein, dass immer nur 1 Feld nahe genug ist), speichern wir es als activeField und führen die Aktionen aus, die wir normalerweise im onEnter oder onProximity definieren. Im Listing wird dazu die Methode handleField aufgerufen, die dann die gewünschten Anweisungen enthält.
Außerdem wird die Variable foundField auf true gesetzt und der Schleifendurchlauf mit break abgebrochen, denn schließlich haben wir unser aktives Feld ja gefunden. Wenn alle Felder durchlaufen sind, aber kein Feld gefunden wurde, so befindet sich der Spieler außerhalb des Spielfeldes (zur Sicherheit nochmals mit Spielfeld:Contains(Player) abgsichert, könnte auch weggelassen werden) so wird das activeField auf fieldOutside(definiert als Feld mit Namen "outside" und Index -1) gesetzt und eine MessageBox ausgegeben, dass man sich doch gefälligst wieder zurück zum Spielfeld begeben sollte. Diesen Teil habe ich in einer Methode out ausgelagert.
Spielfeld habe ich als wirkliche Zone definitert, damit der Spieler sich über den gewohnten Navigationpfeil leiten lassen kann um das Spiel zu beginnen / wieder aufzunehmen. Ganz ohne Zonen geht es also nicht, wenn man die Usabiltiy nicht außer Acht lassen möchte.

Nebenläufige Programmierung und ihre Tücken

Was aber sollen die Vergleich von activeField in Zeile 47 und 54?
Um das zu verstehen muss man wissen, dass die Methode calcActiveField regelmäßig angestoßen werden muss, da sich der Spieler dreisterweise bewegen möchte. Ein Timer mit Interval 1-5 Sekunden (je größer die Felder desto seltener, nicht zu oft, damit das Oregon mitkommt) bietet sich hier an.
Wenn wir die obigen Vergleiche nicht einbauen, wird die handleField Routine alle 5 Sekunden ausgeführt, was bei nem stattfindenen Dialog sehr unangenehm ist und dazu führt, dass der Spieler die gestellte Aufgabe womöglich nie durchführen kann. Also lieber handleField nur einmal ausführen und dann erst wieder wenn der Spieler das Feld wechselt. Das gleiche gilt für den Fall, dass er sich außerhalb des Spielfeldes verlaufen hat. Auch da reicht es wenn die MessageBox nur einmal angezeigt wird.
Bei sehr großer Anzahl von Felder kann es zu der Situation kommen, dass der aktuelle Schleifendurchlauf noch läuft, während der Timer schon wieder das Signal gibt den nächsten Durchlauf zu starten. Um Fehler oder Wechselwirkungen bei dieser Art der parallelen Abläufe zu verhindern bildet die boolean Variable runningActiveFieldCalculation, als Mutex-Variable eingesetzt, einen Schutz gegen gleichzeitiges Ausführen mehrerer Schleifendurchläufe. Vereinfacht gesagt: So lange ein Iterier-über-alle-Felder-Durchlauf läuft besitzt die Variable den Wert true und weist damit weitere Versuche ab. (! Ich rede bewußt von Abweisen, nicht von Blockieren). Erst wenn der kritische Programmteil abgarbeitet ist wird er mit runningActiveFieldCalculation = false wieder freigegeben.

Wann endet das Spiel?

Die ersten drei Zeilen steuern die Beendigung des Spieles. So soll die Methode endGame() aufgerufen werden, wenn das Spiel als beendet markiert wurde, die notwendige Benachrichtigung an der Spieler ("Gewonnen, gehe zum Final bei N 50° ....") noch nicht vollzogen ist. Die beiden boolean Variablen werden irgendwo in handleField gesetzt, z.B wenn alle Memory-Paare aufgedeckt sind. Zeile 32-35 sind aber nicht zwingend notwendig für die Implementierung des Wherigos ohne Zonen.
Am Ende des Listing beginnt die Implementierung von handleField. Besonders bei Schachbrett-Spielfeldern ist es für den Spieler sehr hilfreich gesagt zu bekommen, wo er sich befindet.
Bei der weiteren Abarbeitung des Feld bitte nicht vergessen ob die Siegbedingung (s.o.) erfüllt ist und das Spiel beendet werden kann.

Spielfeld definieren

Ich hoffe ihr habt noch ein wenig Saft im Aufmerksamkeitstank, denn jetzt wird es richtig knifflig.
Was haben wir?
Wir haben zwei Koordinaten für die Eckpunkte unseres Spielfeldes (ObenLinks und UntenRechts)
Was wollen wir berechnen?
Ein schachbrettartiges Spielfeld mit rows Zeilen und columns Spalten, z.B 3 x 5 Felder. Siehe Abb. 5 als Beispiel
Wie machen wir das
Vereinfacht gesagt ermitteln wir Breite und Höhe des Spielfeldes, teilen diese durch die Anzal der Spalten bzw. Zeilen und iterieren über alle Felder (bei 3 x 5 => 15 Felder) und setzen den Mittelpunkt des Feldes. Zum Schluss setzen wir noch die Ecken ObenLinks, ObenRechts, UntenLinks und UntenRechts als Points der wirklichen Zone Spielfeld und schalten es aktiv und sichtbar.

Abbildung 3: Ausgehend von den Ecken ObenLinks sowie UntenRechts wird das Spielfeld ermittelt

Nun im Detail von oben nach unten, beginnend mit den Zeilen 452 und 453. Dort werden die beiden gegenüberliegenden Eckpunkte unserer Rechteckes definiert. Mit Wherigo.VectorToPoint ermitteln wird den Abstand d sowie den Winkelb zwischen den beiden Punkten. Dies ist ähnlich dem Peilen von einer Koordianten zur anderen mittels Entfernung und Winkel. Ein Winkel b von 135° bedeutet, dass man von firstLoc schräg nach rechts unten gehen muss um zu lastLoc zu gelangen.
Die Variable dist hält den Abstand in Meter fest. Zeile 456 ermittelt den Anteil der Distanz in X-Richtung (also auf der genordeten Karte nach rechts). Wie wir an math.sqrt erkennen, kommt hier der Pythagoras-Klassiker a² + b² = c² zum Einsatz. Wer die Berechnung von distX verstanden hat, wird auch mit distY sowie degX und degY zurechtkommen. Die Modulo-Rechnung mit 360 soll verhindern, dass unser Peilwinkel größer als 360° wird.
Wer es genau wissen möchte, dem kann folgende Debug Zeile helfen:
print("b " .. b .. " dist " .. dist .. " distX ".. distX .. " distY " .. distY .." degX " .. degX .. " degY " .. degY)
In der Kurzversion wird zwischen 456 und 459 die Höhe und Breite sowie der Peilwinkel ermittelt um ein Rechteck aufzuziehen, dessen Verhältnis Höhe / Breite dem Verhältnis Zeilen / Spalten entspricht. Abb. 4 und 5 zeigen zwei Beispiele wie das resultierende Spielfeld aussehen kann. ObenLinks und UntenRechts sind auf beiden Abbildungen gleich, nämlich N 50.3691166666667 E 7.6164 bzw. N 50.3680333333333 E 7.61715.

Abbildung 4: Das Spielfeld mit 5 Zeilen und 3 Spalten ist höher als breit ...

Abbildung 5: während 3 Zeilen und 5 Spalten eher breit als hoch ist

Danach wird die Ecke ObenLinks aka firstLoc als Start der Zeile gesetzt. Dies entspricht in unserem Schachbrett A1. (Bitte entschuldigt den Unterschied zum realen Schachbrett, ich habe gerade erst gesehen, dass dort Zeile 1 die unterste Zeile ist. Bei mir ist es die oberste).
Ist der Rest der Division durch die Anzahl der Spalten 1 (tritt in unserem 3 x 5 Beispiel Abb. 5 bei 1, 6 und 11 auf) so wandert unsere Position nach unten, da wir eine neue Zeile starten. "Nach unten" ist aber nicht genau senkrecht (das wären 180°), sondern abhängig vom Peilwinkel den das Rechteck haben soll (im Beispiel SüdSüdWest bzw. 191,43°). Zur Erinnerung Wherigo.TranslatePoint berechnet den Punkt der von firstLoc aus distY Meter in Richtung degY geht. Vektorrechnung für Anfänger.
math.floor macht übrigens nichts anderes als das Ergenis des Bruches abzurunden, damit 0, 1 bzw. 2 herauskommt. (In der ersten Zeile müssen wir noch nicht nach unten verschieben)

Wenn wir nicht gerade das erste Element einer Zeile antreffen, so müssen wir vom Zeilenanfang nach rechts gehen. Genauer genommen in Richtung degX (OstSüdOst oder 101.43°, merkwürdigerweise genau 90° weniger als degY ). Auch hier gehen wir abhängig von der Spalte in der wir uns befinden 1/4, 2/4, 3/4, 4/4 nach rechts um unsere Position neu zu berechnen.

Zum Abschluss noch etwas entspanntes, um das Tutorial sanft ausklingen zu lassen. Um unsere Zone Spielfeld zu erzeugen setzen wir die Ecken ObenLinks, ObenRechts, UntenRechts und UntenLinks als Points, machen sie sichtbar und aktiv.
Die ganz aufmerksamen unter euch werden jetzt zurecht sagen: Moment, das Spielfeld ist kleiner als die Fläche die von den Feldern abgedeckt wird, denn die Eckpunkte von Spielfeld sind die Mittelpunkte der Eckkreise. Dies ist korrekt, aber Spielfeld dient lediglich dazu den Spieler zurück zur Spielfläche zu führen, wenn der diese früher erreicht als vom GPSr angezeigt so ist dies nicht wirklich schlimm, sollte aber bei der Messung der beiden Ecken ObenLinks und UntenRechts beachtet werden.
Vielen Dank an dieser Stelle an bodenseepingu, der mir seinen Quellcode von Geomemory zur Verfügung stellte und so die nötigen Denkanstöße gab ein Feld mit zwei gegenüberliegenden Eckpunkten aufzuziehen.

Keine Kommentare:

Kommentar veröffentlichen