Dein eigenes Stück Land
In diesem Kapitel wirst du erfahren, wie es ist, dein eigenes Stück Land zu besitzen.
Nun ja, nicht ganz – du kannst leider nicht in deinem Fluß baden, und auch nicht auf Bäume klettern. Dafür musst du auch nicht Rasen mähen!
Danke, dass du fragst. Dein Stück Land sieht so aus:
Das werden wir nun erkunden! Dabei setzen wir auf die beste Grafikeinheit, die es gibt: deine Vorstellungskraft!
Nun ja, an Stelle einer mangelhaften Grafik wirst du einen Text lesen wie »Du siehst Bäume und einen Fluß.« Die Landschaft, die du dir vorstellst, wird schöner sein, als alles, was dir ein Computerbildschirm zeigen kann! Und du wirst auf der Karte herumgehen können.
Wir werden hier einfache Zeichenketten verwenden, also etwa "Wiese"
, "Wiese mit Fluß"
, "Wiese mit Hügeln"
, usw. – in komplexeren Programmen könnten dies aber beliebige Objekte sein.
Mmh… wir könnten uns mit Hilfe der Himmelsrichtungen orientieren:
Wir machen es uns etwas einfacher. Wir teilen die Landkarte in etwa in Quadrate auf, und gehen dann von einem Quadrat zum nächsten. Unsere Aufteilung könnte vielleicht so aussehen:
Dann tippen wir vielleicht »S« für Süden, »O« für Osten, und so weiter… und gehen von einem Planquadrat zum nächsten.
So weit scheint die Bedienung klar. Doch wie funktioniert unser Programm? Im Inneren?
Wie immer gibt es verschiedene Möglichkeiten. Sinnvoll könnte sein, eine Art Koordinatensystem zu nutzen:
Überlege, welche der folgenden Möglichkeiten einfache Handhabbarkeit versprechen.
Ein… äh… interessanter Plan! Das würde so halbwegs funktionieren (auch wenn es recht aufwendig wäre und viele Ausgaben sich wiederholen würden) – aber schwer mehr als für einen Weg irgendwohin. Angenommen, du möchtest erlauben, dass Besucher_innen im Kreis gehen – nach wie vielen Kreisen würdest du mit den if
und else
-Blöcken aufhören? Diesen Weg würde ich nicht weiterverfolgen…
Das wäre eine Möglichkeit – so käme jede Ortsbeschreibung nur einmal vor, und das würde deinen Code halbwegs übersichtlich machen. Sinnvoll ist es allerdings oft, Daten (hier also unsere Orte) und Logik (wie komme ich von A nach B) zu trennen. Daher werden wir einen anderen Weg verfolgen.
Das hört sich nach einem geeigneten Weg an! Und genau diese Datenstruktur sehen wir uns gleich etwas genauer an!
Wir werden Logik (»Wie komme ich von A nach B«) und Daten (»Wie sieht die Landkarte an einer bestimmten Stelle aus?«) trennen. Die Bewegung kontrollieren wir mit einer x- und einer y-Koordinate, und für die Karteninformationen nutzen wir eine passende Datenstruktur.
Im Prinzip kennst du sie schon… wir nutzen ein Array!
Wir nutzen die zweite Dimension! Wenn du den Index eines einfachen Arrays als eine Achse im Koordinatensystem auffasst, ist schon die Hälfte geschafft. Wenn du nun unter jedem Index dieses Arrays ein weiteres Array einfügst, kannst du dort jedes Feld unserer Landkarte abspeichern. Wir nutzen also ein Array von Arrays.
Eigentlich nicht – das erste Array enthält einfache String-Arrays. Man spricht auch von mehrdimensionalen Arrays – in unserem Fall: von einem zweidimensionalen Array.
Der Code dazu sieht so aus:
String[][] meinArrayVonStringArrays = new String[3][4];
Das ist – wie beim eindimensionalen Array – die Anzahl der Elemente. Hier gibt es 3 Arrays, die jeweils die Länge 4 haben.
Die Ansprache funktioniert übrigens genau wie beim eindimensionalen Array. Hier sollst du erkennen, welcher Code diese Befüllung unseres Beispielarrays erzeugt. (null
bedeutet hier übrigens, dass noch kein Element in unser Array eingefügt wurde – der Platz für einen String ist allerdings schon da!)
// meinArrayVonStringArrays
[ [null, null, null, null],
[null, null, "Hi", null],
[null, null, null, null] ]
Welche Codezeile erzeugt dieses Array?
Das stimmt leider nicht! Hast du vielleicht nicht bei 0, sondern bei 1 angefangen zu zählen? Die von dir gewählte Zeile würde dieses Array erzeugen:
// meinArrayVonStringArrays
[ [null, null, null, null],
[null, null, null, null],
[null, null, null, "Hi"] ]
Das stimmt leider nicht! Du hast die Richtungen vertauscht: der erste Index (hier: 2
) wählt aus, welche (innere) Liste angesprochen wird, der zweite Index (1
), welches Element der Liste.
Die von dir gewählte Zeile würde dieses Array erzeugen:
// meinArrayVonStringArrays
[ [null, null, null, null],
[null, null, null, null],
[null, "Hi", null, null] ]
Das ist richtig, gut gemacht!
Das stimmt leider nicht! Du hast die Richtungen vertauscht, und bei 1 statt bei 0 angefangen zu zählen.
Der erste Index (hier: 3
) wählt aus, welche (innere) Liste angesprochen wird, der zweite Index (2
), welches Element dieser Liste.
Eine vierte Zeile mit einem Array (Index 3
) gibt es hier nicht – daher würde die von dir gewählte Zeile eine Index out of bounds-Fehlermeldung werfen.
Die erste Zahl wählt also das innere Array aus – in dieser Darstellung ist das die Zeile. Die zweite Zahl wählt das entsprechende Element des inneren Arrays aus.
Wie gewohnt, fangen wir in beiden Fällen wieder bei der 0
mit Zählen an.
Eine gute Frage! Ja, die Kurzschreibweise gibt es!
Am besten starten wir gleich mit unserer Landkarte – dort kommt sie zum Einsatz. Lege einen neuen Workspace in der Online-IDE an, eine neue Datei Landkarte.java, und beginne die Klasse Landkarte
so:
public class Landkarte {
private String[][] karte = { {"Ödland", "Ödland mit Fluß", "Ödland mit Hügeln"},
{"Wiese", "Wiese mit Fluß", "hügelige Wiese"},
{"Büsche", "Büsche mit Fluß", "Büsche auf Hügeln"},
{"Wald", "Wald mit Fluß", "Wald mit Hügeln"}
};
}
Eine kleine Zwischenfrage: Durch welche Zeile lässt sich die Beschreibung »Wald mit Fluß« in der Variable beschreibung
speichern?
Leider daneben – du hast die Richtungen vertauscht! (Und du hast glaube ich wieder bei 1 angefangen zu zählen…)
Leider daneben – du hast die Richtungen vertauscht!
Gut gemacht! Ich glaube du verstehst, wie zweidimensionale Arrays funktionieren!
Leider daneben – du hast glaube ich wieder bei 1 angefangen zu zählen…
Mit dem Orientieren in so einem zweidimensionalen Array wirst du dich nach und nach besser auskennen – am Anfang ist das für viele gewöhnungsbedürftig.
Um den nächsten Schritt zu gehen: Wenn die x-Achse von links nach rechts geht, und die y-Achse von oben nach unten (also oben links die Koordinate x: 0 - y: 0 ist) – welche Koordinate geben wir dann in unserem Array von Arrays als erste an?
So bist du es vermutlich gewöhnt, doch leider stimmt das hier nicht.
Gut erkannt!
Als erstes suchen wir die richtige Zeile (gehen also auf der y-Achse nach unten) und dann suchen wir in der Zeile (gehen also auf der x-Achse nach rechts) die richtige Stelle.
Dann können wir nun eine Methode schreiben, die die Landschaft auch ausgibt. Lege dazu in der Klasse Landkarte folgende Methode an:
public String getLandschaft(int x, int y) {
return this.karte[y][x];
}
Im Methodenkopf können wir die Reihenfolge der Koordinaten nun natürlich frei bestimmen – da liegt es Nahe, die gewohnte Reihenfolge (zuerst x, dann y) einzuhalten. Erst bei der Abfrage ans Array drehen wir sie dann um.
In dem du nach dem Bauplan der Klasse Landkarte
eine Landkarten-Objekt erzeugst, und einmal irgendeine Koordinate ausgeben lässt! Versuche das selbst einmal, und schreibe ein entsprechendes Skript in eine Start-Datei.
Schreibe dir in einer Datei, die du am besten »Start« nennst, ein kleines Skript wie dieses: (in echtem Java wäre das der Inhalt der main-Methode einer Start-Klasse.)
Landkarte meineKarte = new Landkarte();
println(meineKarte.getLandschaft(2, 3));
Variiere auch die Orte, die du dir ausgeben lässt, um zu sehen, ob alles wie erwartet funktioniert.
Sehr schön!
Das muss sogar so sein – wenn du die Karte verlässt, sind wir außerhalb des Arrays und bekommen einen index-out-of-bounds-Fehler. Nachher wird es deine Aufgabe sein, deine Klassen so zu verbessern, dass mit solchen Fehlern besser umgegangen wird. Fürs erste solltest du versuchen, nicht vom Rand der Karte zu fallen…
Dann wird es jetzt Zeit, eine Person in unsere Karte zu setzen. In unserem Programm muss eine solche Person nicht viel wissen, aber an den Ort (also: an die Koordinaten), an dem sie sich befindet, sollte sie sich erinnern.
public class Person {
private int x;
private int y;
}
Das ist völlig richtig! Die Landkarte schreiben wir hier auch noch mit dazu:
public class Person {
private Landkarte karte = new Landkarte();
private int x;
private int y;
}
Angehende Softwarearchitekt_innen greifen sich an dieser Stelle vielleicht an den Kopf…
In einem umfangreicheren Programm, wo vielleicht mehrere Personen auf der Karte herumlaufen, müssten wir uns eine bessere Lösung überlegen, z.B. könnten wir einen Konstruktor schreiben, und eine Landkarte von außen in die Person hereinreichen. (So könnten wir vielen Personen die selbe Landkarte übergeben.) Für unser Beispiel hier ist es in Ordnung, wenn sich unsere Person die Landkarte selbst erzeugt.
Wir sollten außerdem eine Methode schreiben, die uns eine Beschreibung der Landschaft und der Koordinaten ausgibt – mit der wir unsere Person also bitten können, zu sagen, wo sie ist. Sie könnte z.B. beschreibeStandort()
heißen.
Sehr schön!
Nun ja, zunächst sollte die Methode public
sein, damit sie von außen erreichbar ist. Weil sie nichts zurückgibt (dafür aber etwas am Bildschirm ausgibt), ist der Rückgabetyp leer (also void
). »beschreibeStandort()« ist der hier vorgegebene Name (es sei denn, dir fällt ein besserer ein), und die Klammern bleiben leer, weil die Methode nichts entgegennimmt. Damit sieht der Methodenkopf so aus:
public void beschreibeStandort() {
}
Das ist hoffentlich selbst erkärend. Wir geben die Koordinaten unseres Person-Objektes aus (mit this.x
und this.y
), und rufen auch die Beschreibung der Landkarte von der Stelle ab, an der sich die Person gerade befindet:
public void beschreibeStandort() {
println("x: " + this.x);
println("y: " + this.y);
println("Ich sehe " + this.karte.getLandschaft(this.x, this.y));
}
Um die neue Methode auszuprobieren, solltest du das Start-Skript verändern:
Person ich = new Person();
ich.beschreibeStandort();
Wenn du alles wie hier beschrieben gemacht hast, welche Ausgabe wirst du dann erhalten?
Das stimmt leider nicht. Weil die primitiven int-Attribute standardmäßig mit 0 initialisiert werden, landet unsere Person an der Stelle x: 0, y: 0, und das ist das Ödland.
Das stimmt leider nicht. Weil die primitiven int-Attribute standardmäßig mit 0 initialisiert werden, landet unsere Person an der Stelle x: 0, y: 0, und das ist das Ödland.
Gut gemacht, das ist richtig!
Das stimmt leider nicht. Weil die primitiven int-Attribute standardmäßig mit 0 initialisiert werden, landet unsere Person an der Stelle x: 0, y: 0, und das ist das Ödland.
Das stimmt leider nicht. Weil die primitiven int-Attribute standardmäßig mit 0 initialisiert werden, landet unsere Person an der Stelle x: 0, y: 0, und das ist das Ödland.
Nun kommt der interessante Teil: unsere Person lernt endlich laufen! Wir gehen davon aus, dass die Person irgendwo auf unserer Karte steht, und nach Norden, Osten, Süden oder Westen laufen möchte. Die Richtung geben wir über eine Zeichenkette in die Methode hinein, und verändern in der Methode lediglich die x- oder y-Koordinate passend. Die Methode könnte z.B. »geheNach« heißen.
Zunächst schreiben wir den Kopf der Methode:
public void geheNach(String richtung) {
}
Die Methode ist public
, weil wir sie von außen ansprechen möchten, void
, weil sie nichts zurückgibt, heißt »geheNach«, weil das dem Inhalt der Methode entspricht¹, und nimmt mit String richtung
eine Zeichenkette mit der Richtung entgegen.
¹ vielleicht wäre »geheInRichtung« noch besser gewesen – allerdings auch länger…
Die einfache Lösung ist ziemlich gerade heraus. Wir sehen nach, welche Richtung übergeben wurde und verändern die entsprechende Koordinate:
public void geheNach(String richtung) {
if (richtung.equals("n")) {
y--;
} else if (richtung.equals("s")) {
y++;
} else if (richtung.equals("o")) {
x++;
} else if (richtung.equals("w")) {
x--;
}
}
Damit kann ein nach unserer Klasse Person
gebautes Objekt erst einmal alles, was es soll.
Dafür orientieren wir uns an einer typischen Computerspielarchitektur. Wir verwenden eine while
-Wiederholung, in der immer wieder nach einer Eingabe gefragt wird, und setzen die Eingabe Schritt für Schritt um. Das könnte z.B. so aussehen (in unserem Start-Skript):
Person ich = new Person();
println("Herzlich willkommen in unserem Land!");
println("Tippe »Enter« zum beenden.");
String eingabe = ".";
while (!eingabe.isEmpty()) {
ich.beschreibeStandort();
eingabe = Input.readString("In welche Richtung (n/o/w/s) möchtest du gehen?");
ich.geheNach(eingabe);
}
Lies’ dir das Skript Zeile für Zeile durch.
Verstehst du die Zeile String eingabe = ".";
?
Gut!
Der Punkt hat keine Bedeutung. Es ist nur wichtig, dass der String nicht leer ist, weil das das Abbruchkriterium in der nächsten Zeile ist. Außerdem wäre die Angabe irgendeiner Richtung nicht gut, weil dann die Person schon ohne Eingabe irgendwohin gehen würde…
Kannst du nachvollziehen, was in der Zeile while (!eingabe.isEmpty()) {
überprüft wird?
Sehr schön!
Hier bedeutet das !
etwas bestimmtes, es dreht nämlich den Wahrheitswert um, der danach festgestellt wird. Das !
lässt sich also einfach als »nicht« lesen.
eingabe.isEmpty()
prüft, ob die Eingabe leer ist. Wenn das der Fall wäre, würde true
zurückgegeben. Wenn wir also eine Eingabe wie »n«, »s«, »o« oder »w« (oder irgendeine andere Eingabe) machen, ergibt sich bei eingabe.isEmpty()
ein false
, und mit dem !
davor ein true
. Solange wir also Eingaben machen, läuft die while
-Wiederholung weiter. (Damit die while
-Wiederholung nicht sofort wieder verlassen wird, hatten wir zuvor einen Punkt in der Variable eingabe
abgespeichert.)
Verstehst du, was in der while
-Wiederholung passiert?
Das ist gut.
Diese drei Zeilen werden wiederholt, solange die Variable eingabe
nicht leer ist:
ich.beschreibeStandort();
eingabe = Input.readString("In welche Richtung (n/o/w/s) möchtest du gehen?");
ich.geheNach(eingabe);
Das Person-Objekt (hier ich
) beschreibt den Standort, dazu wird die Methode, die wir zu diesem Zweck geschrieben haben, aufgerufen.
In der zweiten Zeile wird eine neue Eingabe eingelesen. Wenn unser User/unsere Userin hier mit der Eingabetast quitiert, wird ein leerer String in eingabe
abgespeichert.
In der dritten Zeile wird die Eingabe verarbeitet, das Person-Objekt bewegt sich in die angegebene Richtung. (Sollten dabei andere Zeicheketten als »n«, »s«, »o« oder »w« übergeben werden, oder auch ein leerer String, wird die Methode einfach keine Wirkung haben – für so etwas haben wir ja keinen Fall definiert. Die Methode geheNach
würde betreten und wieder verlassen, ohne das etwas passiert.)
Vielleicht fällt dir beim Herumlaufen auf der Karte etwas auf…
Wenn du über den Rand der Karte hinaus gehst, bricht das Programm mit einer Fehlermeldung ab. Technisch gesprochen verlassen wir den Bereich gültiger Indizes unseres Karten-Arrays, und wir bekommen einen Index-out-of-bounds-Fehler.
Das stimmt! Trotzdem hast du an dieser sehr einfachen Version hoffentlich etwas über die Verwendung mehrdimensionaler Arrays gelernt – sie lassen sich als eine Art Karte für viele Dinge einsetzen. (Übrigens funktionieren noch höher-dimensionale Arrays ganz genau so…)
Wenn du möchtest, verbessere doch das Programm, und baue einen Sicherungsmechanismus ein, der Personen daran hindert, von der Karte zu fallen.
Brauchst du ein paar Ideen dazu?
%
ist der modulo-Operator, er berechnet den Rest einer Ganzzahldivision.
Mit -1
multipliziert wird eine negative Zahl wieder positiv.
Wahrheitsabfragen lassen sich mit &&
verknüpfen (&&
steht für das logische UND).
Die Länge eines Arrays lässt sich mit nameMeinesArrays.length
bestimmen.
Diese Hinweise sind für unterschiedliche Lösungsansätze nützlich, vielleicht hast du ja selbst eine gute Idee, die du umsetzen möchtest.
Neue Vokabeln in dieser Lektion
Schreibe den entsprechenden Code auf, und überprüfe, ob du richtig liegst!
Erzeuge ein zweidimensonales Ganzzahl-Array plan
, mit zehn mal zehn Elementen.
int[] plan = new int[10][10];
Erzeuge ein Array von boolean-Arrays wahrheitstafelNicht mit den Elementen true
, false
und in der zweiten Zeile false
, true
.
boolean[][] wahrheitstafelNicht = { {true, false}
{false, true}
};
Greife aus dem eben erstellten Wahrheitstafel-Array auf das letzt Element der zweiten Zeile zu, und speicher dieses in der Variable b
ab.
boolean b = wahrheitstafelNicht[1][1];