5. diel - Bojovník do arény
V predchádzajúcom cvičení, Riešené úlohy k 4. lekcii OOP v C# .NET, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
Už vieme, ako fungujú referencie a ako môžeme s objektmi zaobchádzať. Bude sa nám to hodiť dnes aj nabudúce. Tento a budúci C# .NET tutoriál budú totiž venované dokončeniu našej arény. Hraciu kocku už máme, ešte nám chýbajú ďalšie dva objekty: bojovník a samotná aréna. Dnes sa budeme venovať bojovníkovi. Najprv si popíšme, čo má bojovník vedieť, potom sa pustíme do písania kódu.
Atribúty
Bojovník sa bude nejako volať a bude mať určitý počet
hp (teda života, napr. 80 hp). Budeme uchovávať jeho
maximálny život (bude sa líšiť pri každej inštancii) a
jeho súčasný život, teda napr. zranený bojovník bude mať
40 hp z 80 hp. Bojovník má určitý útok a
obranu, oboje vyjadrené opäť v hp. Keď bojovník útočí s
útokom 20 hp na druhého bojovníka s obranou 10 hp, uberie mu 10 hp života.
Bojovník bude mať referenciu na inštancii triedy
RollingDie
. Pri útoku či obrane si vždy hodí kockou a
k útoku/obrane pripočíta padnuté číslo. (Samozrejme by mohol mať každý
bojovník svoju kocku, ale chcel som sa priblížiť stolovej podobe hry a
ukázať, ako OOP naozaj simuluje realitu. Bojovníci teda budú zdieľať jednu
inštanciu kocky.) Kockou dodáme hre prvok náhody, v realite sa jedná vlastne
o šťastie, ako sa útok alebo obrana vydarí. Napokon budeme chcieť, aby
bojovníci podávali správy o tom, čo sa deje, pretože inak
by z toho užívateľ nič nemal. Správa bude vyzerať napr. "Zalgoren attacks
with a hit worth 25 hp.". Správami sa zatiaľ nebudeme zaťažovať a vrátime
sa k nim až nakoniec.
Už vieme, čo budeme robiť, poďme na to! K projektu Arena
si
pridajme triedu Warrior
a dodajme jej patričné atribúty. Všetky
budú privátne:
class Warrior { /// <summary> /// Warrior's name /// </summary> private string name; /// <summary> /// Health in HP /// </summary> private int health; /// <summary> /// Maximum health in HP /// </summary> private int maxHealth; /// <summary> /// Damage in HP /// </summary> private int damage; /// <summary> /// Defense in HP /// </summary> private int defense; /// <summary> /// The rolling die instance /// </summary> private RollingDie die; }
Komentáre môžete sklapnúť, aby nezaberali zbytočné miesto. Avšak je
veľmi dobrý nápad ich k atribútom písať. Trieda RollingDie
musí samozrejme byť v našom projekte.
Metódy
Poďme pre atribúty vytvoriť konštruktor, nebude to nič ťažké. Komentáre tu vynecháme, vy si ich dopíšte podobne, ako u atribútov vyššie. Nebudeme ich písať ani pri ďalších metódach, aby sa tutoriál zbytočne nerozťahoval a zostal prehľadný:
public Warrior(string name, int health, int damage, int defense, RollingDie die) { this.name = name; this.health = health; this.maxHealth = health; this.damage = damage; this.defense = defense; this.die = die; }
Všimnime si, že maximálne zdravie si v konštruktore odvodíme a nemáme naň parameter v hlavičke metódy. Predpokladáme, že bojovník je pri vytvorení plne zdravý, stačí nám teda poznať iba jeho život a maximálny život bude rovnaký.
Prejdime k metódam, opäť sa najskôr zamyslime nad tým, čo by mal
bojovník vedieť. Začnime tým jednoduchším, budeme chcieť nejakú textovú
reprezentáciu, aby sme mohli bojovníka vypísať. Prekryjeme teda metódu
ToString()
, ktorá vráti meno bojovníka. Určite sa nám bude
hodiť metóda, vracajúca či je bojovník nažive (teda typu
bool
). Aby to bolo trochu zaujímavejšie, budeme chcieť kresliť
život bojovníka do konzoly, nebudeme teda písať, koľko má života, ale
"vykreslíme" ho takto:
[######### ]
Vyššie uvedený život by zodpovedal asi 70 %. Doteraz spomínané metódy
nepotrebovali žiadne parametre. Samotný útok a obranu nechajme na neskôr a
poďme si implementovať ToString()
, Alive()
a
HealthBar()
. Začnime s ToString()
, tam nie je čo
vymýšľať:
public override string ToString() { return name; }
Teraz implementujme metódu Alive()
, opäť to nebude nič
ťažké. Stačí skontrolovať, či je život väčší ako 0 a podľa toho sa
zachovať. Mohli by sme ju napísať napríklad takto:
public bool Alive() { if (health > 0) return true; else return false; }
Keďže aj samotný výraz (health > 0)
je vlastne logická
hodnota, môžeme vrátiť tú a kód sa značne zjednoduší:
public bool Alive() { return (health > 0); }
Grafický život
Ako som sa už zmienil, metóda HealthBar()
bude umožňovať
vykresliť ukazovateľ života v grafickej podobe. Už vieme, že z hľadiska
objektového návrhu nie je vhodné, aby metóda objektu priamo vypisovala do
konzoly (pokiaľ nie je na výpis objekt určený), preto si znaky uložíme do
reťazca a ten vrátime pre neskoršie vypísanie. Ukážeme si kód metódy a
následne podrobne popíšeme:
public string HealthBar() { string s = "["; int total = 20; double count = Math.Round(((double)health / maxHealth) * total); if ((count == 0) && (Alive())) count = 1; for (int i = 0; i < count; i++) s += "#"; s = s.PadRight(total + 1); s += "]"; return s; }
Pripravíme si reťazec s
a vložíme doň úvodný znak
[
. Určíme si celkovú dĺžku ukazovateľa života do premennej
total
(napr. 20
). Teraz v podstate nepotrebujeme nič
iné, než trojčlenku. Pokiaľ maxHealth
zodpovedá
total
dielikom, health
bude zodpovedať
count
dielikom. Premenná count
obsahuje počet
dielikov aktuálneho zdravia.
Matematicky platí, že count = (health / maxHealth) * total
. My
ešte doplníme zaokrúhlenie na celé dieliky a tiež pretypovanie jedného z
operandov na double
, aby C# chápal delenie ako
neceločíselné.
Mali by sme ošetriť prípad, keď je život taký nízky, že nám vyjde na
0
dielikov, ale bojovník je stále nažive. V tom prípade
vykreslíme 1
dielik, inak by to vyzeralo, že je už mŕtvy.
Ďalej stačí jednoducho for
cyklom pripojiť k reťazcu
s
patričný počet znakov a doplniť ich medzerami do celkovej
dĺžky. Doplnenie vykonáme pomocou PadRight()
na dĺžku
total + 1
, kde ten znak navyše je úvodný znak [
.
Pridáme koncový znak a reťazec vrátime.
Všetko si vyskúšame, prejdime do Program.cs
a vytvorme si
bojovníka (a kocku, pretože tu musíme konštruktoru bojovníka odovzdať).
Následne vypíšme, či je nažive a jeho život graficky:
{CSHARP_CONSOLE} RollingDie die = new RollingDie(10); Warrior warrior = new Warrior("Zalgoren", 100, 20, 10, die); Console.WriteLine("Warrior: {0}", warrior); // test ToString(); Console.WriteLine("Alive: {0}", warrior.Alive()); // test Alive(); Console.WriteLine("Health: {0}", warrior.HealthBar()); // test HealthBar(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class RollingDie { private Random random; private int sidesCount; public RollingDie() { sidesCount = 6; random = new Random(); } public RollingDie(int sidesCount) { this.sidesCount = sidesCount; random = new Random(); } public int GetSidesCount() { return sidesCount; } public int Roll() { return random.Next(1, sidesCount + 1); } public override string ToString() { return String.Format("Rolling die with {0} sides", sidesCount); } } {/CSHARP_OOP}
{CSHARP_OOP} class Warrior { private string name; private int health; private int maxHealth; private int damage; private int defense; private RollingDie die; public Warrior(string name, int health, int damage, int defense, RollingDie die) { this.name = name; this.health = health; this.maxHealth = health; this.damage = damage; this.defense = defense; this.die = die; } public override string ToString() { return name; } public bool Alive() { return (health > 0); } public string HealthBar() { string s = "["; int total = 20; double count = Math.Round(((double)health / maxHealth) * total); if ((count == 0) && (Alive())) count = 1; for (int i = 0; i < count; i++) s += "#"; s = s.PadRight(total + 1); s += "]"; return s; } } {/CSHARP_OOP}
Konzolová aplikácia
Warrior: Zalgoren
Alive: True
Health: [####################]
Boj
Dostávame sa k samotnému boju. Implementujeme metódy na útok a obranu.
Obrana
Začnime obranou. Metóda Defend()
bude umožňovať brániť sa
úderu, ktorého sila bude odovzdaná metóde ako parameter. Metódu si opäť
ukážeme a potom popíšeme:
public void Defend(int hit) { int injury = hit - (defense + die.Roll()); if (injury > 0) { health -= injury; if (health <= 0) { health = 0; } } }
Najprv spočítame skutočné zranenie a to tak, že z útoku nepriateľa
odpočítame našu obranu zvýšenú o číslo, ktoré padlo na hracej kocke. Ak
sme zranenie celé neodrazili (injury > 0
), budeme znižovať
náš život. Táto podmienka je dôležitá, keby sme zranenia odrazili a bolo
napr. -2, bez podmienky by sa život zvýšil. Po znížení života
skontrolujeme, či nie je v zápornej hodnote a prípadne ho dorovnáme na
nulu.
Útok
Metóda Attack()
bude brať ako parameter inštanciu bojovníka,
na ktorého sa útočí. To preto, aby sme na ňom mohli zavolať metódu
Defend()
, ktorá na náš útok zareaguje a zmenší protivníkov
život. Tu vidíme výhody referencií v C#, môžeme si inštancie jednoducho
odovzdávať a volať na nich metódy bez toho, aby došlo k ich skopírovaniu.
Ako prvý vypočítame úder, podobne ako pri obrane, úder bude náš útok +
hodnota z hracej kocky. Na súperovi následne zavoláme metódu
Defend()
s hodnotou úderu:
public void Attack(Warrior enemy) { int hit = damage + die.Roll(); enemy.Defend(hit); }
To by sme mali, poďme si skúsiť v našom ukážkovom programe zaútočiť a potom znova vykresliť život. Pre jednoduchosť nemusíme zakladať ďalšieho bojovníka, ale môžeme zaútočiť sami na seba:
{CSHARP_CONSOLE} RollingDie die = new RollingDie(10); Warrior warrior = new Warrior("Zalgoren", 100, 20, 10, die); Console.WriteLine("Warrior: {0}", warrior); // test ToString(); Console.WriteLine("Alive: {0}", warrior.Alive()); // test Alive(); Console.WriteLine("Health: {0}", warrior.HealthBar()); // test graphicHealth(); warrior.Attack(warrior); // attack test Console.WriteLine("Health after the hit: {0}", warrior.HealthBar()); {/CSHARP_CONSOLE}
{CSHARP_OOP} class RollingDie { private Random random; private int sidesCount; public RollingDie() { sidesCount = 6; random = new Random(); } public RollingDie(int sidesCount) { this.sidesCount = sidesCount; random = new Random(); } public int GetSidesCount() { return sidesCount; } public int Roll() { return random.Next(1, sidesCount + 1); } public override string ToString() { return String.Format("Rolling die with {0} sides", sidesCount); } } {/CSHARP_OOP}
{CSHARP_OOP} class Warrior { private string name; private int health; private int maxHealth; private int damage; private int defense; private RollingDie die; public Warrior(string name, int health, int damage, int defense, RollingDie die) { this.name = name; this.health = health; this.maxHealth = health; this.damage = damage; this.defense = defense; this.die = die; } public override string ToString() { return name; } public bool Alive() { return (health > 0); } public string HealthBar() { string s = "["; int total = 20; double count = Math.Round(((double)health / maxHealth) * total); if ((count == 0) && (Alive())) count = 1; for (int i = 0; i < count; i++) s += "#"; s = s.PadRight(total + 1); s += "]"; return s; } public void Defend(int hit) { int injury = hit - (defense + die.Roll()); if (injury > 0) { health -= injury; if (health <= 0) { health = 0; } } } public void Attack(Warrior enemy) { int hit = damage + die.Roll(); enemy.Defend(hit); } } {/CSHARP_OOP}
Konzolová aplikácia
Warrior: Zalgoren
Alive: True
Health: [####################]
Health after the hit: [################## ]
Zdá sa, že všetko funguje, ako má. Prejdime k poslednému bodu dnešného tutoriálu a to k správam.
Správy
Ako už bolo povedané, o útokoch a obrane budeme užívateľov informovať
výpisom na konzole. Výpis nebude vykonávať samotná trieda
Warrior
, tá bude len vracať správy ako textové reťazce. Jedna
možnosť by bola nastaviť návratový typ metód Attack()
a
Defend()
na string
a pri ich volaní vrátiť aj
správu. Problém by však nastal v prípade, ak by sme chceli získať správu
od metódy, ktorá už niečo vracia. Metóda samozrejme nemôže jednoducho
vrátiť dve veci.
Poďme na vec univerzálnejšie, správu budeme ukladať do privátnej
premennej message
a urobíme metódy na jej uloženie a vrátenie.
Samozrejme by sme mohli urobiť premennú verejnú, ale nie je tu dôvod, prečo
umožniť zvonku zápis do správy a tiež by skladanie zložitejšej správy vo
vnútri triedy mohlo byť niekedy problematické.
K atribútom triedy teda pridáme:
private string message;
Teraz si vytvoríme dve metódy. Privátna SetMessage()
, ktorá
berie ako parameter text správy a slúži na vnútorné účely triedy, kde
nastaví správu do privátnej premennej:
private void SetMessage(string message) { this.message = message; }
Nič zložité. Podobne jednoduchá bude verejná metóda na vrátenie správy:
public string GetLastMessage() { return message; }
O práci so správami obohatíme naše metódy Attack()
a
Defend()
, teraz budú vyzerať takto:
public void Attack(Warrior enemy) { int hit = damage + die.Roll(); SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit)); enemy.Defend(hit); } public void Defend(int hit) { int injury = hit - (defense + die.Roll()); if (injury > 0) { health -= injury; message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury); if (health <= 0) { health = 0; message += " and died"; } } else message = String.Format("{0} blocked the hit", name); SetMessage(message); }
Všetko si opäť vyskúšame, tentoraz už vytvoríme druhého bojovníka:
{CSHARP_CONSOLE} RollingDie die = new RollingDie(10); Warrior warrior = new Warrior("Zalgoren", 100, 20, 10, die); Console.WriteLine("Health: {0}", warrior.HealthBar()); // test HealthBar(); // Warrior attack phase Warrior enemy = new Warrior("Shadow", 60, 18, 15, die); enemy.Attack(warrior); Console.WriteLine(enemy.GetLastMessage()); Console.WriteLine(warrior.GetLastMessage()); Console.WriteLine("Health: {0}", warrior.HealthBar()); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class RollingDie { private Random random; private int sidesCount; public RollingDie() { sidesCount = 6; random = new Random(); } public RollingDie(int sidesCount) { this.sidesCount = sidesCount; random = new Random(); } public int GetSidesCount() { return sidesCount; } public int Roll() { return random.Next(1, sidesCount + 1); } public override string ToString() { return String.Format("Rolling die with {0} sides", sidesCount); } } {/CSHARP_OOP}
{CSHARP_OOP} class Warrior { private string name; private int health; private int maxHealth; private int damage; private int defense; private RollingDie die; private string message; public Warrior(string name, int health, int damage, int defense, RollingDie die) { this.name = name; this.health = health; this.maxHealth = health; this.damage = damage; this.defense = defense; this.die = die; } public override string ToString() { return name; } public bool Alive() { return (health > 0); } public string HealthBar() { string s = "["; int total = 20; double count = Math.Round(((double)health / maxHealth) * total); if ((count == 0) && (Alive())) count = 1; for (int i = 0; i < count; i++) s += "#"; s = s.PadRight(total + 1); s += "]"; return s; } public void Attack(Warrior enemy) { int hit = damage + die.Roll(); SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit)); enemy.Defend(hit); } public void Defend(int hit) { int injury = hit - (defense + die.Roll()); if (injury > 0) { health -= injury; message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury); if (health <= 0) { health = 0; message += " and died"; } } else message = String.Format("{0} blocked the hit", name); SetMessage(message); } private void SetMessage(string message) { this.message = message; } public string GetLastMessage() { return message; } } {/CSHARP_OOP}
Výstup:
Konzolová aplikácia
Health: [####################]
Shadow attacks with a hit worth 27 hp
Zalgoren defended against the attack but still lost 12 hp
Health: [################## ]
Máme kocku aj bojovníka, teraz už chýba len aréna.
V ďalšej lekcii, C# - Aréna s bojovníkmi, si vytvoríme arénu.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 2x (63.37 kB)
Aplikácia je vrátane zdrojových kódov v jazyku C#