IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

8. diel - Aréna s mágom (dedičnosť a polymorfizmus) vo Swift

V minulej lekcii, Dedičnosť a polymorfizmus vo Swift , 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 Swift 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 a tiež vymýšľajte nejaké svoje aplikácie, aby ste si zažili základné veci. To, že je tu prítomný celý online kurz neznamená, že ho celý naraz prečítate a pochopíte :) Snažte sa programovať priebežne.

mág - Objektovo orientované programovanie vo Swift
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 v súbore Bojovnik.swift, zdedíme ju z Bojovnik a dodáme ju vlastnosti, ktoré chceme oproti bojovníkovi navyše. Bude teda vyzerať takto:

class Mag: Bojovnik {
    private var mana : Double
    private var maxMana : Double
    private var magickyUtok : Int
}

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 ľahko upraviť. Zmeníme modifikátory private u vlastností na fileprivate. Budeme potrebovať len kostka a jmeno, ale pokojne nastavíme ako fileprivate všetky vlastnosti charakteru, pretože sa v budúcnosti môžu hodiť, keby sme sa rozhodli oddědit ďalšie typy bojovníkov. Naopak vlastnosť zprava nie je vhodné nastavovať ako fileprivate, pretože nesúvisí s bojovníkom, ale s nejakou vnútornou logikou triedy. Trieda teda bude vyzerať nejako takto:

class Bojovnik {
    fileprivate var jmeno : String
    fileprivate var zivot : Double
    fileprivate var maxZivot : Double
    fileprivate var utok : Int
    fileprivate var obrana : Int
    fileprivate var kostka : Kostka
    private var zprava : String = ""

    // ...

Prejdime ku konstruktoru.

Viac konštruktor vo Swift

Nastala ideálnu príležitosť vysvetliť si, ako vo Swiftu funguje viac konstruktoru, respektíve metód init(). Swift rozlišuje tzv. Designated a convenience konštruktory. Designated by sa dalo najlepšie preložiť ako "označený" a je to vlastne primárnou / predvolený konštruktor. Ak máme iba jeden init(), tak je automaticky designated.

Ak chceme mať v triede viac konstruktoru, aby sa dala jej inštancie vytvoriť na základe rôznych parametrov, tak musíme mať tie ďalšie označené slovíčkom convenience, čo by sa dalo označiť ako pohodlný. A tieto konštruktory musia cez self volať designated konštruktor. Nie je to nič zložité a môžeme si ukázať jednoduchý prípad, ak by sme chceli nášho bojovníka vytvoriť bez parametrov. Vyzeralo by to asi takto:

init(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka) {
        self.jmeno = jmeno
        self.zivot = Double(zivot)
        self.maxZivot = self.zivot
        self.utok = utok
        self.obrana = obrana
        self.kostka = kostka
}

convenience init() {
        self.init(jmeno: "Standardní válečník", zivot: 100, utok: 20, obrana: 10, kostka: Kostka())
}

Teraz môžeme zadať parametre bojovníka, použiť prvý konštruktor, ale aj napísať iba new Bojovnik(), čím sa použije konštruktor druhý. Ten pomocou volania designated konstruktoru nastaví predvolené hodnoty.

Konštruktor potomka

Swift dedí konstruktory iba v špecifických prípadoch. Konštruktor bude zdedený, ak novým vlastnostiam potomka nastavíme predvolené hodnoty (alebo budú Optional) a tým pádom nie je vyžadovaný konštruktor. Rovnako tak nesmieme vytvoriť designated init() metódu, aby sme neprišli o zdedený konštruktor. Convenience konštruktory je možné pridať.

V našom prípade mága bude lepšie vytvoriť vlastný konštruktor, pretože máme vlastnosti navyše, ktoré v ňom chceme nastavovať.

Definujeme si teda konštruktor v potomkovi, ktorý berie parametre potrebné pre vytvorenie bojovníka a niekoľko parametrov navyše pre mága.

V konštruktor 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 nevoláme iba v prípade, že žiadny nemá. Náš konštruktor musia mať samozrejme všetky parametre potrebné pre predka plus tie nové, čo má navyše potomok. Niektoré potom odovzdáme predkovi a niektoré si spracujeme sami. Konštruktor predka je nutné zavolať až nakoniec, inak Swift zobrazí chybu.

Vo Swift existuje kľúčové slovo super, ktoré je podobné nami už známemu self. Na rozdiel od self, 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.

Konštruktor mága bude teda vyzerať takto:

init(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka, mana: Int, magickyUtok: Int) {
    self.mana = Double(mana)
    self.maxMana = self.mana
    self.magickyUtok = magickyUtok

    super.init(jmeno: jmeno, zivot: zivot, utok: utok, obrana: obrana, kostka: kostka)
}

Rovnako môžeme volať aj iný konštruktor v tej istej triede (nie predka), len miesto super použijeme self.

Opäť sme si previedli manu interne na Double, čoskoro uvidíte prečo.

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

let gandalf : Bojovnik = Mag(jmeno: "Gandalf", zivot: 60, utok: 15, obrana: 12, kostka: kostka, mana: 30, magickyUtok: 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á.

Prepísanie metódy z predka vykonáme v potomkovi pomocou slovíčka override, ako si ukážeme nižšie.

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

fileprivate func 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é vlastnosti a metódy ako fileprivate.

Teraz sa vráťme do potomka a poďme prepísať metódu utoc(). Metódu normálne definujeme v Mag.swift tak, ako sme zvyknutí. Jej definíciu ale začneme slovom override, ktoré značí, že si sme vedomí toho, že sa metóda zdedila, ale prajeme si zmeniť jej správanie.

override func utoc(souper: Bojovnik)

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 func 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: 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(). Iste by bolo prínosné zavolať podobu metódy na predkovi namiesto toho, aby sme správanie odpisovali. K tomu opäť použijeme super:

    override func utoc(souper: Bojovnik) {
        var uder = 0

        // Mana není naplněna
       if mana < maxMana {
            mana += 10;
            if (mana > maxMana) {
                mana = maxMana
            }
            super.utoc(souper: souper)
            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: uder)
    }
class Arena {
    private var bojovnik1 : Bojovnik
    private var bojovnik2 : Bojovnik
    private var kostka : Kostka

    init(bojovnik1: Bojovnik, bojovnik2: Bojovnik, kostka: Kostka) {
        self.bojovnik1 = bojovnik1
        self.bojovnik2 = bojovnik2
        self.kostka = kostka
    }

    func vykresli() {
        print("\n \n \n \n \n \n \n \n")
        print("-------------- Aréna -------------- \n")
        print("Zdraví bojovníků: \n")
        print("\(bojovnik1) \(bojovnik1.grafickyZivot())")
        print("\(bojovnik2) \(bojovnik2.grafickyZivot())")
    }

    private func vypisZpravu(_ zprava: String) {
        print(zprava)
        sleep(1)
    }

    func zapas() {
        // původní pořadí
        var b1 = bojovnik1
        var b2 = bojovnik2
        print("Vítejte v aréně!")
        print("Dnes se utkají \(bojovnik1) s \(bojovnik2)! \n")
        // prohození bojovníků
        let zacinaBojovnik2 = kostka.hod() <= kostka.vratPocetSten() / 2
        if (zacinaBojovnik2) {
            b1 = bojovnik2
            b2 = bojovnik1
        }
        print("Začínat bude bojovník \(b1)! \nZápas může začít...")
        _ = readLine()
        // cyklus s bojem
        while b1.nazivu() && b2.nazivu() {
            b1.utoc(souper: b2)
            vykresli()
            vypisZpravu(b1.vratPosledniZpravu()) // zpráva o útoku
            vypisZpravu(b2.vratPosledniZpravu()) // zpráva o obraně
            if (b2.nazivu()) {
                b2.utoc(souper: b1)
                vykresli()
                vypisZpravu(b2.vratPosledniZpravu()) // zpráva o útoku
                vypisZpravu(b1.vratPosledniZpravu()) // zpráva o obraně
            }
            print(" ")
        }
    }

}
// vytvoření objektů
let kostka = Kostka(pocetSten: 10)
let zalgoren = Bojovnik(jmeno: "Zalgoren", zivot: 100, utok: 20, obrana: 10, kostka: kostka)
let gandalf : Bojovnik = Mag(jmeno: "Gandalf", zivot: 60, utok: 15, obrana: 12, kostka: kostka, mana: 30, magickyUtok: 45)
let arena = Arena(bojovnik1: zalgoren, bojovnik2: gandalf, kostka: kostka)
// zápas
arena.zapas()
class Kostka : CustomStringConvertible {

    var description: String {
        return "Kostka s \(pocetSten) stěnami"
    }

    private var pocetSten : Int

    init() {
        pocetSten = 6
    }

    init(pocetSten: Int) {
        self.pocetSten = pocetSten
    }

    func vratPocetSten() -> Int {
        return pocetSten
    }

    func hod() -> Int {
        return Int(arc4random_uniform(UInt32(pocetSten))) + 1
    }
}

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

Aréna 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.swift. Pripomeňme si, ako vyzerá:

func grafickyZivot() -> String {
    var s = "["
    let celkem : Double = 20

    var pocet : Double = round((zivot / maxZivot) * celkem)
    if (pocet == 0) && (nazivu()) {
        pocet = 1;
    }

    for _ in 0..<Int(pocet) {
        s += "#"
    }

    s = s.padding(toLength: Int(celkem) + 1, withPad: " ", startingAt: 0)
    s += "]"
    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. Premenné zivot a maxZivot v tele metódy potom nahradíme za aktualni a maximalni. Modifikátor bude fileprivate, aby sme metódu mohli v potomkovi použiť:

fileprivate func grafickyUkazatel(aktualni: Double, maximalni: Double) -> String {
    var s = "["
    let celkem : Double = 20

    var pocet : Double = round((aktualni / maximalni) * celkem)
    if (pocet == 0) && (nazivu()) {
        pocet = 1;
    }

    for _ in 0..<Int(pocet) {
        s += "#"
    }

    s = s.padding(toLength: Int(celkem) + 1, withPad: " ", startingAt: 0)
    s += "]"
    return s
}

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

func grafickyZivot() -> String {
    return grafickyUkazatel(aktualni: zivot, maximalni: 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 triedy Mag a naimplementujme metódu grafickaMana():

func grafickaMana() -> String {
    return grafickyUkazatel(aktualni: mana, maximalni: 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.swift.

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:

func vypisBojovnika(_ b: Bojovnik) {
    print(b)
    print("Život:", terminator: " ")
    print(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:

func vypisBojovnika(_ b: Bojovnik) {
    print(b)
    print("Život:", terminator: " ")
    print(b.grafickyZivot())
    if b is Mag {
        print("Mana:", terminator: " ")
        print((b as! Mag).grafickaMana())
    }
}

Bojovníka sme museli na mága pretypovať pomocou operátora as, aby sme sa k metóde grafickaMana() dostali. Samotný Bojovnik ju totiž nemá. Zas tu máme výkričník známy z Optional. Funguje tu veľmi podobne. Keby premenná b nebola na pozadí typu Mag, tak program spadne. My sa ale najskôr pýtame pomocou is, či Mag je a až potom vykonáme vynútené pretypovanie. Mohli by sme použiť ?, Ktorý by vrátil Optional a my mohli výsledok pretypovania spracovať bezpečne. Tu to ale nie je nutné a ani vhodné.

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

    func vykresli() {
        print("\n \n \n \n \n \n \n \n")
        print("-------------- Aréna -------------- \n")
        print("Zdraví bojovníků: \n")
        vypisBojovnika(bojovnik1)
        print(" ")
        vypisBojovnika(bojovnik2)
    }
class Bojovnik: CustomStringConvertible {

    fileprivate var jmeno : String
    fileprivate var zivot : Double
    fileprivate var maxZivot : Double
    fileprivate var utok : Int
    fileprivate var obrana : Int
    fileprivate var kostka : Kostka
    private var zprava : String = ""

    init(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka) {
        self.jmeno = jmeno
        self.zivot = Double(zivot)
        self.maxZivot = self.zivot
        self.utok = utok
        self.obrana = obrana
        self.kostka = kostka
    }

    convenience init() {
        self.init(jmeno: "Standardní válečník", zivot: 100, utok: 20, obrana: 10, kostka: Kostka())
    }

    var description: String {
        return jmeno
    }

    func nazivu() -> Bool {
        return zivot > 0
    }

    fileprivate func grafickyUkazatel(aktualni: Double, maximalni: Double) -> String {
        var s = "["
        let celkem : Double = 20

        var pocet : Double = round((aktualni / maximalni) * celkem)
        if (pocet == 0) && (nazivu()) {
            pocet = 1;
        }

        for _ in 0..<Int(pocet) {
            s += "#"
        }

        s = s.padding(toLength: Int(celkem) + 1, withPad: " ", startingAt: 0)
        s += "]"
        return s
    }

    func grafickyZivot() -> String {
        return grafickyUkazatel(aktualni: zivot, maximalni: maxZivot)
    }

    func utoc(souper: Bojovnik) {
        let uder = utok + kostka.hod()
        nastavZpravu("\(jmeno) útočí s úderem za \(uder) hp")
        souper.branSe(uder: uder)
    }

    func branSe(uder: Int) {
        let zraneni = Double(uder - (obrana + kostka.hod()))
        var zprava = ""
        if (zraneni > 0) {
            zivot -= zraneni
            zprava = "\(jmeno) utrpěl poškození \(Int(zraneni)) hp"
            if (zivot <= 0) {
                zivot = 0
            }
        } else {
            zprava = "\(jmeno) odrazil útok"
        }
        nastavZpravu(zprava)
    }

    fileprivate func nastavZpravu(_ zprava: String) {
        self.zprava = zprava
    }

    func vratPosledniZpravu() -> String {
        return zprava
    }
}

class Mag: Bojovnik {
    private var mana : Double
    private var maxMana : Double
    private var magickyUtok : Int

    init(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka, mana: Int, magickyUtok: Int) {
        self.mana = Double(mana)
        self.maxMana = self.mana
        self.magickyUtok = magickyUtok

        super.init(jmeno: jmeno, zivot: zivot, utok: utok, obrana: obrana, kostka: kostka)
    }

    override func utoc(souper: Bojovnik) {
        var uder = 0

        // Mana není naplněna
       if mana < maxMana {
            mana += 10;
            if (mana > maxMana) {
                mana = maxMana
            }
            super.utoc(souper: souper)
            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: uder)
    }

    func grafickaMana() -> String {
        return grafickyUkazatel(aktualni: mana, maximalni: maxMana)
    }
}
// vytvoření objektů
let kostka = Kostka(pocetSten: 10)
let zalgoren = Bojovnik(jmeno: "Zalgoren", zivot: 100, utok: 20, obrana: 10, kostka: kostka)
let gandalf : Bojovnik = Mag(jmeno: "Gandalf", zivot: 60, utok: 15, obrana: 12, kostka: kostka, mana: 30, magickyUtok: 45)
let arena = Arena(bojovnik1: zalgoren, bojovnik2: gandalf, kostka: kostka)
// zápas
arena.zapas()
class Kostka : CustomStringConvertible {

    var description: String {
        return "Kostka s \(pocetSten) stěnami"
    }

    private var pocetSten : Int

    init() {
        pocetSten = 6
    }

    init(pocetSten: Int) {
        self.pocetSten = pocetSten
    }

    func vratPocetSten() -> Int {
        return pocetSten
    }

    func hod() -> Int {
        return Int(arc4random_uniform(UInt32(pocetSten))) + 1
    }
}

Hotovo :)

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

Zdraví bojovníků:

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

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

Zalgoren útočí s úderem za 28 hp

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 vo Swift , si vysvetlíme pojem statika.

V nasledujúcom cvičení, Riešené úlohy k 5.-8. lekciu OOP vo Swift, si precvičíme nadobudnuté skúsenosti z predchádzajúcich lekcií.


 

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 8x (26.19 kB)

 

Predchádzajúci článok
Dedičnosť a polymorfizmus vo Swift
Všetky články v sekcii
Objektovo orientované programovanie vo Swift
Preskočiť článok
(neodporúčame)
Riešené úlohy k 5.-8. lekciu OOP vo Swift
Článok pre vás napísal Filip Němeček
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity