Lekce 5 - Bojovník do arény
V předešlém cvičení, Řešené úlohy ke 4. lekci OOP v C# .NET, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
Z minulé lekce již víme, jak fungují reference a jak můžeme s objekty zacházet. Bude se nám to hodit v dnešním i příštím C# .NET tutoriálu, neboť se budeme věnovat dokončení naší arény. Hrací kostku již máme, ale ještě nám chybí další dva objekty: bojovník a samotná aréna. Dnes se budeme věnovat bojovníkovi. Nejprve si popíšeme, co má bojovník umět, a 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ř. 80 hp). Budeme uchovávat jeho
maximální život (bude se lišit u každé instance) a jeho
současný život, zraněný bojovník tedy bude mít např. 40
hp z 80 hp. Bojovník má určitý útok a
obranu, obojí vyjádřené opět v hp. Když bojovník
útočí silou 20 hp na druhého bojovníka s obranou 10 hp, útok napadenému
ubere 10 hp života. Bojovník bude mít referenci na instanci třídy
Kostka
. Při útoku či obraně si vždy hodí kostkou a k
útoku/obraně přičte padlé číslo. (Každý bojovník by sice mohl mít
svou kostku, ale chtěli jsme 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 ze hry uživatel nic neměl. Zpráva bude vypadat např.: "Zalgoren útočí
s úderem za 25 hp." Zprávami se zatím nebudeme zatěžovat a vrátíme se k
nim až na konci tutoriálu.
Již víme, co budeme dělat, pojďme tedy na to! K projektu Arena
si
přidejme třídu Bojovnik
a dodejme jí 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ůžeme sklapnout, aby zbytečně nezabíraly místo. Přesto 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áme, vy si je však dopište podobně jako u atributů výše. Nebudeme je psát ani u dalších metod, aby se tutoriál zbytečně nerozpínal 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ěme 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í bojovníkovo jméno. Určitě se nám
bude hodit metoda vracející to, 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 uvádět, kolik má bojovník
života, ale "vykreslíme" ho nározně 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
složitého. Stačí zkontrolovat, zda je život větší než 0, a podle toho
se zachovat. Metodu bychom mohli 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 značně zjednodušit:
public bool Nazivu() { return (zivot > 0); }
Grafický život
Jak jsme již zmínili, 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í objekt k výpisu 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ě si jej 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ů. Proměnná pocet
obsahuje počet
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 jazyk C# chápal dělení jako
neceločíselné.
Měli bychom také 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
bojovník 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 onen znak navíc představuje ú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 to, zda je bojovník naživu, a jeho grafický život:
{CSHARP_CONSOLE} 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(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Kostka { private Random random; private int pocetSten; public Kostka() { pocetSten = 6; random = new Random(); } public Kostka(int pocetSten) { this.pocetSten = pocetSten; random = new Random(); } public int VratPocetSten() { return pocetSten; } public int hod() { return random.Next(1, pocetSten + 1); } public override string ToString() { return String.Format("Kostka s {0} stěnami", pocetSten); } } {/CSHARP_OOP}
{CSHARP_OOP} class Bojovnik { private string jmeno; private int zivot; private int maxZivot; private int utok; private int obrana; private Kostka kostka; 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; } public bool Nazivu() { return (zivot > 0); } 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; } public override string ToString() { return jmeno; } } {/CSHARP_OOP}
Konzolová aplikace
Bojovník: Zalgoren
Naživu: True
Život: [####################]
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
nejprve 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 totiž
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ší život
bránícího se. Zde vidíme výhody referencí v C#. Můžeme si instance
jednoduše předávat a volat na nich metody, aniž dojde k jejich
zkopírování. Jako první vypočteme úder. Podobně jako při obraně se bude
úder skládat z našeho útoku + hodnoty 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); }
Pojďme si nyní 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:
{CSHARP_CONSOLE} 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()); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Kostka { private Random random; private int pocetSten; public Kostka() { pocetSten = 6; random = new Random(); } public Kostka(int pocetSten) { this.pocetSten = pocetSten; random = new Random(); } public int VratPocetSten() { return pocetSten; } public int hod() { return random.Next(1, pocetSten + 1); } public override string ToString() { return String.Format("Kostka s {0} stěnami", pocetSten); } } {/CSHARP_OOP}
{CSHARP_OOP} class Bojovnik { private string jmeno; private int zivot; private int maxZivot; private int utok; private int obrana; private Kostka kostka; 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; } public bool Nazivu() { return (zivot > 0); } 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; } public void BranSe(int uder) { int zraneni = uder - (obrana + kostka.hod()); if (zraneni > 0) { zivot -= zraneni; if (zivot <= 0) { zivot = 0; } } } public void Utoc(Bojovnik souper) { int uder = utok + kostka.hod(); souper.BranSe(uder); } public override string ToString() { return jmeno; } } {/CSHARP_OOP}
Konzolová aplikace
Bojovník: Zalgoren
Naživu: True
Život: [####################]
Život po útoku: [################## ]
Zdá se, že vše funguje, jak má. Přejděme k poslednímu bodu dnešního tutoriálu, a sice 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 pouze vracet zprávy jako textové řetězce.
Jednou z možností by bylo 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ě, pokud bychom chtěli získat
zprávu od metody, která již něco vrací. Metoda samozřejmě nemůže
jednoduše vrátit dvě věci.
Vyřešme tedy problém univerzálněji – zprávu budeme ukládat do
privátní proměnné zprava
a vytvoříme metody pro její
uložení a navrácení. Samozřejmě bychom mohli proměnnou udělat veřejnou,
ale není zde důvod, proč umožnit zápis do zprávy zvenčí. Také
skládání složitější zprávy uvnitř třídy by mohlo být někdy
problematické.
K atributům třídy tedy přidáme:
private string zprava;
Nyní si vytvoříme dvě metody. Nejprve privátní
NastavZpravu()
, která bere jako parametr text zprávy a slouží
ke vnitřním účelům 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 i druhého bojovníka:
{CSHARP_CONSOLE} 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(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Kostka { private Random random; private int pocetSten; public Kostka() { pocetSten = 6; random = new Random(); } public Kostka(int pocetSten) { this.pocetSten = pocetSten; random = new Random(); } public int VratPocetSten() { return pocetSten; } public int hod() { return random.Next(1, pocetSten + 1); } public override string ToString() { return String.Format("Kostka s {0} stěnami", pocetSten); } } {/CSHARP_OOP}
{CSHARP_OOP} class Bojovnik { private string jmeno; private int zivot; private int maxZivot; private int utok; private int obrana; private Kostka kostka; private string zprava; 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; } public bool Nazivu() { return (zivot > 0); } 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; } 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); } private void NastavZpravu(string zprava) { this.zprava = zprava; } public string VratPosledniZpravu() { return zprava; } public override string ToString() { return jmeno; } } {/CSHARP_OOP}
Konzolová aplikace
Život: [####################]
Shadow útočí s úderem za 24 hp
Zalgoren utrpěl poškození 10 hp
Život: [################## ]
Máme kostku i bojovníka, nyní již chybí jen aréna.
Tu si vytvoříme hned v příští lekci, C# - Aréna s bojovníky.
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 2188x (33.07 kB)
Aplikace je včetně zdrojových kódů v jazyce C#