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();}abstract class Character{ public string Name { get; protected set; } public int Health { get; protected set; } public int Power { get; protected set; } public string Race { get; protected set; }
public Character(string name, int health, int power, string race) { Name = name; Health = health; Power = power; Race = race; }
public virtual void TakeDamage(int amount) { Health = Math.Max(Health - amount, 0); }
public bool IsAlive() { return Health > 0; }
public void ShowInfo() { Console.WriteLine(Name + ", " + Race + ": Leben " + Health + ", Kraft " + Power); }
public abstract string UseAbility();}from abc import ABC, abstractmethod
class Character(ABC): def __init__(self, name, health, power, race): self._name = name self._health = health self._power = power self._race = race
@property def name(self): return self._name
@property def health(self): return self._health
@property def power(self): return self._power
def take_damage(self, amount): self._health = max(self._health - amount, 0)
def is_alive(self): return self._health > 0
def show_info(self): print(f"{self._name}, {self._race}: Leben {self._health}, Kraft {self._power}")
@abstractmethod def use_ability(self): passclass Character { constructor(name, health, power, race) { this._name = name; this._health = health; this._power = power; this._race = race; }
get name() { return this._name; }
get health() { return this._health; }
get power() { return this._power; }
takeDamage(amount) { this._health = Math.max(this._health - amount, 0); }
isAlive() { return this._health > 0; }
showInfo() { console.log(`${this._name}, ${this._race}: Leben ${this._health}, Kraft ${this._power}`); }}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 mitgetgelesen:hero.healthliest das Leben, undhero.health = 1000ändert nichts, weil es keinsetgibt. In einer Datei ohneimporttut die Zuweisung stillschweigend nichts. In einer Datei mitimport(einem Modul, wie die Programme mitask) bricht sie mitTypeError: Cannot set property health of #<Character> which has only a getterab.
- 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
takeDamageund 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();Ardan, Mensch: Leben 120, Kraft 14Ardan, Mensch: Leben 90, Kraft 14Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14class Paladin : Character{ public Paladin(string name) : base(name, 120, 14, "Mensch") { }
public override string UseAbility() { Health += 20; return Name + " heilt sich: +20 Leben"; }}
Character hero = new Paladin("Ardan");hero.ShowInfo();hero.TakeDamage(30);hero.ShowInfo();Console.WriteLine(hero.UseAbility());hero.ShowInfo();Ardan, Mensch: Leben 120, Kraft 14Ardan, Mensch: Leben 90, Kraft 14Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14class Paladin(Character): def __init__(self, name): super().__init__(name, 120, 14, "Mensch")
def use_ability(self): self._health += 20 return f"{self._name} heilt sich: +20 Leben"
hero = Paladin("Ardan")hero.show_info()hero.take_damage(30)hero.show_info()print(hero.use_ability())hero.show_info()Ardan, Mensch: Leben 120, Kraft 14Ardan, Mensch: Leben 90, Kraft 14Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14class Paladin extends Character { constructor(name) { super(name, 120, 14, "Mensch"); }
useAbility() { this._health += 20; return `${this._name} heilt sich: +20 Leben`; }}
const hero = new Paladin("Ardan");hero.showInfo();hero.takeDamage(30);hero.showInfo();console.log(hero.useAbility());hero.showInfo();Ardan, Mensch: Leben 120, Kraft 14Ardan, Mensch: Leben 90, Kraft 14Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14Die 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; }}class Barbarian : Character{ public Barbarian(string name) : base(name, 110, 20, "Ork") { }
public override void TakeDamage(int amount) { base.TakeDamage(amount - amount / 5); }
public override string UseAbility() { Power += Power / 2; return Name + " gerät in Rage: Kraft " + Power; }}class Barbarian(Character): def __init__(self, name): super().__init__(name, 110, 20, "Ork")
def take_damage(self, amount): super().take_damage(amount - amount // 5)
def use_ability(self): self._power += self._power // 2 return f"{self._name} gerät in Rage: Kraft {self._power}"class Barbarian extends Character { constructor(name) { super(name, 110, 20, "Ork"); }
takeDamage(amount) { super.takeDamage(amount - Math.trunc(amount / 5)); }
useAbility() { this._power += Math.trunc(this._power / 2); return `${this._name} gerät in Rage: Kraft ${this._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"; }}class Amazon : Character{ private bool dodging;
public Amazon(string name) : base(name, 100, 18, "Elf") { }
public override void TakeDamage(int amount) { if (dodging) { amount = amount / 2; dodging = false; } base.TakeDamage(amount); }
public override string UseAbility() { dodging = true; return Name + " macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert"; }}class Amazon(Character): def __init__(self, name): super().__init__(name, 100, 18, "Elf") self._dodging = False
def take_damage(self, amount): if self._dodging: amount = amount // 2 self._dodging = False super().take_damage(amount)
def use_ability(self): self._dodging = True return f"{self._name} macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert"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`; }}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);Lira beginnt mit 100. Ihre Fähigkeit macht sie zum Ausweichen bereit. Der erste Treffer ist 30 / 2 = 15, und das Ausweichen ist verbraucht: 85. Der zweite zählt voll: 85 − 30 = 55.
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 Necromancer : Character{ public Necromancer(string name) : base(name, 90, 10, "Zwerg") { }
public override string UseAbility() { Power += 3; return Name + " zieht Kraft aus den Toten: +3 Kraft"; }}class Necromancer(Character): def __init__(self, name): super().__init__(name, 90, 10, "Zwerg")
def use_ability(self): self._power += 3 return f"{self._name} zieht Kraft aus den Toten: +3 Kraft"class Necromancer extends Character { constructor(name) { super(name, 90, 10, "Zwerg"); }
useAbility() { this._power += 3; return `${this._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"; }}class Witch : Character{ public Witch(string name) : base(name, 95, 16, "Blutelf") { }
public override string UseAbility() { Health += 15; return Name + " ruft die Elemente an: +15 Leben"; }}class Witch(Character): def __init__(self, name): super().__init__(name, 95, 16, "Blutelf")
def use_ability(self): self._health += 15 return f"{self._name} ruft die Elemente an: +15 Leben"class Witch extends Character { constructor(name) { super(name, 95, 16, "Blutelf"); }
useAbility() { this._health += 15; return `${this._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();}Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14Lira macht sich zum Ausweichen bereit: der nächste Treffer wird halbiertLira, Elf: Leben 85, Kraft 18Grok gerät in Rage: Kraft 30Grok, Ork: Leben 86, Kraft 30Torin zieht Kraft aus den Toten: +3 KraftTorin, Zwerg: Leben 60, Kraft 13Morena ruft die Elemente an: +15 LebenMorena, Blutelf: Leben 80, Kraft 16Character[] party ={ new Paladin("Ardan"), new Amazon("Lira"), new Barbarian("Grok"), new Necromancer("Torin"), new Witch("Morena"),};foreach (Character hero in party){ Console.WriteLine(hero.UseAbility()); hero.TakeDamage(30); hero.ShowInfo();}Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14Lira macht sich zum Ausweichen bereit: der nächste Treffer wird halbiertLira, Elf: Leben 85, Kraft 18Grok gerät in Rage: Kraft 30Grok, Ork: Leben 86, Kraft 30Torin zieht Kraft aus den Toten: +3 KraftTorin, Zwerg: Leben 60, Kraft 13Morena ruft die Elemente an: +15 LebenMorena, Blutelf: Leben 80, Kraft 16party = [ Paladin("Ardan"), Amazon("Lira"), Barbarian("Grok"), Necromancer("Torin"), Witch("Morena"),]for hero in party: print(hero.use_ability()) hero.take_damage(30) hero.show_info()Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14Lira macht sich zum Ausweichen bereit: der nächste Treffer wird halbiertLira, Elf: Leben 85, Kraft 18Grok gerät in Rage: Kraft 30Grok, Ork: Leben 86, Kraft 30Torin zieht Kraft aus den Toten: +3 KraftTorin, Zwerg: Leben 60, Kraft 13Morena ruft die Elemente an: +15 LebenMorena, Blutelf: Leben 80, Kraft 16const 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();}Ardan heilt sich: +20 LebenArdan, Mensch: Leben 110, Kraft 14Lira macht sich zum Ausweichen bereit: der nächste Treffer wird halbiertLira, Elf: Leben 85, Kraft 18Grok gerät in Rage: Kraft 30Grok, Ork: Leben 86, Kraft 30Torin zieht Kraft aus den Toten: +3 KraftTorin, Zwerg: Leben 60, Kraft 13Morena ruft die Elemente an: +15 LebenMorena, Blutelf: Leben 80, Kraft 16Die 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
- Es gibt ein Array party, und es enthält 5 Objekte aus 5 verschiedenen Klassen.
- hero zeigt auf Ardan, ein Objekt der Klasse Paladin. Das useAbility() von Paladin läuft. takeDamage ist die gemeinsame aus Character: 140 − 30 = 110.
- 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.
- 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.
- hero zeigt auf Torin, ein Objekt der Klasse Necromancer. Das useAbility() von Necromancer läuft. takeDamage ist die gemeinsame aus Character: 90 − 30 = 60.
- hero zeigt auf Morena, ein Objekt der Klasse Witch. Das useAbility() von Witch läuft. takeDamage ist die gemeinsame aus Character: 110 − 30 = 80.
- 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.
this. zeigt auf den Barbaren selbst, also ruft die Methode sich selbst auf, nicht die der Elternklasse. Das ist eine Rekursion ohne Basisfall: takeDamage ruft takeDamage auf, das es wieder aufruft ... bis der Aufrufstapel voll ist und das Programm mit RangeError: Maximum call stack size exceeded abbricht. Du brauchst super.takeDamage(amount - Math.trunc(amount / 5));.Probier es aus
Was fehlt?
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`;
}
}extends: Paladin erbt von Character. So hat er den Namen, das Leben, takeDamage und showInfo, und super(...) im Konstruktor ruft den Konstruktor von Character auf.Ü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.
Eli, Waldvolk: Leben 105, Kraft 12Eli verbindet sich mit dem Wald: +8 Leben, +2 KraftEli, Waldvolk: Leben 113, Kraft 142. 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.
1. Ardan, Mensch: Leben 120, Kraft 142. Lira, Elf: Leben 100, Kraft 183. Grok, Ork: Leben 110, Kraft 204. Torin, Zwerg: Leben 90, Kraft 105. Morena, Blutelf: Leben 95, Kraft 16Gesamtleben: 5153. 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.
Stärkster: Grok mit Kraft 30Weitere Ü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
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Instanzmethoden · Kapselung · Vererbung · Polymorphie
- Aufgaben zum Thema: Wir bauen ein Spiel