Zum Inhalt
wannacode

RPG 1: die Helden

Kurz gesagt: Vererbung in einem Spiel: Die fünf Heldenvölker erweitern eine abstrakte Klasse Character, jedes mit eigener Fähigkeit, und eine Schleife ruft sie alle auf.

Problem

Fünf Völker, eine Gruppe

In der Welt von Arkanum kämpfen fünf Völker um ihre Länder. Jedes hat seine eigenen Helden:

Volk Held Was sie auszeichnet
Menschen Paladin mittlerer Schaden, heilt sich
Elfen Amazone schnell, trifft härter als Menschen, weicht Treffern aus
Orks Barbar hoher Schaden, erleidet weniger Schaden, gerät in Rage
Zwerge Nekromant geringer Schaden, zieht Kraft aus den Toten
Blutelfen Hexe mittlerer Schaden, erholt sich mit der Kraft der Elemente

Du willst eine Gruppe aus allen fünf und eine Schleife, die sie anzeigt und in den Kampf schickt. Wenn jedes Volk eine eigene Klasse ohne Gemeinsamkeiten ist, hast du fünf Arrays, fünf Schleifen und fünf Kopien desselben Codes für Leben und Schaden. Und ein sechstes Volk will eine sechste Kopie.

Die Idee

Das Gemeinsame steht in Character

Jeder Held hat einen Namen, Leben, Kraft und ein Volk. Jeder Held erleidet Schaden, kann lebendig oder tot sein und kann zeigen, wer er ist. Das ist ihr Gemeinsames, und es kommt in die Elternklasse:

abstract class Character {
protected String name;
protected int health;
protected int power;
protected String race;
public Character(String name, int health, int power, String race) {
this.name = name;
this.health = health;
this.power = power;
this.race = race;
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
public int getPower() {
return power;
}
public void takeDamage(int amount) {
health = Math.max(health - amount, 0);
}
public boolean isAlive() {
return health > 0;
}
public void showInfo() {
System.out.println(name + ", " + race + ": Leben " + health + ", Kraft " + power);
}
public abstract String useAbility();
}

Nur die Fähigkeit ist verschieden. Deshalb ist useAbility() abstrakt: Jedes Volk muss sie schreiben. Und Character ist eine abstrakte Klasse, weil es keinen Helden gibt, der „nur ein Character“ ist.

Die Fähigkeit gibt einen Text darüber zurück, was passiert ist, statt ihn auszugeben. So entscheidet das Spiel, wer was auf dem Bildschirm sieht, nicht der Held.

Genauer, für Neugierige
  • Die Felder sind protected: Die Unterklassen ändern sie, und von außen lassen sie sich nur lesen, mit Gettern.
  • Die alte Übung hatte für jedes Feld einen Getter und einen Setter. Hier gibt es keinen Weg, das Leben von außen zu ändern. Es ändert sich nur durch takeDamage und die Fähigkeiten, also kann niemand einen Helden mit einer Zeile unsterblich machen.
  • Math.max(health - amount, 0) verhindert, dass das Leben unter 0 fällt.
  • Der Name Character ist in Java schon vergeben: durch die Klasse java.lang.Character für Zeichen. Deine eigene Klasse mit demselben Namen in deiner Datei verdeckt sie. Das funktioniert, aber wenn du auch die Standardklasse brauchst, schreibe ihren vollen Namen: java.lang.Character.isDigit(c).

Sieh es dir an

Der erste Held

Im Konstruktor sagt der Paladin, zu welchem Volk er gehört und mit wie viel Leben und Kraft er beginnt. Alles andere übernimmt er von Character. Er schreibt nur seine Fähigkeit:

class Paladin extends Character {
public Paladin(String name) {
super(name, 120, 14, "Mensch");
}
@Override
public String useAbility() {
health += 20;
return name + " heilt sich: +20 Leben";
}
}
Character hero = new Paladin("Ardan");
hero.showInfo();
hero.takeDamage(30);
hero.showInfo();
System.out.println(hero.useAbility());
hero.showInfo();
Ausgabe
Ardan, Mensch: Leben 120, Kraft 14
Ardan, Mensch: Leben 90, Kraft 14
Ardan heilt sich: +20 Leben
Ardan, Mensch: Leben 110, Kraft 14

Die Variable hat den Typ Character, und das Objekt ist ein Paladin. hero.useAbility() ruft die Methode des Paladins auf.

Die Idee

Jedes Volk auf seine Art

Der Barbar erleidet ein Fünftel weniger Schaden. Er überschreibt takeDamage und ruft die Version der Elternklasse mit der kleineren Zahl auf:

class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "Ork");
}
@Override
public void takeDamage(int amount) {
super.takeDamage(amount - amount / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " gerät in Rage: Kraft " + power;
}
}

Die Amazone merkt sich, ob sie zum Ausweichen bereit ist. Wenn ja, wird der nächste Treffer halbiert, und das Ausweichen ist verbraucht:

class Amazon extends Character {
private boolean dodging;
public Amazon(String name) {
super(name, 100, 18, "Elf");
}
@Override
public void takeDamage(int amount) {
if (dodging) {
amount = amount / 2;
dodging = false;
}
super.takeDamage(amount);
}
@Override
public String useAbility() {
dodging = true;
return name + " macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert";
}
}

Beide nutzen super.takeDamage: Die Prüfung, dass das Leben nicht unter 0 fällt, bleibt an einer einzigen Stelle.

Probier es aus

Wie viel Leben bleibt?

Was gibt das Programm aus? Character stammt von der Karte oben.

class Amazon extends Character {
private boolean dodging;
public Amazon(String name) {
super(name, 100, 18, "Elf");
}
@Override
public void takeDamage(int amount) {
if (dodging) {
amount = amount / 2;
dodging = false;
}
super.takeDamage(amount);
}
@Override
public String useAbility() {
dodging = true;
return name + " macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert";
}
}
Character lira = new Amazon("Lira");
lira.useAbility();
lira.takeDamage(30);
lira.takeDamage(30);
System.out.println(lira.getHealth());
Antwort

Sieh es dir an

Die Gruppe

Der Nekromant und die Hexe schreiben nur einen Konstruktor und eine Fähigkeit:

class Necromancer extends Character {
public Necromancer(String name) {
super(name, 90, 10, "Zwerg");
}
@Override
public String useAbility() {
power += 3;
return name + " zieht Kraft aus den Toten: +3 Kraft";
}
}
class Witch extends Character {
public Witch(String name) {
super(name, 95, 16, "Blutelf");
}
@Override
public String useAbility() {
health += 15;
return name + " ruft die Elemente an: +15 Leben";
}
}

Jetzt kommen alle fünf in ein Array vom Typ Character, und eine Schleife lässt sie ihre Fähigkeit einsetzen und einen Treffer von 30 einstecken:

Character[] party = {
new Paladin("Ardan"),
new Amazon("Lira"),
new Barbarian("Grok"),
new Necromancer("Torin"),
new Witch("Morena"),
};
for (Character hero : party) {
System.out.println(hero.useAbility());
hero.takeDamage(30);
hero.showInfo();
}
Ausgabe
Ardan heilt sich: +20 Leben
Ardan, Mensch: Leben 110, Kraft 14
Lira macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert
Lira, Elf: Leben 85, Kraft 18
Grok gerät in Rage: Kraft 30
Grok, Ork: Leben 86, Kraft 30
Torin zieht Kraft aus den Toten: +3 Kraft
Torin, Zwerg: Leben 60, Kraft 13
Morena ruft die Elemente an: +15 Leben
Morena, Blutelf: Leben 80, Kraft 16

Die Schleife weiß nicht, wer zu welchem Volk gehört. Jeder Held entscheidet selbst, was seine Fähigkeit bewirkt und wie viel Schaden er erleidet. Die Amazone verliert 15, der Barbar 24, die anderen 30. Ein sechstes Volk kommt in dasselbe Array, ohne dass sich die Schleife ändert.

Character[] party = {
new Paladin("Ardan"),
new Amazon("Lira"),
new Barbarian("Grok"),
new Necromancer("Torin"),
new Witch("Morena"),
};
for (Character hero : party) {
System.out.println(hero.useAbility());
hero.takeDamage(30);
hero.showInfo();
}
Schritt für Schritt, in Worten
  1. Das Array hat den Typ Character, aber es enthält 5 Objekte aus 5 verschiedenen Klassen.
  2. hero zeigt auf Ardan, ein Objekt der Klasse Paladin. Das useAbility() von Paladin läuft. takeDamage ist die gemeinsame aus Character: 140 − 30 = 110.
  3. hero zeigt auf Lira, ein Objekt der Klasse Amazon. Das useAbility() von Amazon läuft. Amazon überschreibt takeDamage: Vom Treffer mit 30 erleidet er nur 15, und es bleiben 85.
  4. hero zeigt auf Grok, ein Objekt der Klasse Barbarian. Das useAbility() von Barbarian läuft. Barbarian überschreibt takeDamage: Vom Treffer mit 30 erleidet er nur 24, und es bleiben 86.
  5. hero zeigt auf Torin, ein Objekt der Klasse Necromancer. Das useAbility() von Necromancer läuft. takeDamage ist die gemeinsame aus Character: 90 − 30 = 60.
  6. hero zeigt auf Morena, ein Objekt der Klasse Witch. Das useAbility() von Witch läuft. takeDamage ist die gemeinsame aus Character: 110 − 30 = 80.
  7. Es gibt eine Schleife, und sie kennt die Völker nicht. Welche Methode läuft, entscheidet das Objekt, auf das hero zeigt.

Probier es aus

Finde den Fehler

Das Programm bricht schon beim ersten Treffer gegen den Barbaren ab. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Was fehlt?

Was gehört in die Lücke, damit jedes Volk eine Fähigkeit haben muss?
abstract class Character {
    protected String name;
    protected int health;
    protected int power;
    protected String race;

    public Character(String name, int health, int power, String race) {
        this.name = name;
        this.health = health;
        this.power = power;
        this.race = race;
    }

    public String getName() {
        return name;
    }

    public int getHealth() {
        return health;
    }

    public int getPower() {
        return power;
    }

    public void takeDamage(int amount) {
        health = Math.max(health - amount, 0);
    }

    public boolean isAlive() {
        return health > 0;
    }

    public void showInfo() {
        System.out.println(name + ", " + race + ": Leben " + health + ", Kraft " + power);
    }

    public  String useAbility();
}

Übung

Mehr Helden

Löse diese Aufgaben in deinem Editor. Beginne mit den Klassen aus der Lektion.

1. Ein sechster Held. Füge einen Druiden aus dem Waldvolk hinzu: 105 Leben, 12 Kraft. Seine Fähigkeit gibt +8 Leben und +2 Kraft. Rühre Character nicht an.

Ausgabe
Eli, Waldvolk: Leben 105, Kraft 12
Eli verbindet sich mit dem Wald: +8 Leben, +2 Kraft
Eli, Waldvolk: Leben 113, Kraft 14

2. Die Liste der Gruppe. Schreibe eine Methode showAll(Character[] party), die die Helden mit Nummern anzeigt und am Ende ihr gesamtes Leben. Lass sie für die Gruppe aus der Lektion laufen.

Ausgabe
1. Ardan, Mensch: Leben 120, Kraft 14
2. Lira, Elf: Leben 100, Kraft 18
3. Grok, Ork: Leben 110, Kraft 20
4. Torin, Zwerg: Leben 90, Kraft 10
5. Morena, Blutelf: Leben 95, Kraft 16
Gesamtleben: 515

3. Wer ist der Stärkste? Jedes Mitglied der Gruppe setzt seine Fähigkeit einmal ein. Schreibe eine Methode Character strongest(Character[] party), die den Helden mit der meisten Kraft zurückgibt.

Ausgabe
Stärkster: Grok mit Kraft 30

Weitere Übungen

  • Eine Gruppe aus der Eingabe. Lies fünf Zeilen der Form „Volk Name“, zum Beispiel „paladin Ardan“, und erzeuge die Helden in einem Array. Bei einem unbekannten Volk sagst du das und liest die nächste Zeile.
  • Rage mit Maß. Rage um Rage macht den Barbaren endlos stark. Lass die Fähigkeit die Kraft nur beim ersten Mal erhöhen und sage danach, dass er schon in Rage ist.
  • Wer ist der Zäheste? Wie Aufgabe 3, aber nach Leben, nachdem jeder einen Treffer von 25 eingesteckt hat.

Test

Teste dich

1. Warum ist Character abstrakt?
2. Das Array ist Character[]. Welche takeDamage wird darin für den Barbaren aufgerufen?
3. Die Amazone macht sich zum Ausweichen bereit, dann wird sie mit 40 getroffen und danach noch einmal mit 40. Wie viel Leben verliert sie?
4. Barbarian überschreibt takeDamage. Wie ruft er die Version aus Character auf?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·