8. diel - Aréna s mágom (dedičnosť a polymorfizmus)
V minulej lekcii, Dedičnosť a polymorfizmus, sme si vysvetlili dedičnosť a polymorfizmus.
Dnes máme sľúbené, že si dedičnosť a polymorfizmus vyskúšame v praxi. Bude to opäť na našej aréne, kde z bojovníka budeme dediť mága. Tento tutoriál už patrí k tým náročnejším a bude tomu tak aj u ďalších. Preto si priebežne precvičujte prácu s objektmi, skúšajte si naše cvičenia a tiež vymýšľajte nejaké svoje aplikácie, aby ste si osvojili základné veci. To, že je tu prítomný celý seriál, neznamená, že ho celý naraz prečítate a pochopíte Snažte sa programovať priebežne.
Než začneme niečo písať, zhodneme sa na tom, čo by mal mág vedieť.
Mág bude fungovať rovnako ako bojovník. Okrem života však bude mať aj
manu. Spočiatku bude mana plná. V prípade plnej many môže
mág vykonať magický útok, ktorý bude mať pravdepodobne
vyšší damage (poškodenie) než útok normálny (ale samozrejme záleží na
tom, ako si ho nastavíme). Tento útok manu vyčerpá na 0
.
Každé kolo sa bude mana zvyšovať o 10
a mág bude podnikať len
bežný útok. Akonáhle sa mana úplne doplní, opäť bude môcť magický
útok použiť. Mana bude zobrazená grafickým ukazovateľom, rovnako ako
život.
Do pôvodného projektu ArenaFight
vytvoríme teda triedu
Mage.java
, zdedíme ju z triedy Warrior
a pridáme jej
atribúty, ktoré chceme oproti bojovníkovi navyše. Bude teda vyzerať takto
(opäť si ju okomentujte):
public class Mage extends Warrior { private int mana; private int maxMana; private int magicDamage; }
V mágovi zatiaľ nemáme prístup ku všetkým premenným, pretože sú v
bojovníkovi nastavené ako privátne. Musíme triedu Warrior
mierne upraviť. Zmeníme modifikátory private
u atribútov na
protected
. Budeme potrebovať len atribúty die
a
name
, ale pokojne nastavíme ako protected
všetky
atribúty charakteru, pretože sa v budúcnosti môžu hodiť, keby sme sa
rozhodli oddediť ďalšie typy bojovníkov. Naopak atribút
message
nie je vhodné nastavovať ako protected
,
pretože nesúvisí s bojovníkom, ale s nejakou vnútornou logikou triedy.
Trieda teda bude vyzerať nejako takto:
protected String name; protected int health; protected int maxHealth; protected int damage; protected int defense; protected RollingDie die; private String message; // ...
Prejdime ku konštruktoru.
Konštruktor potomka
Java nededí konštruktory! Je to pravdepodobne z toho dôvodu, že predpokladá, že potomek bude mať navyše nejaké atribúty a pôvodný konštruktor by u neho bol na škodu. To je aj náš prípad, pretože konštruktor mága bude brať oproti tomu z bojovníka navyše 2 parametre (mana a magický útok).
Definujeme si teda konštruktor v potomkovi Mage
, ktorý berie
parametre potrebné na vytvorenie bojovníka a niekoľko parametrov navyše pre
mága.
U potomkov je nutné vždy volať konštruktor predka. Je to z toho dôvodu, že bez volania konštruktora nemusí byť inštancia správne inicializovaná. Konštruktor predka nevoláme len v prípade, že žiadny nemá. Náš konštruktor musí mať samozrejme všetky parametre potrebné pre predka plus tie nové, čo má navyše potomek. Niektoré potom predáme predkovi a niektoré si spracujeme sami. Konštruktor predka sa vykoná pred naším konštruktorom.
V Jave existuje kľúčové slovo super
, ktoré je podobné nám
už známemu this
. Na rozdiel od kľúčového slova
this
, ktoré odkazuje na konkrétnu inštanciu triedy,
super
odkazuje na predka. My teda môžeme
zavolať konštruktor predka s danými parametrami a potom vykonať navyše
inicializáciu pre mága.
Konstruktor mága bude teda vyzerať takto:
public Mage(String name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage) { super(name, health, damage, defense, die); this.mana = mana; this.maxMana = mana; this.magicDamage = magicDamage; }
Stejne môžeme volať aj iný konštruktor v tej istej triede
(nie predka), len miesto kľúčového slova super
použijeme
this
.
Presuňme sa teraz do súboru ArenaFight.java
a druhého
bojovníka (u nás to je Shadow) zmeňme na mága, napr. takto:
Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45);
Zmenu samozrejme musíme urobiť aj v riadku, kde bojovníka do arény
vkladáme. Všimnite si, že mága ukladáme do premennej typu
Warrior
. Nič nám v tom nebráni, pretože bojovník je jeho
predok. Rovnako tak si môžeme typ premennej zmeniť na Mage
. Keď
aplikáciu teraz spustíme, bude fungovať úplne rovnako ako predtým. Mág
všetko dedí z bojovníka a zatiaľ teda funguje ako bojovník.
Polymorfizmus a prepisovanie metód
Bolo by výhodné, keby objekt Arena
mohol s mágom pracovať
rovnakým spôsobom ako s bojovníkom. My už vieme, že takémuto mechanizmu
hovoríme polymorfizmus. Aréna zavolá na objekte metódu
attack()
so súperom v parametri. Nestará sa o to, či bude útok
vykonávať bojovník alebo mág, bude s nimi pracovať rovnako. U mága si teda
prepíšeme metódu attack()
z predka. Prepíšeme
zdedenú metódu tak, aby útok pracoval s manou, hlavička metódy však
zostane rovnaká.
Keď sme u metód, budeme v Warrior.java
ešte určite
používať metódu setMessage()
, tá je však privátna. Označme
ju ako protected
:
protected void setMessage(String message) {
Pri návrhu bojovníka sme samozrejme mali myslieť na to, že
sa z neho bude dediť a už označiť vhodné atribúty a metódy ako
protected
. V tutoriále k bojovníkovi som vás tým však nechcel
zbytočne zaťažovať, preto musíme modifikátory zmeniť až teraz, keď im
rozumieme
Poďme prepísať metódu bojovníka attack()
v mágovi. Metódu
normálne definujeme v súbore Mage.java
tak, ako sme zvyknutí,
len ju označíme kľúčovým slovom @Override
pre
prepísanie:
@Override public void attack(Warrior enemy) {
Podobne sme prepisovali metódu toString()
u našich objektov,
každý objekt v Jave je totiž oddedený od java.lang.Object
,
ktorý obsahuje niekoľko defaultných (východzích) metód a jedna z nich je
aj metóda toString()
. Pri jej implementácii by sme teda mali
označiť, že sa jedná o prepísanú metódu.
Správanie metódy attack()
nebude nijako zložité. Podľa
hodnoty many buď vykonáme bežný útok alebo útok magický. Hodnotu many
potom buď zvýšime o 10
alebo naopak znížime na 0
v prípade magického útoku:
@Override public void attack(Warrior enemy) { int hit = 0; // Mana isn't full if (mana < maxMana) { mana += 10; if (mana > maxMana) { mana = maxMana; } hit = damage + die.roll(); setMessage(String.format("%s attacks with a hit worth %s hp", name, hit)); } else { // Magic damage hit = magicDamage + die.roll(); setMessage(String.format("%s used magic worth %s hp", name, hit)); mana = 0; } enemy.defend(hit); }
Kód je asi zrozumiteľný. Všimnite si obmedzenia many premennou
maxMana
. Môže sa nám totiž stať, že túto hodnotu
presiahneme, keď ju zvyšujeme o 10
. Keď sa nad kódom
zamyslíme, útok vyššie v podstate vykonáva pôvodná metóda
attack()
. Určite by bolo prínosné zavolať podobu metódy na
predkovi miesto toho, aby sme správanie opisovali. K tomu opäť použijeme
kľúčové slovo super
:
{JAVA_OOP} public class Mage extends Warrior { private int mana; private int maxMana; private int magicDamage; public Mage(String name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage) { super(name, health, damage, defense, die); this.mana = mana; this.maxMana = mana; this.magicDamage = magicDamage; } @Override public void attack(Warrior enemy) { // Mana isn't full if (mana < maxMana) { mana += 10; if (mana > maxMana) { mana = maxMana; } super.attack(enemy); } else { // Magic damage int hit = magicDamage + die.roll(); setMessage(String.format("%s used magic worth %s hp", name, hit)); enemy.defend(hit); mana = 0; } } } {/JAVA_OOP}
{JAVA_OOP} import java.util.Random; public class RollingDie { private Random random; private int sidesCount; public RollingDie() { sidesCount = 6; random = new Random(); } public RollingDie(int sidesCount) { this.sidesCount = sidesCount; random = new Random(); } public int getSidesCount() { return sidesCount; } public int roll() { return random.nextInt(sidesCount) + 1; } @Override public String toString() { return String.format("Rolling die with %s sides", sidesCount); } } {/JAVA_OOP}
{JAVA_OOP} public class Warrior { protected String name; protected int health; protected int maxHealth; protected int damage; protected int defense; protected RollingDie die; private String message; public Warrior(String name, int health, int damage, int defense, RollingDie die) { this.name = name; this.health = health; this.maxHealth = health; this.damage = damage; this.defense = defense; this.die = die; } public boolean isAlive() { return (health > 0); } public String healthBar() { String healthBar = "["; int total = 20; double count = Math.round(((double)health / maxHealth) * total); if ((count == 0) && (isAlive())) { count = 1; } for (int i = 0; i < count; i++) { healthBar += "#"; } for (int i = 0; i < total - count; i++) { healthBar += " "; } healthBar += "]"; return healthBar; } public void attack(Warrior enemy) { int hit = damage + die.roll(); setMessage(String.format("%s attacks with a hit worth %s hp", name, hit)); enemy.defend(hit); } public void defend(int hit) { int injury = hit - (defense + die.roll()); if (injury > 0) { health -= injury; message = String.format("%s defended against the attack but still lost %s hp", name, injury); if (health <= 0) { health = 0; message += " and died"; } } else { message = String.format("%s blocked the hit", name); } setMessage(message); } protected void setMessage(String message) { this.message = message; } public String getLastMessage() { return message; } @Override public String toString() { return name; } } {/JAVA_OOP}
{JAVA_OOP} public class Arena { private Warrior warrior1; private Warrior warrior2; private RollingDie die; public Arena(Warrior warrior1, Warrior warrior2, RollingDie die) { this.warrior1 = warrior1; this.warrior2 = warrior2; this.die = die; } private void render() { System.out.println("-------------- Arena -------------- \n"); System.out.println("Warriors health: \n"); System.out.printf("%s %s%n", warrior1, warrior1.healthBar()); System.out.printf("%s %s%n", warrior2, warrior2.healthBar()); } private void printMessage(String message) { System.out.println(message); try { Thread.sleep(500); } catch (InterruptedException ex) { System.err.println("Unable to put the thread to sleep"); } } public void fight() { // The original order Warrior warrior1 = this.warrior1; Warrior warrior2 = this.warrior2; System.out.println("Welcome to the Arena!"); System.out.printf("Today %s will battle against %s! %n", warrior1, warrior2); // swapping the warriors boolean warrior2Starts = (die.roll() <= die.getSidesCount() / 2); if (warrior2Starts) { warrior1 = this.warrior2; warrior2 = this.warrior1; } System.out.printf("%s goes first!%nLet the battle begin...%n", warrior1); // fight loop while (warrior1.isAlive() && warrior2.isAlive()) { warrior1.attack(warrior2); render(); printMessage(warrior1.getLastMessage()); // attack message printMessage(warrior2.getLastMessage()); // defense message if (warrior2.isAlive()) { warrior2.attack(warrior1); render(); printMessage(warrior2.getLastMessage()); // attack message printMessage(warrior1.getLastMessage()); // defense message } System.out.println(); } } } {/JAVA_OOP}
{JAVA_OOP} {JAVA_MAIN_BLOCK} // creating objects RollingDie die = new RollingDie(10); Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die); Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45); Arena arena = new Arena(zalgoren, gandalf, die); // fight arena.fight(); {/JAVA_MAIN_BLOCK} {/JAVA_OOP}
Opäť vidíme, ako môžeme znovupoužívať kód. S dedičnosťou je spojených naozaj mnoho techník, ako si ušetriť prácu. V našom prípade to ušetrí niekoľko riadkov, ale u väčšieho projektu by to mohlo mať obrovský význam.
Aplikácia teraz funguje tak, ako má:
Konzolová aplikácia
-------------- Arena --------------
Warriors health:
Zalgoren [############# ]
Gandalf [################# ]
Gandalf used magic worth 52 hp off
Zalgoren defended against the attack but still lost 36 hp
Aréna nás však neinformuje o maně mága, poďme to napraviť. Pridáme
mágovi verejnú metódu manaBar()
, ktorá bude podobne ako u
života vracať textový reťazec s grafickým ukazovateľom many.
Aby sme nemuseli logiku so zložením ukazovateľa písať dvakrát,
upravíme metódu healthBar()
v súbore Warrior.java
.
Pripomeňme si, ako vyzerá:
public String healthBar() { String healthBar = "["; int total = 20; double count = Math.round(((double)health / maxHealth) * total); if ((count == 0) && (isAlive())) { count = 1; } for (int i = 0; i < count; i++) { healthBar += "#"; } for (int i = 0; i < total - count; i++) { healthBar += " "; } healthBar += "]"; return healthBar; }
Vidíme, že nie je okrem premenných health
a
maxHealth
na živote nijako závislá. Metódu premenujeme na
graphicalBar()
a dáme jej 2 parametre: aktuálnu hodnotu a
maximálnu hodnotu. Premenné health
a maxHealth
v
tele metódy potom nahradíme za current
a maximum
.
Modifikátor bude protected
, aby sme metódu mohli v potomkovi
použiť:
protected String graphicalBar(int current, int maximum) { String healthBar = "["; int total = 20; double count = Math.round(((double)current/ maximum) * total); if ((count == 0) && (isAlive())) { count = 1; } for (int i = 0; i < count; i++) { healthBar += "#"; } for (int i = 0; i < total - count; i++) { healthBar += " "; } healthBar += "]"; return healthBar; }
Metódu healthBar()
v súbore Warrior.java
naimplementujeme znovu, bude nám v nej stačiť jediný riadok a to zavolanie
metódy graphicalBar()
s príslušnými parametrami:
public String healthBar() { return graphicalBar(health, maxHealth); }
Určite som mohol v tutoriále s bojovníkom urobiť metódu
graphicalBar()
rovno. Chcel som však, aby sme si ukázali, ako sa
riešia prípady, keď potrebujeme vykonať podobnú funkcionalitu viackrát. S
takouto parametrizáciou sa v praxi budete stretávať často, pretože nikdy
presne nevieme, čo budeme v budúcnosti od nášho programu požadovať.
Teraz môžeme vykresľovať ukazovateľ tak, ako sa nám to hodí. Presuňme
sa do Mage.java
a naimplementujme metódu
manaBar()
:
public String manaBar() { return graphicalBar(mana, maxMana); }
Jednoduché, že? Teraz je mág hotový, zostáva len naučiť arénu
zobrazovať manu v prípade, že je bojovník mág. Presuňme sa teda do súboru
Arena.java
.
Rozpoznanie typu objektu
Keďže sa nám teraz vykreslenie bojovníka skomplikovalo, urobíme si naň
samostatnú metódu printWarrior()
, jej parametrom bude daná
inštancia bojovníka:
private void printWarrior(Warrior warrior) { System.out.println(warrior); System.out.print("Health: "); System.out.println(warrior.healthBar()); }
Teraz poďme reagovať na to, či je bojovník mág. Minule sme si povedali,
že k tomu slúži operátor instanceof
:
private void printWarrior(Warrior warrior) { System.out.println(warrior); System.out.print("Health:"); System.out.println(warrior.healthBar()); if (warrior instanceof Mage) { System.out.print("Mana: "); System.out.println(((Mage)warrior).manaBar()); } }
Bojovníka sme museli na mága pretypovať, aby sme sa k metóde
manaBar()
dostali. Samotná trieda Warrior
ju totiž
nemá. To by sme mali, metódu printWarrior()
budeme volať v
metóde render()
, ktorá bude vyzerať takto:
{JAVA_OOP} public class Arena { private Warrior warrior1; private Warrior warrior2; private RollingDie die; public Arena(Warrior warrior1, Warrior warrior2, RollingDie die) { this.warrior1 = warrior1; this.warrior2 = warrior2; this.die = die; } private void render() { System.out.println("-------------- Arena -------------- \n"); System.out.println("Warriors: \n"); printWarrior(warrior1); System.out.println(); printWarrior(warrior2); System.out.println(); } private void printMessage(String message) { System.out.println(message); try { Thread.sleep(500); } catch (InterruptedException ex) { System.err.println("Unable to put the thread to sleep"); } } private void printWarrior(Warrior warrior) { System.out.println(warrior); System.out.print("Health: "); System.out.println(warrior.healthBar()); if (warrior instanceof Mage) { System.out.print("Mana: "); System.out.println(((Mage)warrior).manaBar()); } } public void fight() { // The original order Warrior warrior1 = this.warrior1; Warrior warrior2 = this.warrior2; System.out.println("Welcome to the Arena!"); System.out.printf("Today %s will battle against %s! %n", warrior1, warrior2); // swapping the warriors boolean warrior2Starts = (die.roll() <= die.getSidesCount() / 2); if (warrior2Starts) { warrior1 = this.warrior2; warrior2 = this.warrior1; } System.out.printf("%s goes first!%nLet the battle begin...%n", warrior1); // fight loop while (warrior1.isAlive() && warrior2.isAlive()) { warrior1.attack(warrior2); render(); printMessage(warrior1.getLastMessage()); // attack message printMessage(warrior2.getLastMessage()); // defense message if (warrior2.isAlive()) { warrior2.attack(warrior1); render(); printMessage(warrior2.getLastMessage()); // attack message printMessage(warrior1.getLastMessage()); // defense message } System.out.println(); } } } {/JAVA_OOP}
{JAVA_OOP} import java.util.Random; public class RollingDie { private Random random; private int sidesCount; public RollingDie() { sidesCount = 6; random = new Random(); } public RollingDie(int sidesCount) { this.sidesCount = sidesCount; random = new Random(); } public int getSidesCount() { return sidesCount; } public int roll() { return random.nextInt(sidesCount) + 1; } @Override public String toString() { return String.format("Rolling die with %s sides", sidesCount); } } {/JAVA_OOP}
{JAVA_OOP} public class Warrior { protected String name; protected int health; protected int maxHealth; protected int damage; protected int defense; protected RollingDie die; private String message; public Warrior(String name, int health, int damage, int defense, RollingDie die) { this.name = name; this.health = health; this.maxHealth = health; this.damage = damage; this.defense = defense; this.die = die; } public boolean isAlive() { return (health > 0); } protected String graphicalBar(int current, int maximum) { String healthBar = "["; int total = 20; double count = Math.round(((double)current/ maximum) * total); if ((count == 0) && (isAlive())) { count = 1; } for (int i = 0; i < count; i++) { healthBar += "#"; } for (int i = 0; i < total - count; i++) { healthBar += " "; } healthBar += "]"; return healthBar; } public String healthBar() { return graphicalBar(health, maxHealth); } public void attack(Warrior enemy) { int hit = damage + die.roll(); setMessage(String.format("%s attacks with a hit worth %s hp", name, hit)); enemy.defend(hit); } public void defend(int hit) { int injury = hit - (defense + die.roll()); if (injury > 0) { health -= injury; message = String.format("%s defended against the attack but still lost %s hp", name, injury); if (health <= 0) { health = 0; message += " and died"; } } else { message = String.format("%s blocked the hit", name); } setMessage(message); } protected void setMessage(String message) { this.message = message; } public String getLastMessage() { return message; } @Override public String toString() { return name; } } {/JAVA_OOP}
{JAVA_OOP} public class Mage extends Warrior { private int mana; private int maxMana; private int magicDamage; public Mage(String name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage) { super(name, health, damage, defense, die); this.mana = mana; this.maxMana = mana; this.magicDamage = magicDamage; } public String manaBar() { return graphicalBar(mana, maxMana); } @Override public void attack(Warrior enemy) { // Mana isn't full if (mana < maxMana) { mana += 10; if (mana > maxMana) { mana = maxMana; } super.attack(enemy); } else { // Magic damage int hit = magicDamage + die.roll(); setMessage(String.format("%s used magic worth %s hp", name, hit)); enemy.defend(hit); mana = 0; } } } {/JAVA_OOP}
{JAVA_OOP} {JAVA_MAIN_BLOCK} // creating objects RollingDie die = new RollingDie(10); Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die); Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45); Arena arena = new Arena(zalgoren, gandalf, die); // fight arena.fight(); {/JAVA_MAIN_BLOCK} {/JAVA_OOP}
Hotovo
Konzolová aplikácia
-------------- Arena --------------
Warriors:
Zalgoren
Health: [########### ]
Gandalf
Health: [######### ]
Mana: [############# ]
Zalgoren attacks with a hit worth 29 hp
Aplikáciu ešte môžeme dodať krajší vzhľad, vložil som ASCIIart
nadpis Arena, ktorý som vytvoril ASCII generátorom. Metódu na
vykreslenie ukazovateľa som upravil tak, aby vykresľovala plný obdĺžnik
miesto #
(ten napíšete pomocou klávesov Alt +
219). Výsledok môže vyzerať takto:
Konzolová aplikácia
__ ____ ____ _ _ __
/__\ ( _ \( ___)( \( ) /__\
/(__)\ ) / )__) ) ( /(__)\
(__)(__)(_)\_)(____)(_)\_)(__)(__)
Warriors:
Zalgoren
Health: ████
Gandalf
Health: ███████
Mana: █
Gandalf used magic worth 48 hp
Zalgoren defended against the attack but still lost 33 hp
Kód máte v prílohe. Ak ste niečomu nerozumeli, skúste si článok prečítať viackrát alebo pomalšie, sú to dôležité praktiky.
V nasledujúcom cvičení, Riešené úlohy k 5.-8. lekciu OOP v Jave, si precvičíme nadobudnuté skúsenosti z predchádzajúcich lekcií.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 9x (7.17 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java