5. díl - Bojovník do arény

C# .NET Objektově orientované programování Bojovník do arény American English version English version

V minulém dílu seriálu o C# jsme si vysvětlili rozdíly mezi referenčními a hodnotovými datovými typy. Již tedy víme, jak fungují reference a jak můžeme s objekty zacházet. Bude se nám to hodit dnes i příště. Tento a příští tutoriál budou totiž věnovány dokončení naší arény. Hrací kostku již máme, ještě nám chybí další 2 objekty: bojovník a samotná aréna. Dnes se budeme věnovat bojovníkovi. Nejprve si popišme, co má bojovník umět, poté se pustíme do psaní kódu.

Atributy

Bojovník se bude nějak jmenovat a bude mít určitý počet hp (tedy života, např. 80hp). Budeme uchovávat jeho maximální život (bude se lišit u každé instance) a jeho současný život, tedy např. zraněný bojovník bude mít 40hp z 80ti. Bojovník má určitý útok a obranu, obojí vyjádřené opět v hp. Když bojovník útočí s útokem 20hp na druhého bojovníka s obranou 10hp, ubere mu 10hp života. Bojovník bude mít referenci na instanci objektu Kostka. Při útoku či obraně si vždy hodí kostkou a k útoku/obraně přičte padlé číslo. (Samozřejmě by mohl mít každý bojovník svou kostku, ale chtěl jsem se přiblížit stolní podobě hry a ukázat, jak OOP opravdu simuluje realitu. Bojovníci tedy budou sdílet jednu instanci kostky.) Kostkou dodáme hře prvek náhody, v realitě se jedná vlastně o štěstí, jak se útok nebo obrana vydaří. Konečně budeme chtít, aby bojovníci podávali zprávy o tom, co se děje, protože jinak by z toho uživatel nic neměl. Zpráva bude vypadat např. "Zalgoren útočí s úderem za 25hp.". Zprávami se zatím nebudeme zatěžovat a vrátíme se k nim až nakonec.

Již víme, co budeme dělat, pojďme na to! :) K projektu aréna si přidejme třídu Bojovnik a dodejme ji patřičné atributy. Všechny budou privátní:

class Bojovnik
{
        /// <summary>
        /// Jméno bojovníka
        /// </summary>
        private string jmeno;
        /// <summary>
        /// Život v HP
        /// </summary>
        private int zivot;
        /// <summary>
        /// Maximální zdraví
        /// </summary>
        private int maxZivot;
        /// <summary>
        /// Útok v HP
        /// </summary>
        private int utok;
        /// <summary>
        /// Obrana v HP
        /// </summary>
        private int obrana;
        /// <summary>
        /// Instance hrací kostky
        /// </summary>
        private Kostka kostka;

}

Komentáře můžete sklapnout, aby nezabíraly zbytečné místo. Nicméně je velmi dobrý nápad je k atributům psát. Třída Kostka musí samozřejmě být v našem projektu.

Metody

Pojďme pro atributy vytvořit konstruktor, nebude to nic těžkého. Komentáře zde vynechám, vy si je dopište podobně, jako u atributů výše. Nebudu je psát ani u dalších metod, aby se tutoriál zbytečně neroztahoval a zůstal přehledný.

public Bojovnik(string jmeno, int zivot, int utok, int obrana, Kostka kostka)
{
        this.jmeno = jmeno;
        this.zivot = zivot;
        this.maxZivot = zivot;
        this.utok = utok;
        this.obrana = obrana;
        this.kostka = kostka;
}

Všimněte si, že maximální zdraví si v konstruktoru odvodíme a nemáme na něj parametr v hlavičce metody. Předpokládáme, že bojovník je při vytvoření plně zdravý, stačí nám tedy znát pouze jeho život a maximální život bude stejný.

Přejděme k metodám, opět se nejprve zamysleme nad tím, co by měl bojovník umět. Začněme tím jednodušším, budeme chtít nějakou textovou reprezentaci, abychom mohli bojovníka vypsat. Překryjeme tedy metodu ToString(), která vrátí jméno bojovníka. Určitě se nám bude hodit metoda, vracející zda je bojovník naživu (tedy typu bool). Aby to bylo trochu zajímavější, budeme chtít kreslit život bojovníka do konzole, nebudeme tedy psát, kolik má života, ale "vykreslíme" ho takto:

[#########    ]

Výše uvedený život by odpovídal asi 70%. Dosud zmíněné metody nepotřebovaly žádné parametry. Samotný útok a obranu nechme na později a pojďme si implementovat ToString(), Nazivu() a GrafickyZivot(). Začněme s ToString(), tam není co vymýšlet:

public override string ToString()
{
        return jmeno;
}

Nyní implementujme metodu Nazivu(), opět to nebude nic těžkého. Stačí zkontrolovat, zda je život větší než 0 a podle toho se zachovat. Mohli bychom ji napsat třeba takto:

public bool Nazivu()
{
        if (zivot > 0)
                return true;
        else
                return false;
}

Jelikož i samotný výraz (zivot > 0) je vlastně logická hodnota, můžeme vrátit tu a kód se značně zjednodušší:

public bool Nazivu()
{
        return (zivot > 0);
}

Grafický život

Jak jsem se již zmínil, metoda GrafickyZivot() bude umožňovat vykreslit ukazatel života v grafické podobě. Již víme, že z hlediska objektového návrhu není vhodné, aby metoda objektu přímo vypisovala do konzole (pokud není k výpisu objekt určený), proto si znaky uložíme do řetězce a ten vrátíme pro pozdější vypsání. Ukážeme si kód metody a následně podrobně popíšeme:

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;
}

Připravíme si řetězec s a vložíme do něj úvodní znak "[". Určíme si celkovou délku ukazatele života do proměnné celkem (např. 20). Nyní v podstatě nepotřebujeme nic jiného, než trojčlenku. Pokud maxZivot odpovídá celkem dílků, zivot bude odpovídat pocet dílkům. pocet je proměnná s počtem dílků aktuálního zdraví.

Matematicky platí, že pocet = (zivot / maxZivot) * celkem;. My ještě doplníme zaokrouhlení na celé dílky a také přetypování jednoho z operandů na double, aby C# chápal dělení jako neceločíselné.

Měli bychom ošetřit případ, kdy je život tak nízký, že nám vyjde na 0 dílků, ale bojovník je stále naživu. V tom případě vykreslíme 1 dílek, jinak by to vypadalo, že je již mrtvý.

Dále stačí jednoduše for cyklem připojit k řetězci s patřičný počet znaků a doplnit je mezerami do celkové délky. Doplnění provedeme pomocí PadRight() na délku celkem + 1, kde ten znak navíc je úvodní znak "[". Přidáme koncový znak a řetězec vrátíme.

Vše si vyzkoušíme, přejděme do Program.cs a vytvořme si bojovníka (a kostku, protože tu musíme konstruktoru bojovníka předat). Následně vypišme, zda je naživu a jeho život graficky:

Kostka kostka = new Kostka(10);
Bojovnik bojovnik = new Bojovnik("Zalgoren", 100, 20, 10, kostka);

Console.WriteLine("Bojovník: {0}", bojovnik); // test ToString();
Console.WriteLine("Naživu: {0}", bojovnik.Nazivu()); // test Nazivu();
Console.WriteLine("Život: {0}", bojovnik.GrafickyZivot()); // test GrafickyZivot();
Stav bojovníka v C#

Boj

Dostáváme se k samotnému boji. Implementujeme metody pro útok a obranu.

Obrana

Začněme obranou. Metoda BranSe() bude umožňovat bránit se úderu, jehož síla bude předána metodě jako parametr. Metodu si opět ukážeme a poté popíšeme:

public void BranSe(int uder)
{
        int zraneni = uder - (obrana + kostka.hod());
        if (zraneni > 0)
        {
                zivot -= zraneni;
                if (zivot <= 0)
                {
                        zivot = 0;
                }
        }
}

Nejprve spočítáme skutečné zranění a to tak, že z útoku nepřítele odečteme naši obranu zvýšenou o číslo, které padlo na hrací kostce. Pokud jsme zranění celé neodrazili (zraneni > 0), budeme snižovat náš život. Tato podmínka je důležitá, kdybychom zranění odrazili a bylo např. -2, bez podmínky by se život zvýšil. Po snížení života zkontrolujeme, zda není v záporné hodnotě a případně ho dorovnáme na nulu.

Útok

Metoda Utoc() bude brát jako parametr instanci bojovníka, na kterého se útočí. To proto, abychom na něm mohli zavolat metodu BranSe(), která na náš útok zareaguje a zmenší protivníkův život. Zde vidíme výhody referencí v C#, můžeme si instance jednoduše předávat a volat na nich metody, aniž by došlo k jejich zkopírování. Jako první vypočteme úder, podobně jako při obraně, úder bude náš útok + hodnota z hrací kostky. Na soupeři následně zavoláme metodu BranSe() s hodnotou úderu:

public void Utoc(Bojovnik souper)
{
        int uder = utok + kostka.hod();
        souper.BranSe(uder);
}

To bychom měli, pojďme si zkusit v našem ukázkovém programu zaútočit a poté znovu vykreslit život. Pro jednoduchost nemusíme zakládat dalšího bojovníka, ale můžeme zaútočit sami na sebe:

Kostka kostka = new Kostka(10);
Bojovnik bojovnik = new Bojovnik("Zalgoren", 100, 20, 10, kostka);

Console.WriteLine("Bojovník: {0}", bojovnik); // test ToString();
Console.WriteLine("Naživu: {0}", bojovnik.Nazivu()); // test Nazivu();
Console.WriteLine("Život: {0}", bojovnik.GrafickyZivot()); // test GrafickyZivot();

bojovnik.Utoc(bojovnik); // test útoku
Console.WriteLine("Život po útoku: {0}", bojovnik.GrafickyZivot());
Stav bojovníka v C#

Zdá se, že vše funguje, jak má. Přejděme k poslednímu bodu dnešního tutoriálu a to ke zprávám.

Zprávy

Jak již bylo řečeno, o útocích a obraně budeme uživatele informovat výpisem na konzoli. Výpis nebude provádět samotná třída Bojovnik, ta bude jen vracet zprávy jako textové řetězce. Jedna možnost by byla nastavit návratový typ metod Utoc() a BranSe() na string a při jejich volání vrátit i zprávu. Problém by však nastal v případě, když bychom chtěli získat zprávu od metody, která již něco vrací. Metoda samozřejmě nemůže jednoduše vrátit 2 věci.

Pojďme na věc univerzálněji, zprávu budeme ukládat do privátní proměnné zprava a uděláme metody pro její uložení a navrácení. Samozřejmě bychom mohli udělat proměnnou veřejnou, ale není zde důvod, proč umožnit zvenčí zápis do zprávy a také by skládání složitější zprávy uvnitř třídy mohlo být někdy problematické.

K atributům třídy tedy přidáme:

private string zprava;

Nyní si vytvoříme dvě metody. Privátní NastavZpravu(), která bere jako parametr text zprávy a slouží pro vnitřní účely třídy, kde nastaví zprávu do privátní proměnné:

private void NastavZpravu(string zprava)
{
        this.zprava = zprava;
}

Nic složitého. Podobně jednoduchá bude veřejná metoda pro navrácení zprávy:

public string VratPosledniZpravu()
{
        return zprava;
}

O práci se zprávami obohatíme naše metody Utoc() a BranSe(), nyní budou vypadat takto:

public void Utoc(Bojovnik souper)
{
        int uder = utok + kostka.hod();
        NastavZpravu(String.Format("{0} útočí s úderem za {1} hp", jmeno, uder));
        souper.BranSe(uder);
}

public void BranSe(int uder)
{
        int zraneni = uder - (obrana + kostka.hod());
        if (zraneni > 0)
        {
                zivot -= zraneni;
                zprava = String.Format("{0} utrpěl poškození {1} hp", jmeno, zraneni);
                if (zivot <= 0)
                {
                        zivot = 0;
                        zprava += " a zemřel";
                }

        } else
                zprava = String.Format("{0} odrazil útok", jmeno);
        NastavZpravu(zprava);
}

Vše si opět vyzkoušíme, tentokrát již vytvoříme druhého bojovníka:

Kostka kostka = new Kostka(10);
Bojovnik bojovnik = new Bojovnik("Zalgoren", 100, 20, 10, kostka);

Console.WriteLine("Život: {0}", bojovnik.GrafickyZivot()); // test GrafickyZivot();

// útok na našeho bojovníka
Bojovnik souper = new Bojovnik("Shadow", 60, 18, 15, kostka);
souper.Utoc(bojovnik);
Console.WriteLine(souper.VratPosledniZpravu());
Console.WriteLine(bojovnik.VratPosledniZpravu());

Console.WriteLine("Život: {0}", bojovnik.GrafickyZivot());

Console.ReadKey();
Stav bojovníka v C#

Máme kostku i bojovníka, teď již chybí jen aréna, tu si vytvoříme hned příště.


 

Stáhnout

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

 

  Aktivity (3)

Č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.954544.954544.954544.954544.95454


 


Miniatura
Předchozí článek
Cvičení k 4. lekci OOP v C# .NET
Miniatura
Následující článek
C# - Aréna s bojovníky

 

 

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

Avatar
Lukáš Hypša:

Proč nejde jenom:

                           NastavZpravu("{0} útočí za {1} hp", jmeno, uder);
místo
                           NastavZpravu(String.Format("{0} útočí za {1} hp", jmeno, uder));

]
?

Editováno 28. ledna 17:33
Odpovědět 28. ledna 17:32
I když se programování učím jenom z interetu, velmi mě baví a doufám, že se tím jednou budu i živit.
Avatar
Lukas C#
Redaktor
Avatar
Odpovídá na Lukáš Hypša
Lukas C#:

Protože metoda NastavZpravu bere jako parametr string, a tohleto - "{0} útoří za {1} hp", jmeno, uder - samozřejmě text není. je to text a dvě nějaké proměnné. metoda String.Format takový výraz zpracuje, a na místa se závorkami dosadí textovou reprezentaci těch proměnných co jsi uvedl. Mohlo tě zmást, že Console.Write() umí takový výraz přechroustat i bez String.Format - pokud vím, tak jakmile jí toto předáš, tak String.Format vnitřně zavolá.

 
Odpovědět  +1 28. ledna 18:13
Avatar
Jakub Dvorský:

Zdravím, chtěl bych se zeptat jestli můžu nějak upravit GrafickyZivot() tak aby to míst odečítání přičítalo. Děkuji.

 
Odpovědět  +1 24. května 19:19
Avatar
krepsy3
Redaktor
Avatar
Odpovídá na Jakub Dvorský
krepsy3:

Myslíš nějak takto?:

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 < (celkem - pocet); i++)
                s += "#";
        s = s.PadLeft(celkem + 1);
        s += "]";
        return s;
}

Jinými slovy od leva přibývá počet (#) s rostoucím poškozením, tedy při dvaceti nastane smrt bojovníka - jinými slovy zbývající život v metodě akorát odečteme od celkového počtu políček (z 20), čímž vlastně výstup invertujeme

Odpovědět  +1 24. května 20:21
Programátor je stroj k převodu kávy na kód.
Avatar
 
Odpovědět  +1 24. května 22:13
Avatar
krepsy3
Redaktor
Avatar
Odpovědět  +1 25. května 14:04
Programátor je stroj k převodu kávy na kód.
Avatar
huty
Člen
Avatar
huty:

Moc nechápu funkci metody NastavZpravu(string zprava). podle mě je to zbytečný, stejně ta zpráva už je nastavená v proměnné zpráva a neni třeba ji znovu předávat proměnné zpráva... Funguje mi to i bez téhle metody.

Odpovědět 14. července 18:43
obecně je lepší používat více proměnných a dodržovat přehlednost, než psát co nejkratší kód a po měsíci zapomenout, j...
Avatar
krepsy3
Redaktor
Avatar
Odpovídá na huty
krepsy3:

Ano, David to i zmiňuje. Je to kvůli správnosti komunikace objektů.

Odpovědět  +1 14. července 21:11
Programátor je stroj k převodu kávy na kód.
Avatar
Odpovídá na krepsy3
Petr Lipovský:

Souhlasím s hutym. David zmiňuje nepoužití public proměnné. Ale co se týče komunikace mezi objekty. Metoda NastavZpravu není komunikace mezi objekty. Vždy ji nastavuji ve stejném objektu! Komunikace mezi objekty je metoda Utoc(souper), kde jeden objekt/bojovnik předá sílu úderu druhému objektu. Význam NastavZpravu mi uniká. Jedině by se to muselo upravit(napsat jinak) tak, aby i druhý bojovník, přepsal nebo upravil zprávu toho prvního popř. naopak. Jestli to vůbec jde.
(Spíše by to musel poslat jako žádost/(parametr funkce) tomu druhému (podobně jako silu uderu)?
Nebo se mýlím?
Ale jinak je celý Váš web zajímavý a užitečný. Díky

 
Odpovědět  +2 31. července 19:09
Avatar
krepsy3
Redaktor
Avatar
Odpovídá na Petr Lipovský
krepsy3:

Ano, metoda je defakto zbytečná. Ale nebylo by asi úplně hezké, aby konkrétní instance bojovníka měnila ve své metodě přímo univerzální zprávu "string zprava". Je fakt, že z kódového hlediska by to mělo smysl v momentě, kdy by byla zprava staticky vázaná na třídu, čili:

private static string zprava;

private void NastavZpravu(string zprava)
{
        this.zprava = zprava;
}

//...

public void Utoc(Bojovnik souper)
{
        int uder = utok + kostka.hod();
        NastavZpravu(String.Format("{0} útočí s úderem za {1} hp", jmeno, uder));
        souper.BranSe(uder);
}

public void BranSe(int uder)
{
        int zraneni = uder - (obrana + kostka.hod());
        if (zraneni > 0)
        {
                zivot -= zraneni;
                zprava = String.Format("{0} utrpěl poškození {1} hp", jmeno, zraneni);
                if (zivot <= 0)
                {
                        zivot = 0;
                        zprava += " a zemřel";
                }

        } else
                zprava = String.Format("{0} odrazil útok", jmeno);
        NastavZpravu(zprava);
}
Editováno 31. července 20:10
Odpovědět  +1 31. července 20:09
Programátor je stroj k převodu kávy na kód.
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 39. Zobrazit vše