Sitzung: Jeden Freitag in der Vorlesungszeit ab 16 Uhr c. t. im MAR 0.005. In der vorlesungsfreien Zeit unregelmäßig (Jemensch da?). Macht mit!

Javakurs/Übungsaufgaben/AsciiCraft: Unterschied zwischen den Versionen

(draft for asciicraft aufgabe)
(kein Unterschied)

Version vom 9. März 2011, 22:47 Uhr

Eine Komplexe Aufgabe, die Codelesen vertiefen soll, den Kurs zusammenfasst und einen etwas größeren Umfang als andere Aufgaben bietet.


Aufgabe

Im Folgenden wird Schritt für Schritt erklärt werden, wie das Spiel AsciiCraft, eine Anlehnung an Minecraft, geschrieben wurde. Ziel ist es in dieser Aufgabe Quellode zu lesen, diesen zu verstehen und diesen durch Erweiterungen an eigene Vorlieben anzupassen. Dazu wirde es keine Konkreten Aufgaben geben, ziel ist es eher euch einen Plattform für eigene Experimente zu geben.


Installation

Um das Programm zu verstehen, ist es wichtig, dass man eine Vorstellung bekommt, was es eigentlich tun soll. Daher bietet es sich an, dieses auf seinem Rechner/Account zu installieren. Bitte ladet euch dafür das Archiv | auf unserem Server herunter.

In diesem Archiv (zu entpacken mittels tar -xf, oder per grafischer Oberfläche (Linkklick auf das Archiv)) findet ihr einen Ordner und drei Skripte. Die Skripte sind zum compilieren, ausführen und löschen der Kompilate da und sollten selbsterklärend sein. Der Ordner src beinhaltet den Quellcode und wird später für uns sehr interessant werden.

Ihr solltet nun das Projekt kompilieren (./build.sh) und es dann einmal starten (./run.sh, von der Kommandozeile aus).

Nun solltet ihr ein ähnliches Bild wie das folgende sehen: [image].

Ist dies nicht der Falls, solltet ihr die Ausgaben kontrollieren und eventuelle Fehler einem Tutor melden. Auch könntet ihr probieren die Kommandozeilen Optionen euch anzusehen:

./run.sh -h



Ziel des Spieles

Nach dem ihr im letzten Abschnitt erfolgreich das Spiel installiert und kompiliert habt, ist es nun an der Zeit die Aufgabe des Spieles kennen zu lernen: Euer Ziel ist es mit dem kleinen Pfeil (meist in der Mitte der Konsole) den Diamanten aus dem Bergwerk zu fördern.

Dieses Ziel erreicht ihr, in dem ihr mit den Tasten A und D euch nach links oder rechts bewegt. Da ihr an der Oberfläche den Diamanten nicht finden können wird, müsst ihr mit den folgen Tasten euch in den Berg eingraben:

  • E .. baut das Feld direkt über dem Spieler ab
  • R .. gräbt das Feld ein Feld über dem das ihr anschaut an.
  • F .. gräbt in die Richtung in die ihr gerade schaut.
  • C .. Analog zu R legt das Feld eins unter dem Feld das ihr anschaut frei.
  • X .. Gräbt das Feld direkt unter euch ab (Achtung: Mit Vorsicht zu genießen!!!)

Damit ihr auch evtl. wieder nach oben gelangen könnt, kann der Spieler mit der Leertaste ( ) springen, aber nur, wenn dafür genug Platz frei ist.

Die Pfeiltasten und Esc sorgen dafür, dass sofort das Programm beendet wird und der GameOver-Bildschirm angezeigt wird.


Sobald ihr euch einen Weg zum Diamanten gegraben habt, müsst ihr ihn nur noch ausgraben, um zum Ende des Spieles zu kommen.


Konzept des Codes

Um diese Aufgabe erfolgreich und sinnvol zu bearbeiten, ist es am besten, wenn ihr diesen Aufgaben Text parallel zum Code offen lasst, damit ihr schnell zwischen der Beschreibung und dem Code hin und her schalten könnt.

Dieser Code wurde designed, damit er möglichst viele Techniken einsetzt, die ihr schon kennen solltet. Es wurde des Weiteren versucht den Code zu dokumentieren und zu kommentieren. So werdet ihr nicht nur die Vererbeung wiederfinden, sondern auch die Kapselung, Schleifen, Arrays, Ausgabe, Variablen und eigentlich Alles was ihr wissen müsst.

Basisidee ist die der Wiederverwendung. Es wurde versucht alle Klassen die an dem Programm beteiligt sind so zu gestallten, dass sie möglichst leicht auszutauschen sind. So lässt sich ohne großen Aufwand weitere Felder hinzufügen. Doch nun zu den bereits vorhandenen Klassen.


Pakete

Die Klassen die an dem Spiel beteiligt sind, findet ihr unter src/... alle Klassen sind unter dem gleichen Paket gespeichert: javakurs2011.asciicraft. Wobei ein Paket eine Unterteilung von Klassen ist. So ist es möglich, dass es Klassen gibt, die gleich heißen, aber in anderen Paketen definiert wurden.

Eine Klasse markiert man als zu einem Paket gehörend, in dem man es in einen Unterordner der Form paket/unterpaket steckt. Des Weiteren muss jede Klasse am Anfang seiner .java Datei einen Eintrag wie folgt haben: package paket.unterpaket;, welches dafür sorgt, dass die Klasse auch als zu dem Paket gehörig erkannt wird. (bitte beachtet, dass der / in der JavaDatei durch einen Punkt (.) ersetzt wurde.) Die Technik der Pakete wird meist in größeren Produktionen eingesetzt und wird euch in eurem weiteren Studium bestimmt noch häufiger begegegen.


Abstrakte Klasse Tile

Jedes der Felder, die ihr im Spiel seht, ist abgeleitet von der abstrakten Klasse Tile (konkret AirTile.java CoalTile.java DiamondTile.java DirtTile.java IronTile.java, in allen steht bei der Klassendefinition die Zeile extends Tile). Der Sinn dahinter ist, dass ihr in der abstrakten Klasse Methoden angebt, die von allen Unterklassen implementiert werden müssen. Dieser Mechanismus macht es möglich, dass man im Folgenden von der genauen Implementierung der Klasse (bspw. IronTile) abstrahiert und dieses nur noch als Tile auffasst.

Dies wird etwas deutlicher, wenn man sich vor Augen hält, wie das Spielfeld aussieht: In der Datei World.java wird in Zeile 14 ein Feld von mehreren Objekten der Klasse Tile erzeugt. Das bedeutet, dass man in diesem Array alle Unterklassen, also alle Klassen, die von Tile erben, speichern kann, und alle mindestens die Funktionalitäten von Tile anbieten. In unserem Fall ist dies noch sehr wenig, da lediglich eine Methode, die toString-Methode, von Tile als zu implementieren gekennzeichnet wurde. Vorstellbar ist aber, dass in einem späteren Zeitpunkt jedes Tile einen neue Funktionalität haben soll, wie dass beim Abbauen des Tiles eine bestimmte Methode ausgeführt werden soll. Diese Methode könnte zum Beispiel mine() heißen und könnte dafür sorgen, dass das IronTile zwei Versuche braucht um abgebaut zu werden.


Die Welt

Wie bereits im vorherigen Kapitel erwähnt, speichert die Klasse World alles was zur Spielwelt gehört. Dies ist nicht nur die Größe des Spielfeldes, die Spielfelder selbst, sondern auch den Spieler. Der Spieler ansich ist auch nur ein klügeres Tile. So kann er zwar genau wie anderer Tiles auch angezeigt werden, siehe toString(), aber er kann auch in der Methode simulate() sich den aktuellen Gegebenheiten anpassen. So wird gegenwärtig diese Methode dazu genutzt den Spieler auf den Boden fallen zu lassen, sobald er auf einem 'AirTile-Feld steht. Damit das Programm diese Entscheidung treffen kann, muss es wissen auf welcher Welt er sich befindet, also wird der simulate-Methode die Welt als Parameter mit angegeben.

Dies Welt ist des Weiteren auch für das Anzeigen und Erstellen der neuen Spielwelt zuständig. Anzeigen tut sie, indem sie zuerst durch das Feld der Tiles geht und jedes aus dem Array heraus nimmt und dessen toString()-Methode implizit aufruft. (vgl. Zeile 154 in World.java, dort wird nicht .toString() benutzt, sondern implizit das Tile in einen String durch addieren des Tiles zu einem String, aufgerufen). Das ganze passiert jedoch nur, falls das Tile überhaupt sichtbar (.isVisible() liefert true) ist. Ist dies nicht der Fall wird lediglich ein graues Viereck angezeigt. Das Tile ist nur dann nicht sichtbar, wenn es bisher noch nicht entdeckt worden ist und unterhalb der Oberfläche liegt.

Dies führt uns zur Erstellung des Spielfeldes. Das Spielfeld wird in der Zeile 48 der Datei World.java erzeugt. Das Erstellen des Feldes basiert sehr stark auf kontrolliertem Zufall: So wird in Zeile 50 zunächst das gesamte Spielfeld auf Luft gesetzt. Dies wird erreicht, indem an jedem Index des Feldes ein neues Element vom Typ AirTile erstellt wird. Ein Teil dieser Luft-Teile wird im Folgenden durch zufällige DirtTiles ersetzt. Basis dafür ist dass innerhalb der for-Schleife in Zeile 56 an einer zufälligen Höhe nahe der Hälfte des Spielfeldes für jeden Spalteneintrag neu bestimmt wird, ob das Tile nach oben oder unten plaziert werden soll. Nachdem die Höhe des Horizontes erstellt wurde, werden alle folgenden Teile auch als DirtTiles erstellt.

Sobald alle DirtTiles erstellt wurden, werden erneut alle DirtTiles angefasst (Zeile 72) und zufällig durch entweder ein IronTile oder ein CoalTile ersetzt.

Nun sind prinzipiell alle normalen Tiles erstellt und die Welt ansich ist einsatzbereit. Nun der Spaß des Programmes beginnt dann, wenn man die Welt langsam anfängt zu erforschen. Daher müssen noch alle Tiles auf unsichtbar gesetzt werden, die nicht direkt an der Oberfläche liegen. Das passiert, in dem für alle Spalten des Feldes gezählt wird, wieviele nicht-AirTiles bereits gefunden wurden. Ist diese Zahl größer als eins, bedeutet dies, dass wir unterhalb der Oberfläche sind, und somit, zumindest initial alle Tiles unsichtbar sind. Diese Annahme gilt nur, wenn wir keine Überhänge in der Oberwelt zulassen.

Nun wird der Diamant plaziert. Dies passiert auch wieder sehr zufällig, indem einfach eine zufällige Position eines DirtTiles gesucht wird, die nahe am unteren Ende des Spielfeldes liegt.

Nach dem Der Diamant gesetzt wurde, wird versucht, den Spieler leicht mittig auf dem Feld zu plazieren, indem nach einem AirTile in der Mittleren Spalte gesucht wird. Wurde kein Leeres Tile gefunden, so wird es bei der nächsten Spalte weiter probiert. Vorstellen kann man sich dies wie bei dem alten VierGewinnt-Spiel: Der Spieler wird in einer Spalte herein geworfen und geschaut, ob er gleich wieder oben heraus fällt. Ist dies der Fall wird er einfach bei der nächsten Spalte eingeworfen und das ganze nochmal probiert.


Interaktion

Nachdem nun die Welt erstellt wurde, wir nun wissen wie wir sie anzeigen können, ist das Einzige was noch fehlt die Interaktion mit ihr. Zum Anfang des Programmes wird, wie bereits bekannt, die main-Methode ausgeführt (siehe Main.java). Diese sorgt dafür, dass wir auf dem gesamten Terminal schreiben können. Siehe dazu auch die benutzte Bibliothek JLine, da diese genauer beschreibt wie genau es funkioniert. Nachdem die Bibliothek JLine initialisiert wurde, werden die Variablen von Benutzer eingelesen (Mittels der Kommandozeilen Parameter) und das Spielfeld erstellt. Folgend wird die Hauptschleife, also die, die das gesamte Programm kontrolliert gestartet. Die Idee ist, dass nach dem die Methode runMainLoop(..)' zurück gekommen ist, das Programm beendet werden soll. Daher ist der letzte Aufruf der main-Methode ein Reset des Terminals, damit Alles wieder auf seinen initialen Zustand zurück gesetzt wird.

Das eigentlich Spannende passiert in der runMainLoopMethode(..). Hier werden zunächst einige statistische Variablen erzeugt, die speichern wieviele Tile von den unterschiedlichen Typen abgetragen worden sind. Nach der Initialisierung dieser Variablen wird die Schleife gestartet, die erst beendet wird, wenn das Spiel beendet werden soll (also, entweder wenn der Diamant gefunden wurde, oder aber sobald eine der Pfeiltasten oder ESC gedrückt wurde.)

Zu Allererst wird die Welt aktualisiert, indem w.simulate() aufgerufen wird. Diese Methode aktualisiert zur Zeit lediglich den Player, aber könnte Theoretisch auch für allgemeinere Situationen benutzt werden. Nachdem die SpielWelt aktualisiert wurde, ist es an der Zeit die Welt anzuzeigen. Nach dem Anzeigen wird ausgewertet, ob der Benutzer einer der Tasten gedrückt hat. Ist dies der Fall, wird geschaut, ob eines der Tile gemined werden soll. Die Überprüfung passiert in der bereits erwähnten mine()-Methode, die das Tile zurück liefert, welches gerade gemined wurde (dabei kann es sich prinzipiell auch um ein AirTile handeln.) Als nächstes wird diese Tile in der runMainLoop() ausgewertet, und die loop beendet, wenn ein DiamondTile gefunden wurde. Sonst werden einfach die Zähler der passenden Typen hochgesetzt. Nun beginnt die Schleife wieder von vorne, in dem sie zuerst die Welt aktualisiert, dann sie anzeigt und letztlich erneut die Eingaben des Benutzers auswertet.

Nach dem die Schleife verlassen wurde, wird der GameOver-Bildschirm angezeigt, der die statistischen Variablen ausgibt.


Ende

Damit sind wir auch schon am Ende des kurzen Überblicks über das Spiel angelangt. Jetzt bleibt euch nur zu tun, das ebend gelesene noch mal revue passieren zu lassen und evtl. eigenen Erweiterungen zu schreiben (Wie wäre es zum Beispiel mit einem Mehrspielermodus, oder mit Lava, welche dem Spieler Schaden zufügt). Auch könntet ihr einfach von vorne Anfangen und ein ganz eigenes Spiel schreiben.