Informatik

Im Spiel: das Level als Gitter

Optional. Diese Seite ist ein Zusatzangebot am Ende des Kapitels. Sie führt nichts Neues ein, und weder der Rückblick noch die späteren Kapitel setzen sie voraus.

Sie baut auf Im Spiel: ein Typ für alle Objekte auf. Dort ist Spielobjekt als abstrakte Oberklasse entstanden; hier bekommt das Spiel ein Level.

Scratch for Java in fünf Minuten

Diese Seite macht einen Entwurf mit der Grafikbibliothek Scratch for Java sichtbar. Du musst sie nicht kennen – hier steht alles, was du dafür brauchst. Geladen wird sie über libraries="scratch"; import-Zeilen gibt es nicht.

Begriff Was er bedeutet
Stage die Bühne, 480 × 360 Pixel. Der Punkt (0, 0) liegt in der Mitte, die y-Achse zeigt nach oben. Man schreibt eine eigene Klasse, die von Stage erbt.
Sprite eine Figur auf der Bühne. Auch dafür schreibt man eigene Klassen, die von Sprite erben.
addCostume("coin_gold") gibt einer Figur ihr Bild. Die Bibliothek bringt über 1700 fertige Kostüme mit.
this.add(objekt) fügt eine Figur oder einen Text der Bühne hinzu. Was nicht hinzugefügt wurde, sieht man nicht.
run() wird für jedes Objekt etwa 60-mal pro Sekunde aufgerufen. Alles Fortlaufende steht hier drin.
getTouchingSprite(Muenze.class) liefert das berührte Objekt oder null, wenn gerade keins berührt wird. Das null muss man immer abfragen.

Dazu isKeyPressed(KeyCode.RIGHT) für die Tastatur, changeX(4) zum Bewegen, remove() zum Entfernen und new Text(text, x, y, breite) mit showText(...) für die Anzeige.

Zum Nachlesen: Die erste Grafik erklärt Bühne und Koordinatensystem, Eigene Sprites die eigenen Figurenklassen mit run(). Alle Methoden auf einen Blick: Referenz.

Woran es hakt

Bisher wurden die Gegenstände mit pickRandom über die Bühne gestreut:

for (int i = 0; i < 5; i++) {
    this.add(new Muenze(this.pickRandom(-200, 200), this.pickRandom(-20, 120)));
}

Das läuft – aber es ist kein Level. Man kann es nicht entwerfen, nicht zweimal gleich spielen, keinem Mitschüler zeigen und über die Stelle reden, die zu schwer ist. Und zwei Münzen können übereinanderliegen.

Stufe 1: der Plan und das Gitter

Ein Level ist eine Fläche, und Flächen beschreibt man mit zwei Indizes: Zeile und Spalte. Genau dafür gibt es zweidimensionale Felder.

Wir brauchen zwei Dinge, und sie werden leicht verwechselt:

Der Plan ist, was der Mensch schreibt: ein Feld aus Zeichenketten, in dem jedes Zeichen für eine Sorte steht. Er wird einmal gelesen und danach nicht mehr gebraucht.

Das Gitter ist, was das Programm führt: ein zweidimensionales Feld, in dem für jedes Feld steht, welches Objekt dort liegt. Es wird während des ganzen Spiels gebraucht.

private String[] plan = {
    "M.........M",
    ".F.......F.",
    "..M.....M..",
    "...H...H...",
    "....M.M...."
};

Das liest sich wie eine Landkarte – und genau das ist der Gewinn. Wer das Level ändern will, ändert Zeichen, nicht Programmcode.

Eine Figur muss in ihre Zelle passen. Sonst behauptet das Gitter etwas anderes, als man auf der Bühne sieht: Eine Figur, die zwei Zellen hoch ist, steht rechnerisch auf einem Feld und optisch auf zweien – und spätestens wenn ein Programm mit dem Gitter rechnet, stimmt das Ergebnis nicht mehr mit dem Bild überein.

Das ist nicht so einfach hinzuschreiben, wie es klingt, denn setSize erwartet Prozent der Originalgröße, nicht Pixel. Und die Kostüme sind sehr verschieden groß:

Kostüm Originalgröße bei setSize(40)
bunny1_stand 120 × 201 48 × 80 – doppelt so hoch wie eine Zelle
gemRed 128 × 128 51 × 51 – größer als die Zelle
coin_gold 61 × 61 24 × 24
flame 41 × 80 16 × 32

Dieselbe Prozentzahl ergibt also ganz verschiedene Pixelgrößen. Gebraucht wird die umgekehrte Rechnung: Welche Prozentzahl ergibt genau 40 Pixel? Dafür liefern getWidth() und getHeight() die aktuelle Größe – und die Klasse Zelle unten rechnet es für jede Figur aus, egal welches Kostüm sie trägt.

Spielobjekt[][] gitter speichert keine Objekte. Es speichert Verweise auf Objekte. Ein Platz, auf dem nichts liegt, enthält null – und null ist kein Objekt, sondern die Auskunft „hier zeigt nichts hin". Das ist derselbe Gedanke wie in 2.2 Referenzen, nur zweidimensional.

Unten läuft das fertige Level. Sammle ein und achte auf die beiden Zahlen oben.

Stufe 2: der Fehler, den man nicht sieht

Hast du auf die Zahlen geachtet? Sie laufen auseinander. Auf der Bühne verschwindet jede eingesammelte Münze, im Gitter bleibt sie stehen.

Das ist kein Anzeigefehler, sondern genau das Thema von 2.2. Nach dem Aufbau zeigen zwei Stellen auf dasselbe Objekt: die Bühne und gitter[z][s].

this.remove() entfernt das Objekt von der Bühne. Von dem Verweis im Gitter weiß die Bühne nichts – sie kann ihn gar nicht kennen. Also bleibt er stehen und zeigt weiter auf ein Objekt, das im Spiel keine Rolle mehr spielt.

Ein Objekt verschwindet nicht, weil ein Verweis darauf gelöscht wird. Und es bleibt nicht, weil noch einer darauf zeigt.

Aufgabe 1: den Bug beheben

a) Sage voraus: Du sammelst drei Gegenstände ein. Welche beiden Zahlen stehen danach oben?

b) Das Objekt kennt seinen Platz – getZeile() und getSpalte(). Schreibe in Spiel eine Methode leere(int pZeile, int pSpalte), die den Verweis im Gitter auf null setzt und die Anzeige auffrischt.

c) Rufe sie an den drei Stellen auf, an denen ein Objekt verschwindet. Danach müssen die Zahlen gleich bleiben.

d) Warum kann man das nicht in Spieler.run() erledigen, sondern muss es in jeder Unterklasse tun? (Tipp: Die Falle verschwindet, die Bombe aus dem letzten Kapitel nicht.)

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Stufe 3: was ein Gitter kann, was ein Feld nicht kann

Mit dem Gitter kann das Spiel Fragen beantworten, für die es vorher jedes Objekt hätte durchsuchen müssen: Was liegt links von mir? Ist dieser Platz frei? Ein Zugriff statt einer Schleife.

Aufgabe 2: Nachbarn

a) Schreibe istFrei(int pZeile, int pSpalte), die true liefert, wenn auf dem Platz nichts liegt. Denke an Plätze außerhalb des Gitters.

b) Schreibe anzahlNachbarn(int pZeile, int pSpalte): Wie viele der acht umliegenden Plätze sind belegt?

c) Färbe beim Aufbau jedes Objekt mit mehr als zwei Nachbarn rot ein (setTint(255, 0, 0)).

d) Warum ist die Prüfung pZeile >= 0 && pZeile < gitter.length in a) unverzichtbar – und warum genügt gitter[0].length nicht immer als Spaltengrenze?

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Zum Weiterdenken: generisch machen. Das Gitter kann bisher nur Spielobjekte. Die Logik – anlegen, füllen, Grenzen prüfen, Nachbarn zählen – hat damit aber nichts zu tun.

Schreibe eine generische Klasse Gitter<T> mit setze(z, s, T inhalt), hole(z, s) und istFrei(z, s). Dann speichert sie ebenso gut Wahrheitswerte für „hier war ich schon" oder Zahlen für eine Wärmekarte.

Eine Falle wartet dabei: new T[zeilen][spalten] ist in Java nicht erlaubt. Was nimmt man stattdessen – und was handelt man sich damit ein?

Und ohne Spiel?

Im Spiel In den Aufgaben dieses Kapitels
Plan aus Zeichen → Gitter aus Verweisen Sitzplan, Spielbrett, Pixelbild
gitter[z][s] == null „auf diesem Platz sitzt niemand"
Bühne und Gitter laufen auseinander zwei Variablen zeigen auf dasselbe Feld, eine ändert es
gitter[pZeile].length statt gitter[0].length das pascalsche Dreieck mit verschieden langen Zeilen

Die Einsicht, die bleibt: Ein zweidimensionales Feld speichert Verweise. Wer das einmal an verschwundenen Münzen gesehen hat, sucht den Fehler in der Klausur nicht mehr im Algorithmus.


Selbsttest

Im Spiel: das Level als Gitter

Teilbare URL erstellen

Abschnitte auswählen