Lekce 6 - C# - Aréna s bojovníky
V minulé lekci, Bojovník do arény, jsme si vytvořili třídu bojovníka.
Hrací kostku máme hotovou z prvních lekcí objektově orientovaného programování. Dnes tedy dáme vše dohromady a vytvoříme v C# .NET funkční arénu. Tutoriál bude spíše oddechový a pomůže nám zopakovat si práci s objekty.
Potřebujeme napsat nějaký kód pro obsluhu bojovníků a výpis zpráv
uživateli. Samozřejmě ho nebudeme psát rovnou do Program.cs
,
ale vytvoříme si objekt Arena
, kde se bude zápas odehrávat.
Program.cs
potom jen založí objekty a o zbytek se bude starat
právě objekt Arena
. Přidejme tedy k projektu poslední třídu,
a to Arena.cs
.
Třída bude víceméně jednoduchá, jako atributy bude obsahovat tři potřebné instance: dva bojovníky a hrací kostku. V konstruktoru se tyto atributy naplní z parametrů. Kód třídy bude tedy následující (komentáře si dopište):
class Arena { private Bojovnik bojovnik1; private Bojovnik bojovnik2; private Kostka kostka; public Arena(Bojovnik bojovnik1, Bojovnik bojovnik2, Kostka kostka) { this.bojovnik1 = bojovnik1; this.bojovnik2 = bojovnik2; this.kostka = kostka; } }
Zamysleme se nad metodami. Z veřejných metod bude určitě potřeba jen ta
k simulaci zápasu. Výstup programu na konzoli vytvoříme trochu na úrovni a
také umožníme třídě Arena
, aby přímo ke konzoli
přistupovala. Rozhodli jsme se, že výpis bude v kompetenci třídy, jelikož
se nám to zde vyplatí. Pokud by výpis prováděli i bojovníci, bylo by to
naopak na škodu (nebyli by univerzální). Potřebujeme tedy metodu, která
vykreslí obrazovku s aktuálními údaji o kole a životy bojovníků. Zprávy
o útoku a obraně budeme chtít vypisovat s dramatickou pauzou. Aby byl
výsledný efekt lepší, vytvoříme si pro takový typ zprávy ještě
pomocnou metodu. Začněme s vykreslením informační obrazovky:
private void Vykresli() { Console.Clear(); Console.WriteLine("-------------- Aréna -------------- \n"); Console.WriteLine("Zdraví bojovníků: \n"); Console.WriteLine("{0} {1}", bojovnik1, bojovnik1.GrafickyZivot()); Console.WriteLine("{0} {1}", bojovnik2, bojovnik2.GrafickyZivot()); }
Zde asi není co řešit. Budete-li chtít, můžete si ještě obrazovku
vyzdobit barevně. Díky smazání konzole pomocí Clear()
docílíme hezké informační obrazovky namísto klasického konzolového
textu, kde se plná obrazovka roluje dolů. Metoda je privátní, budeme ji
používat jen uvnitř třídy.
Další privátní metodou bude výpis zprávy s dramatickou pauzou:
private void VypisZpravu(string zprava) { Console.WriteLine(zprava); Thread.Sleep(500); }
Kód je zřejmý kromě třídy Thread
, která umožňuje práci
s vlákny. My z ní využijeme pouze metodu Sleep()
, která uspí
vlákno programu na daný počet milisekund. S vlákny budeme pracovat až na
konci kurzu. Aby vše fungovalo, musíme přidat
using System.Threading;
na začátek souboru
Arena.cs
.
Obě metody vlastně jen vypisují na konzoli, proto je nyní zbytečné je
zkoušet. Přesuneme se tedy již k samotnému zápasu. Metoda
Zapas()
nebude mít žádné parametry ani nebude nic vracet.
Uvnitř bude cyklus, který bude střídavě volat útoky bojovníků navzájem
a vypisovat informační obrazovku a zprávy. Metoda by mohla vypadat takto:
public void Zapas() { Console.WriteLine("Vítejte v aréně!"); Console.WriteLine("Dnes se utkají {0} s {1}! \n", bojovnik1, bojovnik2); Console.WriteLine("Zápas může začít..."); Console.ReadKey(); // cyklus s bojem while (bojovnik1.Nazivu() && bojovnik2.Nazivu()) { bojovnik1.Utoc(bojovnik2); Vykresli(); VypisZpravu(bojovnik1.VratPosledniZpravu()); // zpráva o útoku VypisZpravu(bojovnik2.VratPosledniZpravu()); // zpráva o obraně bojovnik2.Utoc(bojovnik1); Vykresli(); VypisZpravu(bojovnik2.VratPosledniZpravu()); // zpráva o útoku VypisZpravu(bojovnik1.VratPosledniZpravu()); // zpráva o obraně Console.WriteLine(); } }
Kód vypíše jednoduché informace a po stisku klávesy přejde do cyklu s
bojem. Jedná se o while
cyklus, který se opakuje, dokud jsou oba
bojovníci naživu. První bojovník zaútočí na druhého, jeho útok
vnitřně zavolá na druhém bojovníkovi obranu. Po útoku vykreslíme
obrazovku s informacemi a dále zprávy o útoku a obraně pomocí naší metody
VypisZpravu()
, která po výpisu udělá dramatickou pauzu. Totéž
provedeme i u druhého bojovníka.
Přesuňme se do Program.cs
, vytvořme patřičné instance a
zavolejme na aréně metodu Zapas()
:
{CSHARP_OOP} {CSHARP_MAIN_BLOCK} // vytvoření objektů Kostka kostka = new Kostka(10); Bojovnik zalgoren = new Bojovnik("Zalgoren", 100, 20, 10, kostka); Bojovnik shadow = new Bojovnik("Shadow", 60, 18, 15, kostka); Arena arena = new Arena(zalgoren, shadow, kostka); // zápas arena.Zapas(); Console.ReadKey(); {/CSHARP_MAIN_BLOCK} {/CSHARP_OOP}
{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}
using System.Threading; {CSHARP_OOP} class Arena { private Bojovnik bojovnik1; private Bojovnik bojovnik2; private Kostka kostka; public Arena(Bojovnik bojovnik1, Bojovnik bojovnik2, Kostka kostka) { this.bojovnik1 = bojovnik1; this.bojovnik2 = bojovnik2; this.kostka = kostka; } private void Vykresli() { Console.Clear(); Console.WriteLine("-------------- Aréna -------------- \n"); Console.WriteLine("Zdraví bojovníků: \n"); Console.WriteLine("{0} {1}", bojovnik1, bojovnik1.GrafickyZivot()); Console.WriteLine("{0} {1}", bojovnik2, bojovnik2.GrafickyZivot()); } private void VypisZpravu(string zprava) { Console.WriteLine(zprava); Thread.Sleep(500); } public void Zapas() { Console.WriteLine("Vítejte v aréně!"); Console.WriteLine("Dnes se utkají {0} s {1}! \n", bojovnik1, bojovnik2); Console.WriteLine("Zápas může začít..."); Console.ReadKey(); // cyklus s bojem while (bojovnik1.Nazivu() && bojovnik2.Nazivu()) { bojovnik1.Utoc(bojovnik2); Vykresli(); VypisZpravu(bojovnik1.VratPosledniZpravu()); // zpráva o útoku VypisZpravu(bojovnik2.VratPosledniZpravu()); // zpráva o obraně bojovnik2.Utoc(bojovnik1); Vykresli(); VypisZpravu(bojovnik2.VratPosledniZpravu()); // zpráva o útoku VypisZpravu(bojovnik1.VratPosledniZpravu()); // zpráva o obraně Console.WriteLine(); } } } {/CSHARP_OOP}
Charakteristiky hrdinů si můžete upravit dle libosti. Program spustíme:
Konzolová aplikace
-------------- Aréna --------------
Zdraví bojovníků:
Zalgoren [###### ]
Shadow [ ]
Shadow útočí úderem za 20 hp
Zalgoren utrpěl poškození 4 hp
Výsledek je docela působivý. Objekty spolu komunikují, grafický život ubývá, jak má, zážitek umocňuje dramatická pauza. Aréna má však dva nedostatky:
- V cyklu s bojem útočí první bojovník na druhého. Poté však vždy
útočí i druhý bojovník, nehledě na to, zda ho první nezabil. Může tedy
útočit již jako mrtvý. Podívejme se na výstup výše. Shadow útočil jako
poslední, přestože byl mrtvý. Až poté se vystoupilo z
while
cyklu. U prvního bojovníka tento problém není, u druhého musíme před útokem kontrolovat, zda je naživu. - Druhým nedostatkem je, že bojovníci vždy bojují ve stejném pořadí
čili "Zalgoren" v našem příkladu má vždy výhodu. Pojďme vnést do hry
další prvek náhody a pomocí kostky rozhodněme, který z bojovníků bude
začínat. Jelikož jsou bojovníci vždy dva, stačí hodit kostkou a podívat
se, zda padlo číslo menší nebo rovné polovině počtu stěn kostky.
Padne-li tedy např. na desetistěnné kostce číslo do pěti, začíná druhý
bojovník, jinak začíná první. Zbývá se zamyslet nad tím, jak do kódu
zanést prohazování bojovníků. Jistě by bylo velmi nepřehledné
opodmínkovat příkazy ve
while
cyklu. Jelikož již víme, že v C# fungují reference, není pro nás problém vytvořit si dvě proměnné, ve kterých budou instance bojovníků. Nazvěme je jednodušeb1
ab2
. Do těchto proměnných si na začátku dosadíme bojovníkybojovnik1
abojovnik2
tak, jak potřebujeme. Při pozitivním hodu kostkou tedy můžeme dosadit dob1
bojovník2
a naopak. Výsledkem bude, že začínat bude ten druhý. Kód cyklu se takto vůbec nezmění a zůstane stále přehledný a jednoduchý, jen místobojovnik
budeb
.
Změněná verze včetně podmínky, aby nemohl útočit mrtvý bojovník, by mohla vypadat nějak takto:
using System.Threading; {CSHARP_OOP} class Arena { private Bojovnik bojovnik1; private Bojovnik bojovnik2; private Kostka kostka; public Arena(Bojovnik bojovnik1, Bojovnik bojovnik2, Kostka kostka) { this.bojovnik1 = bojovnik1; this.bojovnik2 = bojovnik2; this.kostka = kostka; } private void Vykresli() { Console.Clear(); Console.WriteLine("-------------- Aréna -------------- \n"); Console.WriteLine("Zdraví bojovníků: \n"); Console.WriteLine("{0} {1}", bojovnik1, bojovnik1.GrafickyZivot()); Console.WriteLine("{0} {1}", bojovnik2, bojovnik2.GrafickyZivot()); } private void VypisZpravu(string zprava) { Console.WriteLine(zprava); Thread.Sleep(500); } public void Zapas() { // původní pořadí Bojovnik b1 = bojovnik1; Bojovnik b2 = bojovnik2; Console.WriteLine("Vítejte v aréně!"); Console.WriteLine("Dnes se utkají {0} s {1}! \n", bojovnik1, bojovnik2); // prohození bojovníků bool zacinaBojovnik2 = (kostka.Hod() <= kostka.VratPocetSten() / 2); if (zacinaBojovnik2) { b1 = bojovnik2; b2 = bojovnik1; } Console.WriteLine("Začínat bude bojovník {0}! \nZápas může začít...", b1); Console.ReadKey(); // cyklus s bojem while (b1.Nazivu() && b2.Nazivu()) { b1.Utoc(b2); Vykresli(); VypisZpravu(b1.VratPosledniZpravu()); // zpráva o útoku VypisZpravu(b2.VratPosledniZpravu()); // zpráva o obraně if (b2.Nazivu()) { b2.Utoc(b1); Vykresli(); VypisZpravu(b2.VratPosledniZpravu()); // zpráva o útoku VypisZpravu(b1.VratPosledniZpravu()); // zpráva o obraně } Console.WriteLine(); } } } {/CSHARP_OOP}
{CSHARP_OOP} {CSHARP_MAIN_BLOCK} // vytvoření objektů Kostka kostka = new Kostka(10); Bojovnik zalgoren = new Bojovnik("Zalgoren", 100, 20, 10, kostka); Bojovnik shadow = new Bojovnik("Shadow", 60, 18, 15, kostka); Arena arena = new Arena(zalgoren, shadow, kostka); // zápas arena.Zapas(); Console.ReadKey(); {/CSHARP_MAIN_BLOCK} {/CSHARP_OOP}
{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}
Program vyzkoušejme.
Konzolová aplikace
-------------- Aréna --------------
Zdraví bojovníků:
Zalgoren [######### ]
Shadow [ ]
Zalgoren útočí úderem za 27 hp
Shadow utrpěl poškození 11 hp a zemřel
Vidíme, že je vše již v pořádku. Gratulujeme vám! Jestliže jste se dostali až sem a tutoriály jste opravdu četli a pochopili, máte základy objektového programování a dokážete tvořit rozumné aplikace
V příští lekci, Dědičnost a polymorfismus, se podíváme na objektově orientované programování podrobněji. V úvodu jsme si říkali, že OOP stojí na třech pilířích: zapouzdření, dědičnost a polymorfismus. První umíme již velmi dobře a modifikátor private je nám známý. Další dva nás čekají příště.
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 1732x (145.25 kB)
Aplikace je včetně zdrojových kódů v jazyce C#