Zum Inhalt
wannacode

RPG 1: die Helden

Kurz gesagt: Vererbung in einem Spiel: Die fünf Heldenvölker erweitern eine gemeinsame 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 steht sie nicht in Character: Jedes Volk schreibt sein eigenes useAbility(). JavaScript kennt keine abstrakten Klassen oder Methoden. Vergisst ein Volk seine Fähigkeit, bricht das Programm erst ab, wenn die Fähigkeit aufgerufen wird: TypeError: hero.useAbility is not a function.

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 beginnen mit _. Das ist eine Konvention: „nur für die Klasse und ihre Unterklassen“. JavaScript hat auch echte private Felder mit #, aber nicht einmal die Unterklassen sehen sie, deshalb nutzen die gemeinsamen Felder hier _. Von außen werden sie mit get gelesen: hero.health liest das Leben, und hero.health = 1000 ändert nichts, weil es kein set gibt. In einer Datei ohne import tut die Zuweisung stillschweigend nichts. In einer Datei mit import (einem Modul, wie die Programme mit ask) bricht sie mit TypeError: Cannot set property health of #<Character> which has only a getter ab.
  • 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(this._health - amount, 0) verhindert, dass das Leben unter 0 fällt.

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 hero hat keinen Typ; sie zeigt auf ein Objekt der Klasse Paladin. hero.useAbility() ruft die Methode des Paladins auf, und showInfo und takeDamage kommen aus Character.

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.

In JavaScript liefert / auch Brüche: 24 / 5 ist 4,8. Math.trunc schneidet den Nachkommateil ab, wie die Ganzzahldivision in Java. Das Ausweichen der Amazone steckt in #dodging: einem echten privaten Feld, das nur ihr gehört und das nicht einmal Character sehen kann. Eine Methode überschreibst du, indem du sie in der Unterklasse einfach noch einmal schreibst: Davor ist nichts nötig.

Probier es aus

Wie viel Leben bleibt?

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

class Amazon extends Character {
#dodging = false;
constructor(name) {
super(name, 100, 18, "Elf");
}
takeDamage(amount) {
if (this.#dodging) {
amount = Math.trunc(amount / 2);
this.#dodging = false;
}
super.takeDamage(amount);
}
useAbility() {
this.#dodging = true;
return `${this._name} macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert`;
}
}
const lira = new Amazon("Lira");
lira.useAbility();
lira.takeDamage(30);
lira.takeDamage(30);
console.log(lira.health);
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, 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.

const party = [
new Paladin("Ardan"),
new Amazon("Lira"),
new Barbarian("Grok"),
new Necromancer("Torin"),
new Witch("Morena"),
];
for (const hero of party) {
console.log(hero.useAbility());
hero.takeDamage(30);
hero.showInfo();
}
Schritt für Schritt, in Worten
  1. Es gibt ein Array party, und 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?

JavaScript kann ein Volk nicht zwingen, eine Fähigkeit zu haben. Aber der Paladin muss alles Gemeinsame von Character übernehmen. Was gehört in die Lücke?
class Paladin  Character {
  constructor(name) {
    super(name, 120, 14, "Mensch");
  }

  useAbility() {
    this._health += 20;
    return `${this._name} heilt sich: +20 Leben`;
  }
}

Ü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 Funktion showAll(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 Funktion strongest(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 gibt es in Character kein useAbility()?
2. Das Array party enthält Objekte aus fünf Klassen. 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 ·