Black Friday Black Friday
Black Friday výprodej! Až 80 % extra bodů zdarma! Více informací zde

Lekce 8 - Aréna s mágem (dědičnost a polymorfismus) ve Swift

Swift OOP Aréna s mágem (dědičnost a polymorfismus) ve Swift

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Dědičnost a polymorfismus ve Swift, jsme si vysvětlili dědičnost a polymorfismus. Dnes máme slíbeno, že si je vyzkoušíme v praxi. Bude to opět na naší aréně, kde z bojovníka oddědíme mága. Tento Swift 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 a také vymýšlejte nějaké své aplikace, abyste si zažili základní věci. To, že je tu přítomen celý online kurz neznamená, že ho celý najednou přečtete a pochopíte :) Snažte se programovat průběžně.

Mág

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 v souboru Bojovnik.swift, zdědíme ji z Bojovnik a dodáme ji vlastnosti, které chceme oproti bojovníkovi navíc. Bude tedy vypadat takto:

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

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 lehce upravit. Změníme modifikátory private u vlastností na fileprivate. Budeme potřebovat jen kostka a jmeno, ale klidně nastavíme jako fileprivate všechny vlastnosti charakteru, protože se v budoucnu mohou hodit, kdybychom se rozhodli oddědit další typy bojovníků. Naopak vlastnost zprava není vhodné nastavovat jako fileprivate, protože nesouvisí s bojovníkem, ale s nějakou vnitřní logikou třídy. Třída tedy bude vypadat nějak 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 = ""

        // ...

Přejděme ke konstruktoru.

Více konstruktorů ve Swift

Nastala ideální příležitost vysvětlit si, jak ve Swiftu funguje více konstruktorů, respektive metod init(). Swift rozlišuje tzv. designated a convenience konstruktory. Designated by se dalo nejlépe přeložit jako "označený" a je to vlastně primární/výchozí konstruktor. Pokud máme pouze jeden init(), tak je automaticky designated.

Jestliže chceme mít ve třídě více konstruktorů, aby se dala její instance vytvořit na základě různých parametrů, tak musíme mít ty další označené slovíčkem convenience, což by se dalo označit jako pohodlný. A tyto konstruktory musí přes self volat designated konstruktor. Není to nic složitého a můžeme si ukázat jednoduchý případ, pokud bychom chtěli našeho bojovníka vytvořit bez parametrů. Vypadalo 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())
}

Nyní můžeme zadat parametry bojovníka, použít první konstruktor, ale i napsat pouze new Bojovnik(), čímž se použije konstruktor druhý. Ten pomocí volání designated konstruktoru nastaví výchozí hodnoty.

Konstruktor potomka

Swift dědí konstruktory pouze ve specifických případech. Konstruktor bude zděděn, pokud novým vlastnostem potomka nastavíme výchozí hodnoty (nebo budou Optional) a tím pádem není vyžadován konstruktor. Stejně tak nesmíme vytvořit designated init() metodu, abychom nepřišli o zděděný konstruktor. Convenience konstruktory je možné přidat.

V našem případě Mága bude lepší vytvořit vlastní konstruktor, protože máme vlastnosti navíc, které v něm chceme nastavovat.

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.

V konstruktorech 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 nevoláme pouze v případě, že žádný nemá. Náš konstruktor musí mít samozřejmě 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. Konstruktor předka je nutné zavolat až nakonec, jinak Swift zobrazí chybu.

Ve Swift existuje klíčové slovo super, které je podobné námi již známému self. Na rozdíl od self, které odkazuje na konkrétní instanci třídy, super odkazuje na předka. My tedy můžeme zavolat konstruktor předka s danými parametry a poté vykonat navíc inicializaci pro mága.

Konstruktor mága bude tedy vypadat 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)
}

Stejně můžeme volat i jiný konstruktor v té samé třídě (ne předka), jen místo base použijeme self.

Opět jsme si převedli manu interně na Double, brzy uvidíte proč.

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

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

Přepsání metody z předka provedeme v potomkovi pomocí slovíčka override, jak si ukážeme níže.

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

fileprivate func 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é vlastnosti a metody jako fileprivate.

Nyní se vraťme do potomka a pojďme přepsat metodu utoc(). Metodu normálně definujeme v Mag.swift tak, jak jsme zvyklí. Její definici ale začneme slovem override, které značí, že si jsme vědomi toho, že se metoda zdědila, ale přejeme si změnit její chování.

override func utoc(souper: Bojovnik)

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 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 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(). Jistě by bylo přínosné zavolat podobu metody na předkovi místo toho, abychom chování opisovali. K tomu opět 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)
}

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

Aréna 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.swift. Připomeňme si, jak vypadá:

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 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. Proměnné zivot a maxZivot v těle metody poté nahradíme za aktualni a maximalni. Modifikátor bude fileprivate, abychom metodu mohli v potomkovi použít:

fileprivate func grafickyUkazatel(aktualni: Double, maximalni: Double) -> 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
}

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

func grafickyZivot() -> String {
        return grafickyUkazatel(aktualni: zivot, maximalni: 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 třídy Mag a naimplementujme metodu grafickaMana():

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

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:

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

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 jsme museli na mága přetypovat pomocí operátoru as, abychom se k metodě grafickaMana() dostali. Samotný Bojovnik ji totiž nemá. Zas tu máme vykřičník známý z Optional. Funguje tu velmi podobně. Kdyby proměnná b nebyla na pozadí typu Mag, tak program spadne. My se ale nejdříve ptáme pomocí is, jestli Mag je a až poté provedeme vynucené přetypování. Mohli bychom použít ?, který by vrátil Optional a my mohli výsledek přetypování zpracovat bezpečně. Zde to ale není nutné a ani vhodné.

To bychom měli, vypisBojovnika() budeme volat v metodě vykresli(), která bude vypadat takto:

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

Hotovo :)

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

Bojovníci:

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

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

Zalgoren útočí s úderem za 28 hp

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 příští lekci, Statika ve Swift, si vysvětlíme pojem statika.


 

 

Článek pro vás napsal Filip Němeček
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Autor se příležitostně věnuje vývoji iOS aplikací či těch webových za pomocí frameworku Django. Aktuální projekt: hrejzdarma.cz Twitter: @nemecek_f (neprogramátorské tweety)
Aktivity (2)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!