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

Eine komplexe Aufgabe, die die Fähigket Quellcode zu lesen 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 dieser Aufgabe ist es Quellode zu lesen, diesen zu verstehen und durch Erweiterungen an eigene Vorlieben anzupassen. Dazu gibt es keine konkreten Aufgaben, wir bieten euch hier eine Plattform für eigene Experimente.

Installation

Javakurs 2011 - AsciiCraft

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

In diesem Archiv (zu entpacken mittels tar -xf, oder per grafischer Oberfläche (Linksklick auf das Archiv)) findet ihr einen Ordner und drei Skripte. Die Skripte sind zum compilieren, ausführen und löschen des Programms 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 hier abgebildete zu sehen bekommen.

Ist dies nicht der Falls, solltet ihr die Ausgaben kontrollieren und eventuelle Fehler einem Tutor melden. Auch könntet ihr probieren euch die Kommandozeilenoptionen 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 euch mit den Tasten A und D nach links oder rechts bewegt. Da ihr an der Oberfläche den Diamanten nicht finden können werdet, müsst ihr euch mit den folgen Tasten in den Berg eingraben:

  • E .. baut das Feld direkt über dem Spieler ab
  • R .. gräbt das Feld diagonal über dem das ihr anschaut an.
  • F .. gräbt in die Richtung in die ihr gerade schaut.
  • C .. Analog zu R, legt das Feld diagonal 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 wieder nach oben gelangen könnt, kann die Spielfigur mit der Leertaste ( ) springen, aber nur wenn dafür genug Platz frei ist.

Die Pfeiltasten und Esc sorgen dafür, dass das Programm sofort beendet 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 sinnvoll zu bearbeiten, ist es am besten, wenn ihr diesen Text parallel zum Code offen lasst, damit ihr schnell zwischen der Beschreibung und dem Code hin und her schalten könnt.

Dieser Code wurde so designed, dass er möglichst viele Techniken einsetzt die ihr schon kennen solltet. Er ist außerdem dokumentiert und zu kommentiert. Ihr findet hier nicht nur die Vererbeung wieder, sondern auch die Kapselung, Schleifen, Arrays, Ausgabe, Variablen und alles andere was ihr wissen müsst.

Dir Grundidee des Codes ist die Wiederverwendbarkeit und Austauschbarkeit bereits bestehender Klassen. So lassen 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 Sammlung 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 sie in einem Unterordner der Form paket/unterpaket ablegt. Außerdem muss jede Klasse am Anfang ihrer .java Datei folgenden Eintrag haben: package paket.unterpaket;. Dieser sorgt dafür, 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

Invisible, Player, Diamond, Coal, Iron und Dirt Tiles

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 hat zur Folge, 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, dass zu einem späteren Zeitpunkt jedes Tile einen neue Funktionalität hat, z.B. dass beim Abbauen des Tiles eine bestimmte Methode ausgeführt wird. 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. Das umfasst nicht nur die Größe des Spielfeldes und die Spielfelder selbst, sondern auch die Spielfigur. Die Spielfigur ist tatsächlich auch nur ein klügeres Tile! Sie kann also genau wie anderer Tiles auch angezeigt werden, siehe toString(), aber zusätzlich kann sie sich auch mit der Methode simulate() den aktuellen Gegebenheiten anpassen. So wird gegenwärtig diese Methode dazu genutzt die Spielfigur auf den Boden fallen zu lassen, sobald sie auf einem 'AirTile-Feld steht. Damit das Programm diese Entscheidung treffen kann, muss es wissen auf welcher Welt die Figur sich befindet, deswegen wird der simulate-Methode die Welt als Parameter übergeben.

Dies Welt ist des Weiteren auch für das Anzeigen und Erstellen der neuen Spielwelt zuständig. Das Anzeigen funktoniert, 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 umgewandelt, indem es zu einem anderen String addiert wird). Das passiert jedoch nur, falls das Tile überhaupt sichtbar (.isVisible() liefert true) ist. Ist das 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 basiert stark auf kontrolliertem Zufall: So wird in Zeile 50 zunächst das gesamte Spielfeld auf Luft gesetzt. Das wird erreicht, indem an jedem Index des Feldes ein neues Element vom Typ AirTile erstellt wird. Ein Teil dieser Luft-Teile wird dann 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 diese erneut 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.

Der Spaß des Spiels liegt nun u.A. drin, die Welt stückweise zu erforschen. Daher müssen noch alle Tiles auf unsichtbar gesetzt werden, die nicht direkt an der Oberfläche liegen. Das passiert, indem 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.

  • Wichtig:* Diese Annahme gilt nur, wenn wir keine Überhänge in der Oberwelt zulassen.

Nachdem alle anderne Felder definiert sind, wird der Diamant plaziert. Dies passiert auch wieder 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, wird es bei der nächsten Spalte weiter probiert. Vorstellen kann man sich dies wie bei dem alten VierGewinnt-Spiel: Die Figur wird in einer Spalte herein geworfen und getestet, ob sie gleich wieder oben heraus fällt. Ist dies der Fall, wird sie einfach bei der nächsten Spalte eingeworfen und das ganze nochmal probiert.

Interaktion

Nachdem nun die Welt erstellt wurde und wir 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, dort wird genauer beschrieben wie das funkioniert. Nachdem die Bibliothek JLine initialisiert wurde, werden die Variablen des Spielers/der Spielerin eingelesen (über die übergebenen Kommandozeilenparameter) und das Spielfeld erstellt. Darauf folgend wird die Hauptschleife, also die, die das gesamte Programm kontrolliert, gestartet. Die Idee dahinter ist, dass nach dem die Methode runMainLoop(..)' zurück gekommen ist, das Programm beendet werden soll. Deshalb 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 sollen wieviele Tile von den unterschiedlichen Typen bereits abgetragen worden sind. Nach der Initialisierung dieser Variablen wird die Endlosschleife gestartet, die erst dann beendet wird, wenn das Spiel beendet werden soll (also, entweder wenn der Diamant gefunden wurde, oder sobald eine der Pfeiltasten oder ESC gedrückt wurde.)

Jetzt wird zu allererst die Welt aktualisiert, indem w.simulate() aufgerufen wird. Diese Methode aktualisiert zur Zeit lediglich die Spielfigur, könnte aber theoretisch auch für allgemeinere Situationen benutzt werden. Nachdem die Spielwelt aktualisiert wurde, ist es an der Zeit sie anzuzeigen. Nach dem Anzeigen wird ausgewertet, ob der Benutzer einer der Tasten gedrückt hat. Ist dies der Fall, wird festgestellt, ob eines der Tile abgebaut 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 dieses Tile in der runMainLoop() ausgewertet und ggf. die Schleife beendet, falls ein DiamondTile gefunden wurde. Sonst werden einfach die Zähler der passenden Typen hochgesetzt. Nun beginnt die Schleife wieder von vorne, indem sie zuerst die Welt aktualisiert, sie dann anzeigt und letztlich erneut die Eingaben des Benutzers auswertet.

Wird die Schleife verlassen, folgt die Anzeige des GameOver-Bildschirms. Dort werden die statistischen Variablen als Highscore ausgegen.

Ende

Javakurs 2011 - AsciiCraft GameOver

Damit sind wir auch schon am Ende des kurzen Überblicks über das Spiel angelangt. Jetzt bleibt euch nur zu tun, das eben 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 der Figur Schaden zufügt? Auch könntet ihr einfach von vorne Anfangen und ein ganz eigenes Spiel schreiben.