IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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í.

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

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 - Objektově orientované programování ve Swift

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 super 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)
    }
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ě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((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
}

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")
        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

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 ve Swift, 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 8x (26.19 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift

 

Předchozí článek
Dědičnost a polymorfismus ve Swift
Všechny články v sekci
Objektově orientované programování ve Swift
Přeskočit článek
(nedoporučujeme)
Řešené úlohy k 5.-8. lekci OOP ve Swift
Článek pro vás napsal Filip Němeček
Avatar
Uživatelské hodnocení:
5 hlasů
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity