Sonntag, 29. April 2012

Datenstrukturen in lua

Datenstrukturen

Voraussetzung: "Einstieg in Lua“

Abbildung 1: Variablen können lokal oder global definiert werden, es gibt Zahlvariablen, Strings, Booleanwerte und Arrays

Definition von Variablen

Wir beginnen dieses Tutorial mit der Definition von einfachen Variablen. Abb. 1 zeigt die verschiedenen Möglichkeiten Platzhalter zu definieren.
In den ersten vier Zeilen sehen wir global definierte Variablen vom Typ Zahl, Boolean, String und Array. Die ersten drei Typen sind uns von den graphischen Bausteinen her bekannt. Ein Array, oft auch als Tabelle bezeichnet, ist ein sehr flexibler und nützlicher Datentyp. Hier sehen wir ein Beispiel in Form einer einfachen Aufzählung. Der Zugriff auf Arrays erfolgt über einen Index, der die Stelle im Array definiert, indem man hinter den Arraynamen den Index in eckigen Klammern schreibt. An erster Stelle mit Index 1 wurde der String "Michael" gesetzt. Wir können diesen Wert mit names[1] abfragen. ( ! Während in vielen Programmiersprachen mit der 0 zu zählen begonnen wird, startet die Sequence bei lua mit der 1)
Wie man am dritten Wert des Array erkennen kann, können auch andere Platzhalter zur Definition herangezogen werden. names[3] würde folglich "Krolock" liefern.
Entgegen anderen Sprachen muss man bei der Definition weder ein Keywort "var" oder "obj" vor die Variable schreiben, noch den Datentyp explizit dabei schreiben. Dieser ergibt sich aus dem Wert, der ihr zugeordnet wird und kann sogar gewechselt werden. (Auch wenn das im Normalfall nicht empfehlenswert ist).

Lokale Variablen

In der siebten Zeile steht das Wörtchen "local" vor dem Variablen delta. Dies bedeutet, dass sie nur in der Methode nextRound() bekannt ist. Nachdem die Funktion abgearbeitet wurde existieren "delta" und "message" nicht mehr und der von ihnen belegte Speicher wird vom CarbageCollector wieder freigeben. "roundCount" ist aber global definiert und besitzt nicht mehr den Initialwert 0, sondern nunmehr 1.
Wie bereits im lua Einstieg erwähnt werden Strings nicht durch das Pluszeichen, sondern durch zwei Punkte konkateniert.

Ein Array als Tabelle und als Dictionary nutzen

Bisher haben wir das Array lediglich als eine einfache Tabelle genutzt. Das wahre Potentional ist aber viel größer. Wir wollen uns folgende Aufgabenstellung geben:
In einem Wherigo soll der Spiel eine Quizrunde durchlaufen. Dabei gibt es pro Runde 3 Fragen, mit jeweils 3 möglichen Antworten von denen natürlich nur jeweils eine richtig ist.

Abbildung 2: Definition eines Arrays als Tabelle und Dictionary

Abb. 2 zeigt uns eine solche Datenstruktur. In Zeile 13 sehen wir, dass der Index nicht automatisch eine Zahl sein muss, sondern auch ein String sein kann. game["round"] oder auch kurz game.round liefert den Wert 1.
Es geht knifflig weiter mit Zeile 14. Dort definieren wir als Wert für Index 1 ein Subarray, dem wir für den Key game[1].trial den Wert 1 zuordnen. Wir haben somit den ersten Versuch in Runde Eins vorbereitet. Um diesen mit Werten zu füllen wird in den nächsten beiden Zeilen (Zeilenumbruch dient nur der Übersichtlichkeit) der Inhalt von game[1][1] mit der Frage question, den möglichen Antworten answers und der richtigen Antwortnummer correctAnswerNr aufgefüllt.
Zeile 18 und 19 definieren dieses Trippel für die zweite Frage und game[1][3] dementsprechend die dritte Frage der ersten Runde.

Wie man an Zeile 23 erkennen kann, muss man nicht den Umweg über game[2][1], game[2][2], game[2][3] gehen sondern kann die drei Fragentrippel auch als "Einzeiler" definieren. (Damit es lesbar bleibt, hab ich Fragen und Antworten nicht ganz ausformuliert )

Abbildung 3: Hilfsmethoden für den Zugriff

Den Zugriff vereinfachen

Da der Zugriff auf die Datenstruktur nicht mehr der 08/15 Methode myArray[2] entspricht, schreiben wir uns ein paar Helferlein: Wir beginnen mit dem Abruf der aktuellen Frage: Zuerst fragen wir die Rundennummer ab, sobald wir diese haben fragen wir nach der Nummer des aktuellen Versuches. Somit haben wir die beiden Indizes für den Zugriff auf das Fragentrippel, von dem wir die question auswählen und zurückgeben. Der Rückgabewert ist hierbei ein String.
Praktischerweise verwaltet die Datenstruktur selbstständig welche Runde und welcher Versuch gerade akutell ist, sodass wir im späteren Programmablauf nicht mit lokalen Variablen diese Werte merken und aktualisieren müssen.
Die Abfrage der möglichen Antworten funktioniert auf die gleiche Weise. An Zeile 37 sieht man, dass man den Wert nicht erst zwischenspeichern muss, sondern ihn direkt zurückgeben kann.
Noch weniger Code ist bei der Ermittlung der korrekten Antwortnummer verwendet. Man sieht aber auch, dass sich der Code hart an der Grenze der Lesbarkeit befindet bzw. diesen Bereich eigentlich schon verlassen hat.

Abbildung 4: Der Runden- und Versuchszugriff wird durch weitere Getter abstrahiert.
Wir wollen den Vorteil, dass die Datenstruktur ihre "Pointer" Runde und Versuch selbst pflegt, weiter ausbauen, indem wir Getter für den Zugriff auf die aktuelle Round und Trail erstellen.
Während getRoundNr() noch sehr unspektakulär game.round zurückliefert, wird es bei getRoundArray(), getTrialNr() und getTrialArray() schon komplizierter, zumal sich die Getter auch gegenseitig aufrufen.
Den Vorteil dieses Verfahrens sehen wir in Zeile 45. Für das Ermittlung der korrekten Antwort brauchen wir nur getTrialArray() aufzurufen, um das Fragentrippel zu erhalten. Es wäre sogar noch eine weiter Optimierung möglich indem man getAnswers()[getCorrectAnswertNr] verwendet.

Bleibt noch eine Frage zu klären: Wie erreiche ich die nächste Runde, bzw. den nächsten Versuch?
Auch hier soll im laufenden Programmcode nicht auf die Variablen round und trial zugegriffen werden, sondern abstrahiert durch zwei increment (erhöhe) Methoden.
incrementRound() und incrementTrial() zeigen uns wie es geht.

Abbildung 5: Die Bedienung unserer game Datenstruktur ist recht einfach.
Die Logik der Datenstruktur ist erledigt, jetzt wollen wir sie bedienen. Der Dialgog in Abb. 5 zeigt uns wie einfach dies geschehen kann. Dieser Dialog ist natürlich nur zu Testzwecken sinnvoll. Im Spiel wäre ein MultipeChoice Dialog geeigneter.
Während getQuestion() uns die Frage "Wo wurde der erste deutsche Cache gelegt ?" liefert, liefert getAnswers(1) den String "Hamburg" zurück.
getAnswer(2) liefert genauso wie getCorrectAnswer() "Berlin", da getCorrectAnswerNr() als Ergebnis 2 zurück gibt.

Abbildung 6: Vor dem Aufruf des Inputs müssen dessen Fragen und MultipleChoice Möglichkeiten aktualisiert werden
Diese Abfragen sind nützlich um einen Input damit aufzubauen. Beim Start des Spiels (alternativ beim Erreichen der ersten Fragezone) rufen wir updateInput() auf, um Frage und potentielle Antworten im Input MyInput auf erste Runde, erster Versuch zu setzen. Anschließend starten wir den Input. Wie simple updateInput() implementier ist, werden wir weiter unten sehen.

Abbildung 7: Im Dialog MyInput wird die Antwort verglichen und Rounde / Versuch erhöht.
Nachdem der Spieler eine Lösung der MultipleChoice Frage ausgewählt hat, kann die Antwort mit getCorrectAnswer() verglichen werden. Ist die Lösung korrekt, wird die Frage der nächsten Runde vorgelegt, ansonsten die des nächsten Versuchs. Dabei fungieren prepareNextRound() und prepareNextTrial() wieder als Hilfsmethoden um Frage und Antworten anzupassen.

Abbildung 8: Der Dialog wird ausgebaut
Natürlich sollte man noch ein wenig Output in Form von MessageBoxen an den Player geben, die fallen der Übersichtlichkeit halber hier recht kurz aus. Man muss übrigens nicht im If und im Else Zweig eine MessageBox plazieren. Alternativ könnte man in jedem Zweig in der Lua User code Box eine Variable message definieren, die man vor dem Input in einer MessageBox über Lua user expression als Outputwert wieder aufruft.

Abbildung 9: Während objMyInput.Text Zugriff auf die Frage bietet, können die möglichen Antworten mit objMyInput.Choices überschrieben werden
Wie funktionieren prepareNextRound() und prepareNextTrial() eigentlich? Die Frage ist sehr einfach zu beantworten Beim Vorbereiten der nächsten Runde, wird der Rundezähler erhöht. Mit objMyInput.Text wird die Frage und mit objMyInput.Choices die möglichen Antworten im Input überschrieben. Und schon kann die nächsten Frage vorgelegt werden

Persistenz

Ein Problem bleibt aber noch übrig: Wie werden meine Variablen persistiert und überleben ein Abspeichern und Wiederherstellen (Laden) des Cartrigdes. Es werden nämlich nicht automatisch alle Variablen gespeichert. Die von Uriwgo angelegten Variablen werden in einem Feld namens ZVariables gespeichert.

Abbildung 10: Die einfachste Art eigene Variablen zu persistieren ist sie an ZVariables anzuhängen
Variablen, die man im lua Code anlegt werden natürlich nicht automatisch in dieses Feld aufgenommen. Möchte man also die eigenen Objekte sichern -für unser Array game sehr empfehlenswert-, so gibt es verschiedene Möglichkeiten:
  • Man legt eine Variable namens game in der graphischen Oberfläche von Urwigo an. Entweder setzt man den Identifier ebenfalls auf game, oder man spricht sie mit objGame an
  • Man legt eine Variable namens storage in der graphischen Oberfläche von Urwigo an. Im OnSave Block kann man dann storage als Array umdeklarieren und alle eigenen Variablen dort einfügen. ! In OnRestore muss das Array wieder aufgedröselt werden
  • Man fügt beim Anlegen der Variable dieselbige dem Feld ZVariables hinzu (Zeile 2 in Abb. 10). Dies kann in einer Zeile geschehen und man muss sich nicht mehr um die Serialisierung kümmern. Dabei ist zu beachten, dass das Feld ZVariables an der Cartridgevariablen objDatenstrukturen (obj + Cartridgename) hängt.

Keine Kommentare:

Kommentar veröffentlichen