8. díl - Aréna s mágem (dědičnost a polymorfismus)

C# .NET Objektově orientované programování Aréna s mágem (dědičnost a polymorfismus) American English version English version

V minulém dílu tutoriálů o C# 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 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ý seriál 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.cs, zdědíme ji z Bojovnik a dodáme ji atributy, které chceme oproti bojovníkovi navíc. Bude tedy vypadat takto (opět si ji okomentujte):

class Mag: Bojovnik
{
        private int mana;
        private int maxMana;
        private int magickyUtok;
}

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 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:

protected string jmeno;
protected int zivot;
protected int maxZivot;
protected int utok;
protected int obrana;
protected Kostka kostka;
private string zprava;

...

Přejděme ke konstruktoru.

Konstruktor potomka

C# 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.

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 se vykoná před naším konstruktorem.

V C# .NET existuje klíčové slovo base, které je podobné námi již známému this. Na rozdíl od this, které odkazuje na konkrétní instanci třídy, base 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. V C# se volání konstruktoru předka píše do hlavičky metody.

Konstruktor mága bude tedy vypadat takto:

public Mag(string jmeno, int zivot, int utok, int obrana, Kostka kostka, int mana, int magickyUtok): base(jmeno, zivot, utok, obrana, kostka)
{
        this.mana = mana;
        this.maxMana = mana;
        this.magickyUtok = magickyUtok;
}

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

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

Bojovnik gandalf = new Mag("Gandalf", 60, 15, 12, 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á.

Abychom mohli nějakou metodu přepsat, musí být v předkovi označena jako virtuální. Nehledejte za tím žádnou vědu, jednoduše pomocí klíčového slova virtual C# sdělíme, že si přejeme, aby potomek mohl tuto metodu přepsat. Hlavičku metody v Bojovnik.cs tedy změníme na:

public virtual void Utoc(Bojovnik souper)

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

protected void NastavZpravu(string zprava)

Pozn. 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, případně metody jako virtuální. Klíčovým slovem virtual je označena metoda, kterou lze v potomkovi přepsat, jinak to není možné. 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 :)

Metoda Utoc() v bojovníkovi bude tedy public virtual. Nyní se vraťme do potomka a pojďme ji přepsat. Metodu normálně definujeme v Mag.cs tak, jak jsme zvyklí. Za modifikátorem public však ještě použijeme klíčové slovo override, které značí, že si jsme vědomi toho, že se metoda zdědila, ale přejeme si změnit její chování.

public override void Utoc(Bojovnik souper)

Podobně jsme přepisovali metodu ToString() u našich objektů, každý objekt v C# je totiž odděděný od System.Object, který obsahuje 4 metody, jedna z nich je i ToString(). Při její implementaci tedy musíme použít override.

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.

public override void Utoc(Bojovnik souper)
{
        int uder = 0;
        // Mana není naplněna
        if (mana < maxMana)
        {
                mana += 10;
                if (mana > maxMana)
                        mana = maxMana;
                uder = utok + kostka.hod();
                NastavZpravu(String.Format("{0} útočí s úderem za {1} hp", jmeno, uder));
        }
        else // Magický útok
        {
                uder = magickyUtok + kostka.hod();
                NastavZpravu(String.Format("{0} použil magii za {1} hp", jmeno, uder));
                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(). Jistě by bylo přínosné zavolat podobu metody na předkovi místo toho, abychom chování opisovali. K tomu opět použijeme base:

public override void Utoc(Bojovnik souper)
{
        // Mana není naplněna
        if (mana < maxMana)
        {
                mana += 10;
                if (mana > maxMana)
                        mana = maxMana;
                base.Utoc(souper);
        }
        else // Magický útok
        {
                int uder = magickyUtok + kostka.hod();
                NastavZpravu(String.Format("{0} použil magii za {1} hp", jmeno, uder));
                souper.BranSe(uder);
                mana = 0;
        }
}

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á.

Tahová hra aréna s mágem v C#

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

public string GrafickyZivot()
{
        string s = "[";
        int celkem = 20;
        double pocet = Math.Round(((double)zivot / maxZivot) * celkem);
        if ((pocet == 0) && (Nazivu()))
                pocet = 1;
        for (int i = 0; i < pocet; i++)
                s += "#";
        s = s.PadRight(celkem + 1);
        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. zivot a maxZivot v těle metody poté nahradíme za aktualni a maximalni. Modifikátor bude protected, abychom metodu mohli v potomkovi použít:

protected string GrafickyUkazatel(int aktualni, int maximalni)
{
        string s = "[";
        int celkem = 20;
        double pocet = Math.Round(((double)aktualni / maximalni) * celkem);
        if ((pocet == 0) && (Nazivu()))
                pocet = 1;
        for (int i = 0; i < pocet; i++)
                s += "#";
        s = s.PadRight(celkem + 1);
        s += "]";
        return s;
}

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

public string GrafickyZivot()
{
        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.cs a naimplementujme metodu GrafickaMana():

public string GrafickaMana()
{
        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.cs.

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 void VypisBojovnika(Bojovnik b)
{
        Console.WriteLine(b);
        Console.Write("Zivot: ");
        Console.WriteLine(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 void VypisBojovnika(Bojovnik b)
{
        Console.WriteLine(b);
        Console.Write("Zivot: ");
        Console.WriteLine(b.GrafickyZivot());
        if (b is Mag)
        {
                Console.Write("Mana:  ");
                Console.WriteLine(((Mag)b).GrafickaMana());
        }
}

Bojovníka jsme museli na mága přetypovat, abychom se k metodě GrafickaMana() dostali. Samotný Bojovnik ji totiž nemá. To bychom měli, VypisBojovnika budeme volat v metodě Vykresli(), která bude vypadat takto:

private void Vykresli()
{
        Console.Clear();
        Console.WriteLine("-------------- Aréna -------------- \n");
        Console.WriteLine("Bojovníci: \n");
        VypisBojovnika(bojovnik1);
        Console.WriteLine();
        VypisBojovnika(bojovnik2);
        Console.WriteLine();
}

Hotovo :)

Tahová hra aréna s mágem v C#

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 . Navíc jsem obarvil ukazatele pomocí barvy pozadí a popředí. 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:

Tahová hra aréna s mágem v C#

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. Příště si vysvětlíme pojem statika .


 

Stáhnout

Staženo 2010x (40.04 kB)
Aplikace je včetně zdrojových kódů v jazyce C#

 

  Aktivity (5)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Celkem (22 hlasů) :
4.818184.818184.818184.818184.81818


 


Miniatura
Předchozí článek
Dědičnost a polymorfismus
Miniatura
Následující článek
Cvičení k 5.-8. lekci OOP v C# .NET

 

 

Komentáře
Zobrazit starší komentáře (82)

Avatar
Patrik Bak
Člen
Avatar
Patrik Bak:

Ahojte. Z tej dedičnosti som dosť zmätený, prečo je toto prosím zle ?

    class Uzivatel
    {
        protected string meno;
        protected string heslo;

        public Uzivatel(string meno, string heslo)
        {
            this.meno = meno;
            this.heslo = heslo;
        }
    }

    class Admin : Uzivatel
    {
        public Admin(string meno, string heslo)
        {
            this.meno = meno;
            this.heslo = heslo;
        }
    }

Keď tam dám to base, tak to funguje, ale nechápem, prečo nefunguje toto. Napríklad tu:

https://msdn.microsoft.com/en-us/library/ms173149.aspx

je kód, kde base nie je a funguje to. Ako to prosím s tými konštruktormi je ?
 
Odpovědět 17.8.2015 4:40
Avatar
Odpovídá na Patrik Bak
Ondřej Štorc:

Co kdyby jsi měl v rodičovské třídě konstruktor s 50 parametry (což by asi značilo, že je v návrhu tý aplikace něco špatně...) a v poděděné třídě by jsi měl o jeden parametr navíc, psal by jsi se s všemi těmi přiřazování, nebo nějaký výpočty znova, když jsou úplně stejný? Asi ne. Proto je tady base.
P.S: Samozřejmě tu může být za tím ještě něco jiného (to ať mě kdyžtak opraví/doplní zkušenější programátoři :))

Editováno 17.8.2015 7:15
Odpovědět 17.8.2015 7:15
Život je příliš krátký na to, abychom bezpečně odebírali USB z počítače..
Avatar
Zdeněk Pavlátka
Tým ITnetwork
Avatar
Odpovídá na Patrik Bak
Zdeněk Pavlátka:

Nefunguje to proto, že když v rodiči máš pouze konstruktor s parametry, musíš ho v potomkovi zavolat. Pokud bys to chtěl takhle musel bys do předka přidat protected konstruktor bez parametrů.

class Uzivatel
{
    protected string meno;
    protected string heslo;

    public Uzivatel(string meno, string heslo)
    {
        this.meno = meno;
        this.heslo = heslo;
    }

    /**/ protected Uzivatel() { }
}

class Admin : Uzivatel
{
    public Admin(string meno, string heslo)
    {
        this.meno = meno;
        this.heslo = heslo;
    }
}

tohle by se ale dělat nemělo - co kdyby ho nějaký potomek použil a pak proměnným nenastavil hodnotu... Když už by to bylo takhle, měl by ten "prázdný" konstruktor nastavit proměnným výchozí hodnoty, což se v tomhle případě použít moc nedá.

Odpovědět  +1 17.8.2015 7:58
Kolik jazyků umíš, tolikrát jsi programátor.
Avatar
Jan Vargovský
Redaktor
Avatar
Odpovídá na Ondřej Štorc
Jan Vargovský:

Když dedis z nějaké třídy, tak při vytváření jdeš až k rodici, ktery je úplně nejvýš, u něj se zvola konstruktor a takhle se jde tou vetvi až ke třídě kterou chceš vytvořit. Tvůj případ porušuje zapouzdreni. Jeste k te inicializaci, kompilator musí zavolat nějaký konstruktor, v případě bezparametrickeho nemusíš nic explicitně definovat, zavola se sám, ale v případě parametrickeho musíš říct jaký se má zavolat a předat mu hodnoty. Proto tam musíš volat to base.

 
Odpovědět  +2 17.8.2015 7:58
Avatar
Michal Gros
Redaktor
Avatar
Michal Gros :

a co ti říká visual studio za chybu ?

Odpovědět 28.9.2015 23:39
Jestli jste dobří nahrnou na Vás spoustu práce. Jestli jste sakra dobří, tak se jí dokážete zbavit.
Avatar
Michal Gros
Redaktor
Avatar
Michal Gros :

Prosím jak najít na webu volbu zavináč před závorkou když chci vytvořit v konzoli předformátovaný text.

Když jsem do svého programu dával Ascii art dost jsem si s s tím zabojoval jsou i jiné fígle ?

Odpovědět 28.9.2015 23:47
Jestli jste dobří nahrnou na Vás spoustu práce. Jestli jste sakra dobří, tak se jí dokážete zbavit.
Avatar
Roman
Člen
Avatar
Roman:

Prosím Vás, ako sa dá zmeniť tá farba pozadia / popredia v kóde na ukazovateli ďakujem :)

 
Odpovědět 8. února 14:06
Avatar
ondra.exner
Člen
Avatar
ondra.exner:

Pomocí příkazů: Console.Backgrou­ndColor = ConsoleColor.BARVA, Console.Forge­roundColor = ConsoleColor.BARVA a Console.Reset­Color()

Tady je to dyžtak víc rozepsaný: http://www.dotnetperls.com/console-color

(Nebo si tu stáhni zdroják a podívej se:))

 
Odpovědět 21. července 18:09
Avatar
fenrir
Člen
Avatar
fenrir:

Ahoj.
Proč je gandalf deklarovaný jako datový typ Bojovnik a ne jako Mag? Podle toho jak tomu rozumím, nepřináší to nic než drobnou komplikaci v nutnosti přetypování v metodě VypisBojovnika, ale žádnou výhodu to nepřináší. Proč se teda nedeklaruje rovnou jako Mag?

 
Odpovědět 12. října 20:23
Avatar
pocitac770
Redaktor
Avatar
Odpovídá na fenrir
pocitac770:

To je podle me jenom ukazka, ze ho muzes do Bojovnika normalne ulozit, pro pochopení např pole, co v sobě bude mít např. Bojovníky, Mágy, Střelce... jen díky tomu, že bude pole typováno "obecně" na Bojovnika a tudíž se tam v tomto stavu budou ukládat, samozřejmě s vnitřní logikou svého vlastního typu.
A pokud jde o tu metodu "VypisBojovnika()", tam to ani jinak nemůžeš udělat, jako s tím polem. Musíš umět použít jakéhokoliv bojovníka s jakýmkoliv interním typem, takže ti přes parametr projde Bojovnik -> i jeho potomci, kdybys tam dal jenom Mag, tak by ti nešel vykreslit normální Bojovnik, protože by tě to přes parametr prostě nepustilo (-> jeden z principů OOP - znovupoužotelnost kódu, a může se jednat takto i o metodu v rámci jednoho projektu)

 
Odpovědět 12. října 22:53
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.

Zobrazeno 10 zpráv z 92. Zobrazit vše