Ein Stapel von Bildschirmen
Kurz gesagt: Ein Stapel von Bildschirmen hält die geöffneten Bildschirme übereinander: Die Eingabe geht an den obersten, push öffnet einen neuen, pop schließt ihn, replace tauscht ihn aus, und durchsichtige werden über die darunterliegenden gezeichnet.
Problem
Eine Variable für alle Bildschirme
Das Spiel hat einen Titelbildschirm, ein Dorf, die Karte, einen Kampf, ein Inventar, das sich über der Karte öffnet, und ein Menü, das sich über dem Kampf öffnet. Jeder Bildschirm liest Befehle auf seine Art: Auf der Karte ist a ein Schritt nach links, im Kampf ein Hieb, im Inventar nichts.
Der erste Versuch, den fast alle machen: eine Variable mit dem Namen des Bildschirms und ein if darauf. Einmal bei der Behandlung des Befehls und noch einmal beim Zeichnen:
Scanner in = new Scanner(System.in);String screen = "title";int wolf = 2;while (in.hasNextLine()) { String command = in.nextLine().trim(); if (screen.equals("title")) { if (command.equals("n")) screen = "map"; } else if (screen.equals("map")) { if (command.equals("i")) screen = "inventory"; if (command.equals("f")) screen = "battle"; } else if (screen.equals("battle")) { if (command.equals("i")) screen = "inventory"; if (command.equals("a")) { wolf--; if (wolf == 0) screen = "map"; } } else if (screen.equals("inventory")) { if (command.equals("x")) screen = "map"; } System.out.print(command + " → "); if (screen.equals("title")) System.out.println("Chroniken von Arkanum"); else if (screen.equals("map")) System.out.println("Karte, Wolf: " + wolf); else if (screen.equals("battle")) System.out.println("Kampf, Wolf: " + wolf); else if (screen.equals("inventory")) System.out.println("Inventar: Trank x2");}n → Karte, Wolf: 2i → Inventar: Trank x2x → Karte, Wolf: 2f → Kampf, Wolf: 2i → Inventar: Trank x2x → Karte, Wolf: 2a → Karte, Wolf: 2a → Karte, Wolf: 2Die Befehle kommen einer pro Zeile: n neues Spiel, i Inventar, x schließen, f gegen den Wolf kämpfen, a Hieb.
nixfixaaSieh dir die sechste Zeile an. Das Inventar wurde aus dem Kampf geöffnet, und x schließt es zur Karte hin. Der Kampf ist weg, der Wolf lebt mit 2 Leben, und die beiden Hiebe danach landen auf der Karte, wo a nichts bedeutet. Die Variable merkt sich, wo du bist, aber nicht, woher du gekommen bist.
Das zweite Problem zeigt sich, wenn das Spiel wächst. Jeder neue Bildschirm ist ein neuer Zweig in jedem if: bei der Behandlung, beim Zeichnen und in jedem Bildschirm, der ihn öffnet. Ein Menü über dem Kampf und über der Karte sind zwei weitere Zweige an zwei Stellen. Vergisst du einen, hast du einen Bildschirm, der Befehle annimmt, aber nie gezeichnet wird.
Die Idee
Ein Bildschirm entscheidet über sich selbst
Dreh die Frage um. Statt dass die Schleife fragt „auf welchem Bildschirm bin ich“ und jeden Bildschirm kennt, ist jeder Bildschirm ein Objekt mit denselben vier Methoden:
handle: was der Befehl hier bedeutet;update: was jeden Zug passiert, solange der Bildschirm oben liegt (im Kampf beißt der Wolf);draw: was der Bildschirm zeichnet;isTransparent: ob der Bildschirm darunter durchscheint.
abstract class Screen { Game game; String name;
Screen(Game game, String name) { this.game = game; this.name = name; }
boolean isTransparent() { return false; }
abstract void handle(String command);
void update() {}
abstract void draw(TextBuffer buffer);}abstract sagt, dass jeder Bildschirm sein eigenes handle und sein eigenes draw haben muss: Ohne sie lässt sich die Klasse nicht kompilieren. update und isTransparent haben ein Standardverhalten, und ein Bildschirm schreibt nur dann ein eigenes, wenn er es braucht.Hier sind der Kampf und das Inventar. Der Kampf kennt nur sich selbst: i öffnet das Inventar obenauf, a schlägt zu, und wenn der Wolf fällt, entfernt sich der Kampf selbst. Das Inventar weiß nicht, woraus es geöffnet wurde. Es sagt nur „entferne mich“:
class BattleScreen extends Screen { BattleScreen(Game game) { super(game, "Kampf"); }
@Override void handle(String command) { if (command.equals("i")) game.stack.push(new InventoryScreen(game)); if (command.equals("a")) { game.wolf--; if (game.wolf == 0) game.stack.pop(); } }
// In jedem Zug des Kampfes beißt der Wolf. @Override void update() { if (game.wolf > 0) game.life--; }
@Override void draw(TextBuffer buffer) { buffer.box(0, 0, TextBuffer.W, TextBuffer.H); buffer.write(2, 1, "Wolf: " + game.wolf); buffer.write(2, 2, "Leben: " + game.life); buffer.write(2, 3, "a: Hieb i: Inventar"); }}class InventoryScreen extends Screen { InventoryScreen(Game game) { super(game, "Inventar"); }
@Override void handle(String command) { if (command.equals("x")) game.stack.pop(); }
@Override void draw(TextBuffer buffer) { buffer.box(12, 1, 11, 3); buffer.write(13, 2, "Trank x2"); }}Die Bildschirme zeichnen in einen TextBuffer: fünf Zeilen zu je 24 Zeichen. write setzt Text an eine bestimmte Stelle, box zeichnet einen Rahmen. Der ganze Puffer steht im Programm unten, und wie das Zeichnen in einem echten Terminal funktioniert, steht in der Lektion über das Zeichnen.
Ein neuer Bildschirm ist eine neue Klasse und eine Zeile in dem Bildschirm, der ihn öffnet. Kein if auf Bildschirmnamen wächst.
Genauer, für Neugierige
So sieht es im Spiel aus. Die Region screen des Kanons:
// Jeder Bildschirm ist ein Objekt mit derselben Schnittstelle. Das Spiel fragt nicht „auf welchem Bildschirm bin ich“; es schickt den Befehl an// den Bildschirm oben auf dem Stapel.static class Screen { Game game; String kindName = "";
Screen(Game game) { this.game = game; }
// Ein durchsichtiger Bildschirm (Menü, Inventar, Dialog) lässt den Bildschirm darunter durchscheinen. boolean isTransparent() { return false; }
void handle(String command) {}
void update() {}
void draw(CellBuffer buffer) {}
// Der Bildschirm unter dem entfernten bekommt seine Antwort (das gewählte Ziel, „ja“ oder „nein“, der gewählte Slot). void onResult(Object result) {}}Die Methode onResult ist für Bildschirme, die eine Antwort zurückschicken (das gewählte Ziel, „ja“ oder „nein“). Sie ist das Thema der nächsten Lektion.
Die Idee
Der Stapel: woher du gekommen bist
Bildschirme liegen übereinander: das Inventar über dem Kampf, der Kampf über der Karte. Wenn du den obersten schließt, willst du genau dort sein, wo du warst. Das ist ein Stapel: Der Letzte, der hineinkommt, geht als Erster hinaus. Der Stapel der Bildschirme hat drei Operationen:
- push: einen neuen Bildschirm obenauf öffnen (die Karte öffnet den Kampf, der Kampf öffnet das Inventar);
- pop: den obersten schließen und zu dem darunter zurückkehren;
- replace: den obersten gegen einen neuen austauschen, ohne Weg zurück (der Titelbildschirm wird zur Karte, die Heldenerschaffung wird zum Dorf).
class ScreenStack { List<Screen> screens = new ArrayList<>();
void push(Screen screen) { screens.add(screen); }
void pop() { screens.remove(screens.size() - 1); }
void replace(Screen screen) { pop(); push(screen); }
Screen top() { return screens.get(screens.size() - 1); }
String names() { List<String> names = new ArrayList<>(); for (Screen screen : screens) names.add(screen.name); return "[" + String.join(", ", names) + "]"; }
void draw(TextBuffer buffer) { top().draw(buffer); }}Die Spielschleife gibt den Befehl nur an den obersten. Nicht an die Karte, nicht an den Kampf unter dem Inventar. Nur der oberste weiß, was der Befehl gerade bedeutet. Dieselben Befehle, jetzt mit dem Stapel. show gibt nach jedem Befehl den Stapel und das Bild aus:
static void show(Game game, TextBuffer buffer) { System.out.println(game.stack.names()); buffer.clear(); game.stack.draw(buffer); buffer.print();}
Game game = new Game();TextBuffer buffer = new TextBuffer();game.stack.push(new TitleScreen(game));show(game, buffer);Scanner in = new Scanner(System.in);while (in.hasNextLine()) { String command = in.nextLine().trim(); System.out.println("> " + command); Screen top = game.stack.top(); top.handle(command); top.update(); show(game, buffer);}[Titel]
Chroniken von Arkanum
n: Neues Spiel
> n[Karte]#########################..@......~~~..........##.......w.~~~..........######################### Leben: 10> i[Karte, Inventar]
+---------+ |Trank x2 | +---------+
> x[Karte]#########################..@......~~~..........##.......w.~~~..........######################### Leben: 10> f[Karte, Kampf]+----------------------+| Wolf: 2 || Leben: 10 || a: Hieb i: Inventar |+----------------------+> i[Karte, Kampf, Inventar]
+---------+ |Trank x2 | +---------+
> x[Karte, Kampf]+----------------------+| Wolf: 2 || Leben: 9 || a: Hieb i: Inventar |+----------------------+> a[Karte, Kampf]+----------------------+| Wolf: 1 || Leben: 8 || a: Hieb i: Inventar |+----------------------+> a[Karte]#########################..@......~~~..........##.........~~~..........######################### Leben: 8Das Inventar, das aus dem Kampf geöffnet wurde, schließt sich jetzt zurück zum Kampf: [Karte, Kampf]. Der Wolf fällt beim zweiten Hieb, der Kampf entfernt sich selbst, und darunter liegt die Karte, jetzt ohne das w. Kein Bildschirm merkt sich, woher er kam. Der Stapel tut es.
Aber sieh dir die Bilder mit dem Inventar an. Die Karte ist verschwunden, und der Rahmen hängt im leeren Raum. Das draw des Stapels zeichnet nur den obersten. Darum geht es als Nächstes.
Genauer, für Neugierige
Nur der oberste wird aktualisiert. Die Schleife ruft update für den obersten auf. Solange das Inventar über dem Kampf offen ist, wartet der Kampf und der Wolf beißt nicht: Das Spiel ist unter dem Inventar angehalten. Das ist eine Entscheidung, kein Gesetz. In einem Echtzeitspiel kann die Welt unter dem Menü weiterlaufen, und dann aktualisiert der Stapel auch Bildschirme unter dem obersten, die sagen, dass sie nicht anhalten.
pop auf einem leeren Stapel. Wenn sich der letzte Bildschirm selbst entfernt, ist der Stapel leer, und das nächste pop hat nichts zu entfernen. In Java wirft remove(-1) eine IndexOutOfBoundsException. Im Spiel bedeutet ein leerer Stapel das Ende: Die Schleife läuft, bis der Stapel leer ist, und „Beenden“ im Titelbildschirm leert den Stapel.
Probier es aus
Sage die Ausgabe voraus
Der Stapel ist leer. Was liegt am Ende darauf?
Game game = new Game();ScreenStack stack = game.stack;stack.push(new TitleScreen(game));stack.replace(new MapScreen(game));stack.push(new BattleScreen(game));stack.push(new InventoryScreen(game));stack.pop();stack.pop();stack.push(new InventoryScreen(game));System.out.println(stack.names());Die Idee
Durchsichtige Bildschirme
Das Inventar ist ein kleiner Rahmen über der Karte. Damit die Karte um ihn herum zu sehen ist, muss die Karte zuerst gezeichnet werden und das Inventar danach, darüber. Wie ein Maler, der zuerst den Hintergrund malt und dann die Figuren davor.
Aber nicht vom untersten Bildschirm aus. Unter dem Kampf liegt die Karte, und der Kampf verdeckt sie ganz: Sie zu zeichnen wäre verschwendete Arbeit. Also beginnt das Zeichnen beim obersten undurchsichtigen Bildschirm. Geh von oben nach unten, solange die Bildschirme durchsichtig sind. Der erste, der es nicht ist, ist der Anfang. Von dort zeichnest du nach oben:
void draw(TextBuffer buffer) { int first = screens.size() - 1; while (first > 0 && screens.get(first).isTransparent()) first--; for (int i = first; i < screens.size(); i++) { screens.get(i).draw(buffer); }}Das Inventar sagt, dass es durchsichtig ist:
class InventoryScreen extends Screen { InventoryScreen(Game game) { super(game, "Inventar"); }
@Override boolean isTransparent() { return true; }
@Override void handle(String command) { if (command.equals("x")) game.stack.pop(); }
@Override void draw(TextBuffer buffer) { buffer.box(12, 1, 11, 3); buffer.write(13, 2, "Trank x2"); }}Nur diese beiden Änderungen. Dieselbe Schleife, dieselben Befehle:
[Titel]
Chroniken von Arkanum
n: Neues Spiel
> n[Karte]#########################..@......~~~..........##.......w.~~~..........######################### Leben: 10> i[Karte, Inventar]#########################..@......~~+---------+##.......w.~~|Trank x2 |#############+---------+# Leben: 10> x[Karte]#########################..@......~~~..........##.......w.~~~..........######################### Leben: 10> f[Karte, Kampf]+----------------------+| Wolf: 2 || Leben: 10 || a: Hieb i: Inventar |+----------------------+> i[Karte, Kampf, Inventar]+----------------------+| Wolf: 2 +---------+|| Leben: 9 |Trank x2 ||| a: Hieb i+---------+|+----------------------+> x[Karte, Kampf]+----------------------+| Wolf: 2 || Leben: 9 || a: Hieb i: Inventar |+----------------------+> a[Karte, Kampf]+----------------------+| Wolf: 1 || Leben: 8 || a: Hieb i: Inventar |+----------------------+> a[Karte]#########################..@......~~~..........##.........~~~..........######################### Leben: 8[Karte, Inventar]: die Karte, mit dem Rahmen darüber, und der Wolf ist darunter weiter zu sehen. [Karte, Kampf, Inventar]: Das Zeichnen beginnt beim Kampf, weil er nicht durchsichtig ist. Die Karte darunter wird gar nicht gezeichnet.
first > 0 hält beim untersten Bildschirm an: Er wird immer gezeichnet, auch wenn er durchsichtig ist, weil nichts darunter liegt.
Genauer, für Neugierige
Die Methode draw aus der Region screen-stack des Kanons:
// Das Zeichnen beginnt beim obersten undurchsichtigen Bildschirm und geht nach oben.void draw(CellBuffer buffer) { int first = this.screens.size() - 1; while (first > 0 && this.screens.get(first).isTransparent()) first--; for (int i = first; i < this.screens.size(); i++) { if (i >= 0) this.screens.get(i).draw(buffer); }}Die Prüfung i >= 0 ist für einen leeren Stapel: Dann ist first gleich −1, und es gibt nichts zu zeichnen.
Warum nicht einfach bei null? Wenn jeder undurchsichtige Bildschirm alles verdeckt, ist das Ergebnis dasselbe, aber jedes Bild zeichnet alle Bildschirme darunter: Karte, Kampf, Zielwahl, Dialog. Es gibt auch etwas Schlimmeres: Ein undurchsichtiger Bildschirm, der nicht jede Zelle beschreibt, lässt Stücke der Karte durchscheinen. „Ich bin nicht durchsichtig“ ist ein Versprechen, das besagt „Ich verdecke alles“, und der Stapel verlässt sich darauf.
Sieh es dir an
Zug um Zug
Dasselbe Programm mit dem durchsichtigen Inventar. Links ist der Stapel: Jede Platte ist ein Bildschirm mit dem, was er für sich allein zeichnet. Der Befehl geht an die oberste Platte. Die verblassten Platten liegen unter dem obersten undurchsichtigen und werden nicht gezeichnet. Unter dem Stapel ist das Bild, das das Programm ausgibt.
class ScreenStack { List<Screen> screens = new ArrayList<>();
void push(Screen screen) { screens.add(screen); }
void pop() { screens.remove(screens.size() - 1); }
void replace(Screen screen) { pop(); push(screen); }
Screen top() { return screens.get(screens.size() - 1); }
String names() { List<String> names = new ArrayList<>(); for (Screen screen : screens) names.add(screen.name); return "[" + String.join(", ", names) + "]"; }
void draw(TextBuffer buffer) { int first = screens.size() - 1; while (first > 0 && screens.get(first).isTransparent()) first--; for (int i = first; i < screens.size(); i++) { screens.get(i).draw(buffer); } }}
while (in.hasNextLine()) { String command = in.nextLine().trim(); System.out.println("> " + command); Screen top = game.stack.top(); top.handle(command); top.update(); show(game, buffer);}Schritt für Schritt, in Worten
- Das Spiel beginnt mit einem Bildschirm auf dem Stapel: dem Titelbildschirm. Er liegt oben und bekommt die Eingabe.
- n geht an den Titelbildschirm. Er setzt die Karte an seine eigene Stelle (replace): Der Titelbildschirm kommt nicht zurück.
- i geht an die Karte, und sie legt das Inventar obenauf (push). Das Inventar ist durchsichtig: Zuerst wird die Karte gezeichnet, dann das Inventar darüber.
- x geht an das Inventar: Es entfernt sich selbst (pop). Darunter liegt die Karte, genau so, wie du sie verlassen hast.
- f: Die Karte legt den Kampf obenauf (push). Der Kampf ist undurchsichtig, also wird die Karte darunter gar nicht gezeichnet.
- i geht jetzt an den Kampf, nicht an die Karte. Der Kampf öffnet das Inventar über sich. Das Zeichnen beginnt beim Kampf, dem obersten undurchsichtigen Bildschirm. Der Wolf hat gebissen: Leben 9.
- x schließt das Inventar, und darunter liegt der Kampf: Du kehrst dorthin zurück, woher du gekommen bist. Die einzelne Variable vom Anfang der Lektion hat dich hier zur Karte gebracht.
- a geht an den Kampf: Der Wolf wird getroffen, und der Stapel ändert sich nicht. Das update des Kampfes läuft: Der Wolf beißt zurück, Leben 8.
- Der Wolf fällt. Der Kampf entfernt sich selbst (pop), und die Karte liegt wieder oben, jetzt ohne den Wolf.
- Die Eingabe geht nach oben. Das Zeichnen geht von dem obersten undurchsichtigen Bildschirm aus aufwärts. Bildschirme, die eine Antwort zurückschicken, und Änderungen, die auf das Ende des Zuges warten, sind die nächste Lektion.
Probier es aus
Was fehlt?
Das Zeichnen des Stapels. Solange der Bildschirm bei first durchsichtig ist, gehst du nach unten:
void draw(TextBuffer buffer) {
int first = screens.size() - 1;
while (first > 0 && ) first--;
for (int i = first; i < screens.size(); i++) {
screens.get(i).draw(buffer);
}
}Die Frage ist, ob der Bildschirm an der Position first durchsichtig ist: screens.get(first).isTransparent(). Wenn ja, muss auch der darunter gezeichnet werden.
Probier es aus
Finde den Fehler
Ein Kollege hat den Stapel umgeschrieben. Das Programm mit den Befehlen n, i, x gibt aus:
[Titel]
Chroniken von Arkanum
n: Neues Spiel
> n[Karte]#########################..@......~~~..........##.......w.~~~..........######################### Leben: 10> i[Karte, Inventar]#########################..@......~~+---------+##.......w.~~|Trank x2 |#############+---------+# Leben: 10> x[Karte, Inventar]#########################..@......~~+---------+##.......w.~~|Trank x2 |#############+---------+# Leben: 10Das Inventar schließt sich nicht: Nach x ist der Stapel immer noch [Karte, Inventar]. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
Das oberste Element des Stapels ist das letzte, nicht das erste. Mit screens.get(0) geht der Befehl an die Karte ganz unten. Die Karte weiß nicht, was x bedeutet, und das Inventar hört es nie. Die Liste wächst am Ende, also arbeiten push, pop und top alle mit ihrem Ende.
Die Idee
Der Stapel im echten Spiel
Die Region screen-stack des Kanons. Die Methoden sind dieselben, mit ein paar Unterschieden:
// Der Stapel ändert sich während der Verarbeitung nicht: push, pop und replace merken die Änderung nur, und// apply() wendet sie nach dem Zug an. Sonst würde ein Bildschirm, der vom Stapel genommen wird, weiterlaufen.static class StackChange { String kind; Screen screen; Object result;
StackChange(String kind, Screen screen, Object result) { this.kind = kind; this.screen = screen; this.result = result; }}
static class ScreenStack { List<Screen> screens = new ArrayList<>(); List<StackChange> changes = new ArrayList<>();
void push(Screen screen) { this.changes.add(new StackChange("push", screen, null)); }
void pop(Object result) { this.changes.add(new StackChange("pop", null, result)); }
void replace(Screen screen) { this.changes.add(new StackChange("replace", screen, null)); }
// Leert den ganzen Stapel (Laden, Game over); mit null bleibt der Stapel leer. void reset(Screen screen) { this.changes.add(new StackChange("reset", screen, null)); }
Screen top() { return this.screens.size() == 0 ? null : this.screens.get(this.screens.size() - 1); }
boolean isEmpty() { return this.screens.size() == 0 && this.changes.size() == 0; }
boolean contains(String kindName) { for (Screen screen : this.screens) { if (screen.kindName.equals(kindName)) return true; } return false; }
// pop auf einem leeren Stapel tut in JavaScript nichts; in Java wirft remove, darum gibt es eine Prüfung. void removeTop() { if (this.screens.size() > 0) this.screens.remove(this.screens.size() - 1); }
void apply() { int index = 0; while (index < this.changes.size()) { StackChange change = this.changes.get(index); index++; if (change.kind.equals("push")) { this.screens.add(change.screen); } else if (change.kind.equals("pop")) { this.removeTop(); Screen below = this.top(); if (below != null) below.onResult(change.result); } else if (change.kind.equals("replace")) { this.removeTop(); this.screens.add(change.screen); } else { this.screens = new ArrayList<>(); if (change.screen != null) this.screens.add(change.screen); } } this.changes = new ArrayList<>(); }
// Das Zeichnen beginnt beim obersten undurchsichtigen Bildschirm und geht nach oben. void draw(CellBuffer buffer) { int first = this.screens.size() - 1; while (first > 0 && this.screens.get(first).isTransparent()) first--; for (int i = first; i < this.screens.size(); i++) { if (i >= 0) this.screens.get(i).draw(buffer); } }}- push, pop und replace fassen die Liste nicht sofort an. Sie notieren die Änderung in
changes, undapply()führt sie nach dem Zug aus. In unserem Programm entfernt sich der Kampf inhandle, und gleich danach ruft die Schleifeupdatefür denselben Kampf auf, der nicht mehr auf dem Stapel liegt. Hier geht das gut, weil ein toter Wolf nicht beißt. In einem größeren Spiel ist das ein Fehler, den man lange sucht. Warum und wie die Änderungen aufgeschoben werden, ist die nächste Lektion. - pop trägt eine Antwort: Der Bildschirm darunter bekommt sie in
onResult. So gibt der Bildschirm der Zielwahl das Ziel an den Kampf zurück. Auch das steht in der nächsten Lektion. - reset leert den ganzen Stapel (Spiel laden, Ende des Spiels), und contains fragt, ob ein Bildschirm einer bestimmten Art schon auf dem Stapel liegt (zum Beispiel, damit sich kein zweiter Stufenaufstieg-Bildschirm öffnet).
Genauer
Dieselbe Idee anderswo
Die Navigation in mobilen Apps ist ein Stapel von Bildschirmen. Auf Android und iOS öffnest du einen Bildschirm obenauf (push), und die Zurück-Taste entfernt ihn (pop). Der Einstieg nach dem Login ist meist ein replace: Zurück soll nicht wieder zum Login-Formular führen.
Ein modaler Dialog im Browser ist ein Bildschirm über der Seite. Solange er offen ist, bekommt nur er Klicks und Tasten, und die Seite darunter ist sichtbar, reagiert aber nicht. Genau wie das Inventar über der Karte.
Der Aufrufstapel ist dieselbe Idee in der Sprache selbst. Eine Methode ruft eine andere auf (push), die kehrt zurück (pop), und der Aufrufer macht genau dort weiter, wo er aufgehört hat. Der Kampf geht nach dem Inventar auf dieselbe Weise weiter.
Wenn ein Stapel nicht reicht. Ein Stapel sagt, dass genau ein Bildschirm oben liegt und alles bekommt. Manchmal stimmt das nicht:
- zwei Bildschirme nebeneinander, die zusammenarbeiten: eine Karte und eine Minikarte, ein Kampf und das Kampfprotokoll, ein Spiel für zwei Spieler auf geteiltem Bildschirm. Dann ist der Bildschirm ein Baum von Panels, und die Eingabe geht an das Panel mit dem Fokus;
- Dinge, die immer oben liegen, egal welcher Bildschirm: eine Lebensleiste, ein Hinweis „Spiel gespeichert“. Sie sind eine eigene Schicht, die nach dem Stapel gezeichnet wird;
- Fenster, die in beliebiger Reihenfolge geschlossen werden (ein Strategiespiel mit fünf offenen Fenstern). Das ist kein Stapel mehr, sondern eine Liste mit Fokus.
Übung
Ein Menü über allem
Löse das in deinem eigenen Editor. Das ganze Programm mit durchsichtigen Bildschirmen steht unter der Aufgabe.
Menü. Füge einen MenuScreen mit dem Namen „Menü“ hinzu. m öffnet ihn von der Karte, vom Kampf und vom Inventar aus. Das Menü ist durchsichtig, und x schließt es. Es zeichnet einen Rahmen box(14, 2, 10, 3) mit x: raus darin, bei (15, 3).
nfimxxaa[Titel]
Chroniken von Arkanum
n: Neues Spiel
> n[Karte]#########################..@......~~~..........##.......w.~~~..........######################### Leben: 10> f[Karte, Kampf]+----------------------+| Wolf: 2 || Leben: 10 || a: Hieb i: Inventar |+----------------------+> i[Karte, Kampf, Inventar]+----------------------+| Wolf: 2 +---------+|| Leben: 9 |Trank x2 ||| a: Hieb i+---------+|+----------------------+> m[Karte, Kampf, Inventar, Menü]+----------------------+| Wolf: 2 +---------+|| Leben: 9 |T+--------+| a: Hieb i+-|x: raus |+-------------+--------+> x[Karte, Kampf, Inventar]+----------------------+| Wolf: 2 +---------+|| Leben: 9 |Trank x2 ||| a: Hieb i+---------+|+----------------------+> x[Karte, Kampf]+----------------------+| Wolf: 2 || Leben: 9 || a: Hieb i: Inventar |+----------------------+> a[Karte, Kampf]+----------------------+| Wolf: 1 || Leben: 8 || a: Hieb i: Inventar |+----------------------+> a[Karte]#########################..@......~~~..........##.........~~~..........######################### Leben: 8Sieh dir das Bild mit dem Menü an: Der Kampf, das Inventar und das Menü werden gezeichnet, in dieser Reihenfolge. Das Menü verdeckt eine Ecke des Inventars, weil es im Stapel höher liegt.
Genauer, für Neugierige
Das ganze Programm mit durchsichtigen Bildschirmen:
import java.util.ArrayList;import java.util.List;import java.util.Scanner;
class TextBuffer { static final int W = 24; static final int H = 5; String[] rows = new String[H];
void clear() { for (int y = 0; y < H; y++) rows[y] = " ".repeat(W); }
void write(int x, int y, String text) { rows[y] = rows[y].substring(0, x) + text + rows[y].substring(x + text.length()); }
void box(int x, int y, int w, int h) { String edge = "+" + "-".repeat(w - 2) + "+"; write(x, y, edge); for (int i = 1; i < h - 1; i++) write(x, y + i, "|" + " ".repeat(w - 2) + "|"); write(x, y + h - 1, edge); }
void print() { for (String row : rows) System.out.println(row.stripTrailing()); }}
class Game { ScreenStack stack = new ScreenStack(); int life = 10; int wolf = 2;}
abstract class Screen { Game game; String name;
Screen(Game game, String name) { this.game = game; this.name = name; }
boolean isTransparent() { return false; }
abstract void handle(String command);
void update() {}
abstract void draw(TextBuffer buffer);}
class ScreenStack { List<Screen> screens = new ArrayList<>();
void push(Screen screen) { screens.add(screen); }
void pop() { screens.remove(screens.size() - 1); }
void replace(Screen screen) { pop(); push(screen); }
Screen top() { return screens.get(screens.size() - 1); }
String names() { List<String> names = new ArrayList<>(); for (Screen screen : screens) names.add(screen.name); return "[" + String.join(", ", names) + "]"; }
void draw(TextBuffer buffer) { int first = screens.size() - 1; while (first > 0 && screens.get(first).isTransparent()) first--; for (int i = first; i < screens.size(); i++) { screens.get(i).draw(buffer); } }}
class TitleScreen extends Screen { TitleScreen(Game game) { super(game, "Titel"); }
@Override void handle(String command) { if (command.equals("n")) game.stack.replace(new MapScreen(game)); }
@Override void draw(TextBuffer buffer) { buffer.write(1, 1, "Chroniken von Arkanum"); buffer.write(1, 3, "n: Neues Spiel"); }}
class MapScreen extends Screen { static final String[] ROWS = { "########################", "#..@......~~~..........#", "#.........~~~..........#", "########################", };
MapScreen(Game game) { super(game, "Karte"); }
@Override void handle(String command) { if (command.equals("i")) game.stack.push(new InventoryScreen(game)); if (command.equals("f") && game.wolf > 0) game.stack.push(new BattleScreen(game)); }
@Override void draw(TextBuffer buffer) { for (int y = 0; y < ROWS.length; y++) buffer.write(0, y, ROWS[y]); if (game.wolf > 0) buffer.write(8, 2, "w"); buffer.write(1, 4, "Leben: " + game.life); }}
class BattleScreen extends Screen { BattleScreen(Game game) { super(game, "Kampf"); }
@Override void handle(String command) { if (command.equals("i")) game.stack.push(new InventoryScreen(game)); if (command.equals("a")) { game.wolf--; if (game.wolf == 0) game.stack.pop(); } }
// In jedem Zug des Kampfes beißt der Wolf. @Override void update() { if (game.wolf > 0) game.life--; }
@Override void draw(TextBuffer buffer) { buffer.box(0, 0, TextBuffer.W, TextBuffer.H); buffer.write(2, 1, "Wolf: " + game.wolf); buffer.write(2, 2, "Leben: " + game.life); buffer.write(2, 3, "a: Hieb i: Inventar"); }}
class InventoryScreen extends Screen { InventoryScreen(Game game) { super(game, "Inventar"); }
@Override boolean isTransparent() { return true; }
@Override void handle(String command) { if (command.equals("x")) game.stack.pop(); }
@Override void draw(TextBuffer buffer) { buffer.box(12, 1, 11, 3); buffer.write(13, 2, "Trank x2"); }}
public class Main { static void show(Game game, TextBuffer buffer) { System.out.println(game.stack.names()); buffer.clear(); game.stack.draw(buffer); buffer.print(); }
public static void main(String[] args) { Game game = new Game(); TextBuffer buffer = new TextBuffer(); game.stack.push(new TitleScreen(game)); show(game, buffer); Scanner in = new Scanner(System.in); while (in.hasNextLine()) { String command = in.nextLine().trim(); System.out.println("> " + command); Screen top = game.stack.top(); top.handle(command); top.update(); show(game, buffer); } }}Weitere Aufgaben
- Heldenerschaffung. Füge zwischen dem Titelbildschirm und der Karte einen Bildschirm ein, der nach einem Namen fragt. Beide Übergänge benutzen replace: Vom Helden gibt es keinen Weg zurück.
- Pause.
pöffnet von jedem Bildschirm aus einen undurchsichtigen Bildschirm „Pause“. Prüfe, dass der Wolf nicht beißt, solange das Spiel pausiert. - Wächter für den Stapel. Lass
popablehnen, wenn nur noch ein Bildschirm auf dem Stapel liegt, und gib den Grund aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Polymorphie · Abstrakte Klassen · Listen · Stack, Queue und verkettete Liste
- Aufgaben zum Thema: Die Chroniken von Arkanum