Zarábaj až 6 000 € mesačne! Akreditované rekvalifikačné kurzy od 0 €. Viac informácií.
Hľadáme nové posily do ITnetwork tímu. Pozri sa na voľné pozície a pridaj sa k najagilnejšej firme na trhu - Viac informácií.

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.

Mág - Objektovo orientované programovanie v Jave

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:

    @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;
        }
    }
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);
    }
}
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;
    }

}
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();
        }
    }
}

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:

    private void render() {
        System.out.println("-------------- Arena -------------- \n");
        System.out.println("Warriors: \n");
        printWarrior(warrior1);
        System.out.println();
        printWarrior(warrior2);
        System.out.println();
    }
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;
    }

}
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;
        }
    }
}

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é 1x (7.17 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Predchádzajúci článok
Dedičnosť a polymorfizmus
Všetky články v sekcii
Objektovo orientované programovanie v Jave
Preskočiť článok
(neodporúčame)
Riešené úlohy k 5.-8. lekciu OOP v Jave
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
2 hlasov
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity