Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

8. diel - Aréna s mágom - Dedičnosť a polymorfizmus v Kotlin

V minulej lekcii, Dedičnosť a polymorfizmus v Kotlin , sme si vysvetlili dedičnosť a polymorfizmus. Dnes máme sľúbené, že si ich vyskúšame v praxi. Bude to opäť na našej aréne, kde z bojovníka oddědíme 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 zažili základné veci. To, že je tu prítomný celý kurz neznamená, že ho celý naraz prečítate a pochopíte :) Snažte sa programovať priebežne.

mág - Objektovo orientované programovanie v Kotlin
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 bude mať však aj manu. Spočiatku bude mana plná. V prípade plnej many môže mág vykonať magický útok, ktorý bude mať pravdepodobne vyššie damage, ako útok normálne (ale samozrejme záleží na tom, ako si ho nastavíme). Tento útok manu vybije 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.

Vytvoríme teda triedu Mag.kt. Budeme ju chcieť zdediť z triedy Bojovnik a preto najprv povieme Kotlinu, že sa Bojovnik dá dediť pomocou modifikátora open.

Začiatok triedy Bojovník:

open class Bojovnik(private val jmeno: String, private var zivot: Int, private val utok: Int,
               private val obrana: Int, private val kostka: Kostka) {
// Zbytek implementace...
}

Triede Mag dodáme atribúty, ktoré chceme oproti bojovníkovi navyše. Trieda Mag by nateraz vyzerala nejako takto:

class Mag : Bojovnik {
    private val mana: Int
    private val maxMana: Int
    private val magickyUtok: Int
}

Kód zatiaľ nepôjde skompilovať, pretože sme si ešte nevytvorili konštruktor.

V mágovi nemáme zatiaľ prístup ku všetkým premenným, pretože sú v bojovníkovi nastavené ako privátne. Musíme triedu Bojovnik ešte raz zľahka upraviť. Zmeníme modifikátory private u atribútov na protected. Budeme potrebovať len kostka a jmeno, ale pokojne nastavíme ako protected všetky atribúty charakteru, pretože sa v budúcnosti môžu hodiť, keby sme sa rozhodli oddědit ďalšie typy bojovníkov. Naopak atribút zprava 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:

open class Bojovnik(protected val jmeno: String, protected var zivot: Int, protected val utok: Int, protected val obrana: Int, protected val kostka: Kostka) {
    // ...
}

Prejdime ku konstruktoru.

Konštruktor potomka

Kotlín nededia konstruktory! Je to pravdepodobne z toho dôvodu, že predpokladá, že potomok 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, ktorý berie parametre potrebné pre 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 konstruktoru nemusí byť inštancie správne inicializovaná. Konštruktor predka sa vykoná pred naším konštruktory a zavoláme ho pomocou syntaxe : Bojovnik(...)., Kam odovzdáme potrebné parametre.

Konštruktor mága bude vyzerať takto:

class Mag(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka, private var mana: Int, private val magickyUtok: Int) : Bojovnik(jmeno, zivot, utok, obrana, kostka) {

    private val maxMana: Int

    init {
        maxMana = mana
    }
}

Náš konštruktor má teda všetky parametre potrebné pre predka plus tie nové, čo má navyše potomok. Niektoré potom odovzdáme predkovi a niektoré si spracujeme sami.

Presuňme sa teraz do Main.kt a druhého bojovníka (Shadow) zmeňme na mága, napr. Takto:

val gandalf: Bojovnik = Mag("Gandalf", 60, 18, 15, kostka, 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 Bojovnik. Nič nám v tom nebráni, pretože bojovník je jeho predok. Rovnako tak si môžeme typ premennej zmeniť na Mag. 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ť rovnako ako s bojovníkom. My už vieme, že takémuto mechanizmu hovoríme polymorfizmus. Aréna zavolá na objekte metódu utoc() 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 utoc() z predka. Prepíšeme zdedenú metódu tak, aby útok pracoval s mannou, hlavička metódy však zostane rovnaká.

Keď sme pri metódach, budeme v Bojovnik.kt ešte určite používať metódu nastavZpravu(), tá je však privátne. Označme ju ako protected:

protected fun nastavZpravu(zprava: String) {

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, kedy im rozumieme :)

Poďme prepísať metódu utoc() bojovníka v mágovi. Metódu musíme najprv označiť ako open v predkovi:

open fun utoc(souper: Bojovnik) {

Teraz metódu normálne definujeme v Mag.kt tak, ako sme zvyknutí, len použijeme kľúčové slovíčko override:

override fun utoc(souper: Bojovnik) {

Podobne sme prepisovali metódu toString() u našich objektov.

Správanie metódy utoc() 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 fun utoc(souper: Bojovnik) {
    var uder = 0
    // Mana není naplněna
    if (mana < maxMana) {
        mana += 10
        if (mana > maxMana) {
            mana = maxMana
        }
        uder = utok + kostka.hod()
        nastavZpravu("$jmeno útočí s úderem za $uder hp")
    } else { // Magický útok
        uder = magickyUtok + kostka.hod()
        nastavZpravu("$jmeno použil magii za $uder hp")
        mana = 0
    }
    souper.branSe(uder)
}

Kód je asi zrozumiteľný. Všimnite si obmedzenia many na maxMana, môže sa nám totiž stať, že túto hodnotu presiahne, keď ju zvyšujeme o 10. Keď sa nad kódom zamyslíme, tak útok vyššie v podstate vykonáva pôvodnej metóda utoc().

V Kotlinu existuje tiež kľúčové slovo super, ktoré je podobné nami už známemu this. Na rozdiel od this, ktoré odkazuje na konkrétnu inštanciu triedy, super odkazuje na predka. Môžeme teda volať metódy predka, aj keď je potomok treba prepísal.

Iste by bolo prínosné zavolať podobu metódy na predkovi namiesto toho, aby sme správanie odpisovali. K tomu použijeme práve super:

    override fun utoc(souper: Bojovnik) {
        // Mana není naplněna
        if (mana < maxMana) {
            mana += 10
            if (mana > maxMana) {
                mana = maxMana
            }
            super.utoc(souper)
        } else { // Magický útok
            val uder = magickyUtok + kostka.hod()
            nastavZpravu("$jmeno použil magii za $uder hp")
            souper.branSe(uder)
            mana = 0
        }
    }
// vytvoření objektů
val kostka = Kostka(10)
val zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka)
val gandalf: Bojovnik = Mag("Gandalf", 60, 18, 15, kostka, 30, 45)
val arena = Arena(zalgoren, gandalf, kostka)

// zápas
arena.zapas()
class Kostka(pocetSten: Int) {

    val pocetSten: Int

    constructor() : this(6)

    init {
        this.pocetSten = pocetSten
    }

    fun hod(): Int {
        return (1..pocetSten).shuffled().first()
    }

    override fun toString(): String {
        return "Kostka s $pocetSten stěnami"
    }
}
import kotlin.math.*

open class Bojovnik(protected val jmeno: String, protected var zivot: Int, protected val utok: Int,
protected val obrana: Int, protected val kostka: Kostka) {

    protected val maxZivot = zivot
    private var zprava = ""

    protected fun nastavZpravu(zprava: String) {
        this.zprava = zprava
    }

    fun vratPosledniZpravu(): String {
        return zprava
    }

    fun nazivu(): Boolean {
        return (zivot > 0)
    }

    fun grafickyZivot(): String {
        var s = "["
        val celkem = 20
        var pocet = round((zivot.toDouble()/maxZivot) * celkem).toInt()
        if ((pocet == 0) && (nazivu()))
        pocet = 1
        s = s.padEnd(pocet + s.length, '#')
        s = s.padEnd(celkem - pocet + s.length, ' ')
        s += "]"
        return s
    }

    open fun utoc(souper: Bojovnik) {
        val uder = utok + kostka.hod()
        nastavZpravu("$jmeno útočí s úderem za $uder hp")
        souper.branSe(uder)
    }

    fun branSe(uder: Int) {
        val zraneni = uder - (obrana + kostka.hod())
        if (zraneni > 0) {
            zivot -= zraneni
            zprava = "$jmeno utrpěl poškození $zraneni hp"
            if (zivot <= 0) {
                zivot = 0
                zprava += " a zemřel"
            }
        } else
            nastavZpravu("$jmeno odrazil útok")
        nastavZpravu(zprava)
    }

    override fun toString(): String {
        return jmeno
    }
}
class Arena(private val bojovnik1: Bojovnik, private val bojovnik2: Bojovnik,
    val kostka: Kostka) {

    private fun vykresli() {
        println("-------------- Aréna --------------\n")
        println("Zdraví bojovníků: \n")
        println("$bojovnik1 ${bojovnik1.grafickyZivot()}")
        println("$bojovnik2 ${bojovnik2.grafickyZivot()}")
    }

    private fun vypisZpravu(zprava: String) {
        println(zprava)
        Thread.sleep(500)
    }

    fun zapas() {
        // původní pořadí
        var b1 = bojovnik1
        var b2 = bojovnik2
        println("Vítejte v aréně!")

        println("Dnes se utkají $bojovnik1 s $bojovnik2! \n")
        // prohození bojovníků
        val zacinaBojovnik2 = kostka.hod() <= kostka.pocetSten / 2
        if (zacinaBojovnik2) {
            b1 = bojovnik2
            b2 = bojovnik1
        }
        println("Začínat bude bojovník $b1! \n\nZápas může začít...")
        // cyklus s bojem
        while (b1.nazivu() && b2.nazivu()) {
            b1.utoc(b2)
            vykresli()
            vypisZpravu(b1.vratPosledniZpravu()) // zpráva o útoku
            vypisZpravu(b2.vratPosledniZpravu()) // zpráva o obraně
            if (b2.nazivu()) {
                b2.utoc(b1)
                vykresli()
                vypisZpravu(b2.vratPosledniZpravu()) // zpráva o útoku
                vypisZpravu(b1.vratPosledniZpravu()) // zpráva o obraně
            }
            System.out.println()
        }
    }
}

Opäť vidíme, ako môžeme znovupoužívat kód. S dedičnosťou je spojené 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á.

-------------- Aréna --------------

Zdraví bojovníků:

Zalgoren [#############       ]
Gandalf [#################   ]
Gandalf použil magii za 52 hp
Zalgoren utrpěl poškození 36 hp

Arena nás však neinformuje o mane mága, poďme to napraviť. Pridáme mágovi verejnú metódu grafickaMana(), ktorá bude obdobne ako u života vracať String s grafickým ukazovateľom many.

Aby sme nemuseli logiku so zložením ukazovatele písať dvakrát, upravíme metódu grafickyZivot() v Bojovnik.kt. Pripomeňme si, ako vyzerá:

fun grafickyZivot(): String {
    var s = "["
    val celkem = 20
    var pocet = round((zivot.toDouble()/maxZivot) * celkem).toInt()
    if ((pocet == 0) && (nazivu()))
        pocet = 1
    s = s.padEnd(pocet + s.length, '#')
    s = s.padEnd(celkem - pocet + s.length, ' ')
    s += "]"
    s.length
    return s
}

Vidíme, že nie je výnimkou premenných zivot a maxZivot na živote nijako závislá. Metódu premenujeme na grafickyUkazatel() a dáme ju 2 parametre: aktuálnu hodnotu a maximálnu hodnotu. zivot a maxZivot v tele metódy potom nahradíme za aktualni a maximalni. Modifikátor metódy bude protected, aby sme ju mohli v potomkovi použiť:

protected fun grafickyUkazatel(aktualni: Int, maximalni: Int): String {
    var s = "["
    val celkem = 20
    var pocet = round((aktualni.toDouble()/ maximalni) * celkem).toInt()
    if ((pocet == 0) && (nazivu()))
        pocet = 1
    s = s.padEnd(pocet + s.length, '#')
    s = s.padEnd(celkem - pocet + s.length, ' ')
    s += "]"
    s.length
    return s
}

Metódu grafickyZivot() v Bojovnik.kt naimplementujeme znovu, bude nám v nej stačiť jediný riadok a to zavolanie metódy grafickyUkazatel() s príslušnými parametrami:

fun grafickyZivot(): String {
    return grafickyUkazatel(zivot, maxZivot)
}

Určite som mohol v tutoriálu s bojovníkom urobiť metódu grafickyUkazatel() rovno. Chcel som však, aby sme si ukázali, ako sa rieši prípady, keď potrebujeme vykonať podobnú funkčnosť 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 Mag.kt a naimplementujme metódu grafickaMana():

fun grafickaMana(): String {
    return grafickyUkazatel(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 Arena.

Rozpoznanie typu objektu

Keďže sa nám teraz vykreslenie bojovníka skomplikovalo, urobíme si na neho samostatnú metódu vypisBojovnika(), jej parametrom bude daná inštancie bojovníka:

private fun vypisBojovnika(b: Bojovnik) {
    println(b)
    print("Zivot: ")
    println(b.grafickyZivot())
}

Teraz poďme reagovať na to, či je bojovník mág. Minule sme si povedali, že k tomu slúži operátor is:

private fun vypisBojovnika(b: Bojovnik) {
    println(b)
    print("Zivot: ")
    println(b.grafickyZivot())
    if (b is Mag)
        print("Mana: ${b.grafickaMana()}") //Kotlin je chytrý a sám přetypovává Bojovníka na Mága
}

To by sme mali, vypisBojovnika() budeme volať v metóde vykresli(), ktorá bude vyzerať takto:

    private fun vykresli() {
        println("-------------- Aréna --------------\n")
        println("Zdraví bojovníků: \n")
        vypisBojovnika(bojovnik1)
        println()
        vypisBojovnika(bojovnik2)
        println("\n")
    }
class Mag(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka, private var mana: Int, private val magickyUtok: Int) : Bojovnik(jmeno, zivot, utok, obrana, kostka) {

    private val maxMana: Int

    init {
        maxMana = mana
    }

    override fun utoc(souper: Bojovnik) {
        // Mana není naplněna
        if (mana < maxMana) {
            mana += 10
            if (mana > maxMana) {
                mana = maxMana
            }
            super.utoc(souper)
        } else { // Magický útok
            val uder = magickyUtok + kostka.hod()
            nastavZpravu("$jmeno použil magii za $uder hp")
            souper.branSe(uder)
            mana = 0
        }
    }

    fun grafickaMana(): String {
        return grafickyUkazatel(mana, maxMana)
    }
}
// vytvoření objektů
val kostka = Kostka(10)
val zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka)
val gandalf: Bojovnik = Mag("Gandalf", 60, 18, 15, kostka, 30, 45)
val arena = Arena(zalgoren, gandalf, kostka)

// zápas
arena.zapas()
class Kostka(pocetSten: Int) {

    val pocetSten: Int

    constructor() : this(6)

    init {
        this.pocetSten = pocetSten
    }

    fun hod(): Int {
        return (1..pocetSten).shuffled().first()
    }

    override fun toString(): String {
        return "Kostka s $pocetSten stěnami"
    }
}
import kotlin.math.*

open class Bojovnik(protected val jmeno: String, protected var zivot: Int, protected val utok: Int, protected val obrana: Int, protected val kostka: Kostka) {
    protected val maxZivot = zivot
    private var zprava = ""

    protected fun nastavZpravu(zprava: String) {
        this.zprava = zprava
    }

    fun vratPosledniZpravu(): String {
        return zprava
    }

    fun nazivu(): Boolean {
        return (zivot > 0)
    }

    protected fun grafickyUkazatel(aktualni: Int, maximalni: Int): String {
        var s = "["
        val celkem = 20
        var pocet = round((aktualni.toDouble()/ maximalni) * celkem).toInt()
        if ((pocet == 0) && (nazivu()))
            pocet = 1
        s = s.padEnd(pocet + s.length, '#')
        s = s.padEnd(celkem - pocet + s.length, ' ')
        s += "]"
        s.length
        return s
    }

    fun grafickyZivot(): String {
        return grafickyUkazatel(zivot, maxZivot)
    }

    open fun utoc(souper: Bojovnik) {
        val uder = utok + kostka.hod()
        nastavZpravu("$jmeno útočí s úderem za $uder hp")
        souper.branSe(uder)
    }

    fun branSe(uder: Int) {
        val zraneni = uder - (obrana + kostka.hod())
        if (zraneni > 0) {
            zivot -= zraneni
            zprava = "$jmeno utrpěl poškození $zraneni hp"
            if (zivot <= 0) {
                zivot = 0
                zprava += " a zemřel"
            }
          } else
            nastavZpravu("$jmeno odrazil útok")
        nastavZpravu(zprava)
    }

    override fun toString(): String {
        return jmeno
    }
}

Hotovo :)

-------------- Aréna --------------

Bojovníci:

Zalgoren
Život: [##########          ]

Gandalf
Život: [#####               ]
Mana: [#############       ]

Zalgoren útočí s úderem za 28 hp

Aplikáciu ešte môžeme dodať krajší vzhľad, vložil som ASCIIart nadpis Aréna, ktorý som vytvoril touto aplikáciou: http://patorjk.com/software/taag. Metódu k vykreslenie ukazovatele som upravil tak, aby vykreslovala plný obdĺžnik miesto # (ten napíšete pomocou Alt + 219). Výsledok môže vyzerať takto:

   __    ____  ____  _  _    __
  /__\  (  _ \( ___)( \( )  /__\
 /(__)\  )   / )__)  )  (  /(__)\
(__)(__)(_)\_)(____)(_)\_)(__)(__)

Bojovníci:

Zalgoren
Život: ████

Gandalf
Život: ███████
Mana:  █

Gandalf použil magii za 48 hp
Zalgoren utrpěl poškození 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 budúcej lekcii, Riešené úlohy k 5.-8. lekciu OOP v Kotlin , si vysvetlíme pojem statika.

V nasledujúcom cvičení, Riešené úlohy k 5.-8. lekciu OOP v Kotlin, 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é 42x (45.03 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Kotlin

 

Predchádzajúci článok
Dedičnosť a polymorfizmus v Kotlin
Všetky články v sekcii
Objektovo orientované programovanie v Kotlin
Preskočiť článok
(neodporúčame)
Riešené úlohy k 5.-8. lekciu OOP v Kotlin
Článok pre vás napísal Samuel Kodytek
Avatar
Užívateľské hodnotenie:
1 hlasov
Autor se věnuje všem jazykům okolo JVM. Rád pomáhá lidem, kteří se zajímají o programování. Věří, že všichni mají šanci se naučit programovat, jen je potřeba prorazit tu bariéru, který se říká lenost.
Aktivity