NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

Lekce 8 - Aréna s mágem - Dědičnost a polymorfismus v Kotlin

V minulé lekci, Dědičnost a polymorfismus v Kotlin, jsme si vysvětlili dědičnost a polymorfismus.

Dnes si vyzkoušíme dědičnost a polymorfismus v praxi. Bude to opět na naší aréně, kde z bojovníka oddědíme mága. Tento tutoriál již patří k těm náročnějším a bude tomu tak i u dalších. Proto si průběžně procvičujte práci s objekty, zkoušejte si naše cvičení a také vymýšlejte nějaké své aplikace, abyste si zažili základní věci. To, že je tu přítomen celý kurz neznamená, že ho celý najednou přečtete a pochopíte :) Snažte se programovat průběžně.

Mág - Objektově orientované programování v Kotlin

Než začneme něco psát, shodněme se na tom, co by měl mág umět. Mág bude fungovat stejně, jako bojovník. Kromě života bude mít však i manu. Zpočátku bude mana plná. V případě plné many může mág vykonat magický útok, který bude mít pravděpodobně vyšší damage, než útok normální (ale samozřejmě záleží na tom, jak si ho nastavíme). Tento útok manu vybije na 0. Každé kolo se bude mana zvyšovat o 10 a mág bude podnikat jen běžný útok. Jakmile se mana zcela doplní, opět bude moci magický útok použít. Mana bude zobrazena grafickým ukazatelem, stejně jako život.

Vytvoříme tedy třídu Mag.kt. Budeme ji chtít zdědit z třídy Bojovnik a proto nejprve řekneme Kotlinu, že se Bojovnik dá dědit pomocí modifikátoru open.

Začátek třídy 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...
}

Třídě Mag dodáme atributy, které chceme oproti bojovníkovi navíc. Třída Mag by prozatím vypadala nějak takto:

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

Kód zatím nepůjde zkompilovat, protože jsme si ještě nevytvořili konstruktor.

V mágovi nemáme zatím přístup ke všem proměnným, protože jsou v bojovníkovi nastavené jako privátní. Musíme třídu Bojovnik ještě jednou lehce upravit. Změníme modifikátory private u atributů na protected. Budeme potřebovat jen kostka a jmeno, ale klidně nastavíme jako protected všechny atributy charakteru, protože se v budoucnu mohou hodit, kdybychom se rozhodli oddědit další typy bojovníků. Naopak atribut zprava není vhodné nastavovat jako protected, protože nesouvisí s bojovníkem, ale s nějakou vnitřní logikou třídy.

Třída tedy bude vypadat nějak takto:

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

Přejděme ke konstruktoru.

Konstruktor potomka

Kotlin nedědí konstruktory! Je to pravděpodobně z toho důvodu, že předpokládá, že potomek bude mít navíc nějaké atributy a původní konstruktor by u něj byl na škodu. To je i náš případ, protože konstruktor mága bude brát oproti tomu z bojovníka navíc 2 parametry (mana a magický útok).

Definujeme si tedy konstruktor v potomkovi, který bere parametry potřebné pro vytvoření bojovníka a několik parametrů navíc pro mága.

U potomků je nutné vždy volat konstruktor předka. Je to z toho důvodu, že bez volání konstruktoru nemusí být instance správně inicializovaná. Konstruktor předka se vykoná před naším konstruktorem a zavoláme jej pomocí syntaxe : Bojovnik(...)., kam předáme potřebné parametry.

Konstruktor mága bude vypadat 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áš konstruktor má tedy všechny parametry potřebné pro předka plus ty nové, co má navíc potomek. Některé potom předáme předkovi a některé si zpracujeme sami.

Přesuňme se nyní do Main.kt a druhého bojovníka (Shadow) změňme na mága, např. takto:

val gandalf: Bojovnik = Mag("Gandalf", 60, 18, 15, kostka, 30, 45)

Změnu samozřejmě musíme udělat i v řádku, kde bojovníka do arény vkládáme. Všimněte si, že mága ukládáme do proměnné typu Bojovnik. Nic nám v tom nebrání, protože bojovník je jeho předek. Stejně tak si můžeme typ proměnné změnit na Mag. Když aplikaci nyní spustíme, bude fungovat úplně stejně, jako předtím. Mág vše dědí z bojovníka a zatím tedy funguje jako bojovník.

Polymorfismus a přepisování metod

Bylo by výhodné, kdyby objekt Arena mohl s mágem pracovat stejným způsobem jako s bojovníkem. My již víme, že takovémuto mechanismu říkáme polymorfismus. Aréna zavolá na objektu metodu utoc() se soupeřem v parametru. Nestará se o to, jestli bude útok vykonávat bojovník nebo mág, bude s nimi pracovat stejně. U mága si tedy přepíšeme metodu utoc() z předka. Přepíšeme zděděnou metodu tak, aby útok pracoval s manou, hlavička metody však zůstane stejná.

Když jsme u metod, budeme v Bojovnik.kt ještě jistě používat metodu nastavZpravu(), ta je však privátní. Označme ji jako protected:

protected fun nastavZpravu(zprava: String) {

Při návrhu bojovníka jsme samozřejmě měli myslet na to, že se z něj bude dědit a již označit vhodné atributy a metody jako protected. V tutoriálu k bojovníkovi jsem vás tím však nechtěl zbytečně zatěžovat, proto musíme modifikátory změnit až teď, kdy jim rozumíme :)

Pojďme přepsat metodu utoc() bojovníka v mágovi. Metodu musíme nejprve označit jako open v předkovi:

open fun utoc(souper: Bojovnik) {

Nyní metodu normálně definujeme v Mag.kt tak, jak jsme zvyklí, pouze použijeme klíčové slovíčko override:

override fun utoc(souper: Bojovnik) {

Podobně jsme přepisovali metodu toString() u našich objektů.

Chování metody utoc() nebude nijak složité. Podle hodnoty many buď provedeme běžný útok nebo útok magický. Hodnotu many potom buď zvýšíme o 10 nebo naopak snížíme na 0 v případě 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 srozumitelný. Všimněte si omezení many na maxMana, může se nám totiž stát, že tuto hodnotu přesáhneme, když ji zvyšujeme o 10. Když se nad kódem zamyslíme, tak útok výše v podstatě vykonává původní metoda utoc().

V Kotlinu existuje také klíčové slovo super, které je podobné námi již známému this. Na rozdíl od this, které odkazuje na konkrétní instanci třídy, super odkazuje na předka. Můžeme tedy volat metody předka, i když je potomek třeba přepsal.

Jistě by bylo přínosné zavolat podobu metody na předkovi místo toho, abychom chování opisovali. K tomu použijeme právě 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ět vidíme, jak můžeme znovupoužívat kód. S dědičností je spojeno opravdu mnoho technik, jak si ušetřit práci. V našem případě to ušetří několik řádků, ale u většího projektu by to mohlo mít obrovský význam.

Aplikace nyní funguje tak, jak 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 maně mága, pojďme to napravit. Přidáme mágovi veřejnou metodu grafickaMana(), která bude obdobně jako u života vracet String s grafickým ukazatelem many.

Abychom nemuseli logiku se složením ukazatele psát dvakrát, upravíme metodu grafickyZivot() v Bojovnik.kt. Připomeňme si, jak vypadá:

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 není kromě proměnných zivot a maxZivot na životě nijak závislá. Metodu přejmenujeme na grafickyUkazatel() a dáme ji 2 parametry: aktuální hodnotu a maximální hodnotu. zivot a maxZivot v těle metody poté nahradíme za aktualni a maximalni. Modifikátor metody bude protected, abychom ji mohli v potomkovi použít:

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
}

Metodu grafickyZivot() v Bojovnik.kt naimplementujeme znovu, bude nám v ní stačit jediný řádek a to zavolání metody grafickyUkazatel() s příslušnými parametry:

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

Určitě jsem mohl v tutoriálu s bojovníkem udělat metodu grafickyUkazatel() rovnou. Chtěl jsem však, abychom si ukázali, jak se řeší případy, kdy potřebujeme vykonat podobnou funkčnost vícekrát. S takovouto parametrizací se v praxi budete setkávat často, protože nikdy přesně nevíme, co budeme v budoucnu od našeho programu požadovat.

Nyní můžeme vykreslovat ukazatel tak, jak se nám to hodí. Přesuňme se do Mag.kt a naimplementujme metodu grafickaMana():

fun grafickaMana(): String {
    return grafickyUkazatel(mana, maxMana)
}

Jednoduché, že? Nyní je mág hotový, zbývá jen naučit arénu zobrazovat manu v případě, že je bojovník mág. Přesuňme se tedy do Arena.

Rozpoznání typu objektu

Jelikož se nám nyní vykreslení bojovníka zkomplikovalo, uděláme si na něj samostatnou metodu vypisBojovnika(), jejím parametrem bude daná instance bojovníka:

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

Nyní pojďme reagovat na to, jestli je bojovník mág. Minule jsme si řekli, že k tomu slouží 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 bychom měli, vypisBojovnika() budeme volat v metodě vykresli(), která bude vypadat 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

Aplikaci ještě můžeme dodat hezčí vzhled, vložil jsem ASCIIart nadpis Aréna, který jsem vytvořil touto aplikací: http://patorjk.com/software/taag . Metodu k vykreslení ukazatele jsem upravil tak, aby vykreslovala plný obdelník místo # (ten napíšete pomocí Alt + 219). Výsledek může vypadat 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 příloze. Pokud jste něčemu nerozuměli, zkuste si článek přečíst vícekrát nebo pomaleji, jsou to důležité praktiky.

V následujícím cvičení, Řešené úlohy k 5.-8. lekci OOP v Kotlin, si procvičíme nabyté zkušenosti z předchozích lekcí.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 42x (45.03 kB)
Aplikace je včetně zdrojových kódů v jazyce Kotlin

 

Předchozí článek
Dědičnost a polymorfismus v Kotlin
Všechny články v sekci
Objektově orientované programování v Kotlin
Přeskočit článek
(nedoporučujeme)
Řešené úlohy k 5.-8. lekci OOP v Kotlin
Článek pro vás napsal Samuel Kodytek
Avatar
Uživatelské hodnocení:
18 hlasů
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