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.
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
:
{KOTLIN_OOP} 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 } } } {/KOTLIN_OOP}
{KOTLIN_OOP} {KOTLIN_MAIN_BLOCK} // 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() {/KOTLIN_MAIN_BLOCK} {/KOTLIN_OOP}
{KOTLIN_OOP} 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" } } {/KOTLIN_OOP}
{KOTLIN_OOP} 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 } } {/KOTLIN_OOP}
{KOTLIN_OOP} 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() } } } {/KOTLIN_OOP}
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:
{KOTLIN_OOP} 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") vypisBojovnika(bojovnik1) println() vypisBojovnika(bojovnik2) println("\n") } 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 } 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ě } println() } } } {/KOTLIN_OOP}
{KOTLIN_OOP} 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) } } {/KOTLIN_OOP}
{KOTLIN_OOP} {KOTLIN_MAIN_BLOCK} // 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() {/KOTLIN_MAIN_BLOCK} {/KOTLIN_OOP}
{KOTLIN_OOP} 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" } } {/KOTLIN_OOP}
{KOTLIN_OOP} 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 } } {/KOTLIN_OOP}
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