8. diel - Aréna s mágom (dedičnosť a polymorfizmus)
V minulej lekcii, Dedičnosť a polymorfizmus, sme si vysvetlili dedičnosť a polymorfizmus.
Dnes máme sľúbené, že si dedičnosť a polymorfizmus vyskúšame v praxi. Bude to opäť na našej aréne, kde z bojovníka oddedíme mága. Tento C# .NET tutoriál už patrí k tým náročnejším a bude tomu tak aj pri ďalších. Preto si priebežne precvičujte prácu s objektmi, skúšajte si naše cvičenia a taktiež vymýšľajte nejaké svoje aplikácie, aby ste si zažili základné veci. To, že je tu prítomný celý online kurz neznamená, že ho celý naraz prečítate a pochopíte Snažte sa programovať priebežne.
Než začneme niečo písať, zhodnime sa na tom, čo by mal mág vedieť.
Mág bude fungovať rovnako, ako bojovník. Okrem života bude mať však aj
manu. Spočiatku bude mana plná. V prípade plnej many môže
mág vykonať magický útok, ktorý bude mať pravdepodobne
vyšší damage než útok normálny (ale samozrejme záleží na tom, ako si ho
nastavíme). Tento útok manu vybije na 0
. Každé kolo sa bude
mana zvyšovať o 10
a mág bude podnikať iba bežný útok.
Akonáhle sa mana úplne doplní, opäť bude môcť magický útok použiť.
Mana bude zobrazená grafickým ukazovateľom, rovnako ako život.
Vytvoríme teda triedu Mage.cs
, zdedíme ju z
Warrior
a dodáme jej atribúty, ktoré chceme oproti bojovníkovi
navyše. Bude teda vyzerať takto (opäť si ju okomentujte):
class Mage: Warrior { private int mana; private int maxMana; private int magicDamage; }
V mágovi nemáme zatiaľ prístup ku všetkým premenným, pretože sú v
bojovníkovi nastavené ako privátne. Musíme triedu Warrior
ľahko upraviť. Zmeníme modifikátory private
u atribútov na
protected
. Budeme potrebovať len die
a
name
, ale pokojne nastavíme ako protected
všetky
atribúty charakteru, pretože sa v budúcnosti môžu hodiť, keby sme sa
rozhodli oddediť ďalšie typy bojovníkov. Naopak atribút
message
nie je vhodné nastavovať ako protected
,
pretože nesúvisí s bojovníkom, ale s nejakou vnútornou logikou triedy.
Trieda teda bude vyzerať nejako takto:
protected string name; protected int health; protected int maxHealth; protected int damage; protected int defense; protected RollingDie die; private string message; ...
Prejdime ku konštruktoru.
Konštruktor potomka
C# nededí konštruktory! Je to pravdepodobne z toho dôvodu, že predpokladá, že potomok bude mať navyše nejaké atribúty a pôvodný konštruktor by u neho bol na škodu. To je aj náš prípad, pretože konštruktor mága bude brať oproti tomu z bojovníka navyše dva parametre (mana a magický útok).
Definujeme si teda konštruktor v potomkovi, ktorý berie parametre potrebné na vytvorenie bojovníka a niekoľko parametrov navyše pre mága.
V konštruktoroch potomkov je nutné vždy volať konštruktor predka. Je to z toho dôvodu, že bez volania konštruktora nemusí byť inštancia správne inicializovaná. Konštruktor predka nevoláme iba v prípade, že žiadny nemá. Náš konštruktor musí mať samozrejme všetky parametre potrebné pre predka plus tie nové, čo má navyše potomok. Niektoré potom odovzdáme predkovi a niektoré si spracujeme sami. Konštruktor predka sa vykoná pred naším konštruktorom.
V C# .NET existuje kľúčové slovo base
, base
je
podobné this
, ktoré už poznáme. Na rozdiel od
this
, ktoré odkazuje na konkrétnu inštanciu triedy,
base
odkazuje na predka. My teda môžeme zavolať
konštruktor predka s danými parametrami a potom vykonať naviac inicializáciu
pre mága. V C# sa volanie konštruktora predka píše do hlavičky metódy.
Konštruktor mága bude teda vyzerať takto:
public Mage(string name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage): base(name, health, damage, defense, die) { this.mana = mana; this.maxMana = mana; this.magicDamage = magicDamage; }
Rovnako môžeme volať aj iný konštruktor v tej istej triede
(nie predka), len namiesto base použijeme this
.
Presuňme sa teraz do Program.cs
a druhého bojovníka (Shadow)
zmeňme na mága, napr. takto:
Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45);
Zmenu samozrejme musíme urobiť aj v riadku, kde bojovníka do arény
vkladáme. Všimnite si, že mága ukladáme do premennej typu
Warrior
. Nič nám v tom nebráni, pretože bojovník je jeho
predok. Rovnako si môžeme typ premennej zmeniť na Mage
. Keď
aplikáciu teraz spustíme, bude fungovať úplne rovnako, ako predtým. Mág
všetko dedí z bojovníka a zatiaľ teda funguje ako bojovník.
Polymorfizmus a prepisovanie metód
Bolo by výhodné, keby objekt Arena
mohol s mágom pracovať
rovnakým spôsobom ako s bojovníkom. My už vieme, že takémuto mechanizmu
hovoríme polymorfizmus. Aréna zavolá na objekte metódu
Attack()
so súperom v parametri. Nestará sa o to, či bude útok
vykonávať bojovník alebo mág, bude s nimi pracovať rovnako. U mága si teda
prepíšeme metódu Attack()
z predka. Prepíšeme
zdedenú metódu tak, aby útok pracoval s manom, hlavička metódy však
zostane rovnaká.
Aby sme mohli nejakú metódu prepísať, musí byť v predkovi označená
ako virtuálna. Nehľadajte za tým žiadnu vedu, jednoducho
pomocou kľúčového slova virtual
C# povieme, že si prajeme, aby
potomok mohol túto metódu prepísať. Hlavičku metódy v
Warrior.cs
teda zmeníme na:
public virtual void Attack(Warrior enemy)
Keď sme pri metódach, budeme ešte určite používať metódu
SetMessage()
, tá je však privátna. Označme ju ako
protected
:
protected void SetMessage(string message)
Pri návrhu bojovníka sme samozrejme mali myslieť na to, že
sa z neho bude dediť a už označiť vhodné atribúty a metódy ako
protected
, prípadne metódy ako virtuálne. Kľúčovým slovom
virtual
je označená metóda, ktorú je možné v potomkovi
prepísať, inak to nie je možné. V tutoriáli k bojovníkovi som vás tým
však nechcel zbytočne zaťažovať, preto musíme modifikátory zmeniť až
teraz, keď im rozumieme
Metóda Attack()
v bojovníkovi bude teda
public virtual
. Teraz sa vráťme do potomka a poďme ju
prepísať. Metódu normálne definujeme v Mage.cs
tak, ako sme
zvyknutí. Za modifikátorom public
však ešte použijeme
kľúčové slovo override
, ktoré značí, že si sme vedomí
toho, že sa metóda zdedila, ale prajeme si zmeniť jej správanie:
public override void Attack(Warrior enemy)
Podobne sme prepisovali metódu ToString()
u našich objektov.
Každý objekt v C# je totiž oddedený od System.Object
, ktorý
obsahuje štyri metódy, jedna z nich je aj ToString()
. Pri jej
implementácii teda musíme použiť override
.
Správanie metódy Attack()
nebude nijako zložité. Podľa
hodnoty many buď vykonáme bežný útok alebo útok magický. Hodnotu many
potom buď zvýšime o 10
alebo naopak znížime na 0
v prípade magického útoku:
public override void Attack(Warrior enemy) { int hit = 0; // Mana isn't full if (mana < maxMana) { mana += 10; if (mana > maxMana) mana = maxMana; hit = damage + die.Roll(); SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit)); } else // Magic damage { hit = magicDamage + die.Roll(); SetMessage(String.Format("{0} used magic for {1} hp", name, hit)); mana = 0; } enemy.Defend(hit); }
Kód je asi zrozumiteľný. Všimnite si obmedzenie many na
maxMana
, môže sa nám totiž stať, že túto hodnotu
presiahneme, keď ju zvyšujeme o 10
. Keď sa nad kódom
zamyslíme, tak útok vyššie v podstate vykonáva pôvodná metóda
Attack()
. Iste by bolo prínosné zavolať podobu metódy na
predkovi namiesto toho, aby sme správanie opisovali. K tomu opäť použijeme
base
:
{CSHARP_OOP} class Mage: Warrior { private int mana; private int maxMana; private int magicDamage; public Mage(string name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage): base(name, health, damage, defense, die) { this.mana = mana; this.maxMana = mana; this.magicDamage = magicDamage; } public override void Attack(Warrior enemy) { // Mana isn't full if (mana < maxMana) { mana += 10; if (mana > maxMana) mana = maxMana; base.Attack(enemy); } else // Magic damage { int hit = magicDamage + die.Roll(); SetMessage(String.Format("{0} used magic and took {1} hp off", name, hit)); enemy.Defend(hit); mana = 0; } } } {/CSHARP_OOP}
{CSHARP_OOP} {CSHARP_MAIN_BLOCK} // creating objects RollingDie die = new RollingDie(10); Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die); Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45); Arena arena = new Arena(zalgoren, gandalf, die); // fight arena.Fight(); Console.ReadKey(); {/CSHARP_MAIN_BLOCK} {/CSHARP_OOP}
{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 { protected string name; protected int health; protected int maxHealth; protected int damage; protected int defense; protected 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 virtual 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); } protected void SetMessage(string message) { this.message = message; } public string GetLastMessage() { return message; } } {/CSHARP_OOP}
using System.Threading; {CSHARP_OOP} class Arena { private Warrior warrior1; private Warrior warrior2; private RollingDie die; public Arena(Warrior warrior1, Warrior warrior2, RollingDie die) { this.warrior1 = warrior1; this.warrior2 = warrior2; this.die = die; } private void Render() { Console.Clear(); Console.WriteLine("-------------- Arena -------------- \n"); Console.WriteLine("Warriors health: \n"); Console.WriteLine("{0} {1}", warrior1, warrior1.HealthBar()); Console.WriteLine("{0} {1}", warrior2, warrior2.HealthBar()); } private void PrintMessage(string message) { Console.WriteLine(message); Thread.Sleep(500); } public void Fight() { // The original order Warrior w1 = warrior1; Warrior w2 = warrior2; Console.WriteLine("Welcome to the Arena!"); Console.WriteLine("Today {0} will battle against {1}! \n", warrior1, warrior2); // swapping the warriors bool warrior2Starts = (die.Roll() <= die.GetSidesCount() / 2); if (warrior2Starts) { w1 = warrior2; w2 = warrior1; } Console.WriteLine("{0} goes first! \nLet the battle begin...", w1); Console.ReadKey(); // fight loop while (w1.Alive() && w2.Alive()) { w1.Attack(w2); Render(); PrintMessage(w1.GetLastMessage()); // attack message PrintMessage(w2.GetLastMessage()); // defense message if (w2.Alive()) { w2.Attack(w1); Render(); PrintMessage(w2.GetLastMessage()); // attack message PrintMessage(w1.GetLastMessage()); // defense message } Console.WriteLine(); } } } {/CSHARP_OOP}
Opäť vidíme, ako môžeme znovupoužívať kód. S dedičnosťou je spojených naozaj mnoho techník, ako si ušetriť prácu. V našom prípade to ušetrí niekoľko riadkov, ale pri väčšom projekte by to mohlo mať obrovský význam.
Aplikácia teraz funguje tak, ako má:
Konzolová aplikácia
-------------- Arena --------------
Warriors health:
Zalgoren [########### ]
Gandalf [############## ]
Gandalf attacks with a hit worth 23 hp
Zalgoren defended against the attack but still lost 9 hp
Aréna nás však neinformuje o mane mága, poďme to napraviť. Pridáme
mágovi verejnú metódu ManaBar()
, ktorá bude obdobne ako pri
živote vracať string
s grafickým ukazovateľom many.
Aby sme nemuseli logiku so zložením ukazovateľa písať dvakrát,
upravíme metódu HealthBar()
v Warrior.cs
.
Pripomeňme si, ako vyzerá:
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; }
Vidíme, že nie je okrem premenných health
a
maxHealth
na živote nijako závislá. Metódu premenujeme na
GraphicalBar()
a dáme ju dva parametre: aktuálnu hodnotu a
maximálnu hodnotu. Premenné health
a maxHealth
v
tele metódy potom nahradíme za current
a maximum
.
Modifikátor bude protected
, aby sme metódu mohli v potomkovi
použiť:
protected string GraphicalBar(int current, int maximum) { string s = "["; int total = 20; double count = Math.Round(((double)current / maximum) * total); if ((count == 0) && (Alive())) count = 1; for (int i = 0; i < count; i++) s += "#"; s = s.PadRight(total + 1); s += "]"; return s; }
Metódu HealthBar()
v Warrior.cs
naimplementujeme
znova, bude nám v nej stačiť jediný riadok a to zavolanie metódy
GraphicalBar()
s príslušnými parametrami:
public string HealthBar() { return GraphicalBar(health, maxHealth); }
Určite sme mohli v tutoriáli s bojovníkom urobiť metódu
GraphicalBar()
rovno. Chceli sme si však ukázať, ako sa riešia
prípady, keď potrebujeme vykonať podobnú funkčnosť viackrát. S takouto
parametrizáciou sa v praxi budete stretávať často, pretože nikdy presne
nevieme, čo budeme v budúcnosti od nášho programu požadovať.
Teraz môžeme vykresľovať ukazovateľ tak, ako sa nám to hodí. Presuňme
sa do Mage.cs
a naimplementujme metódu ManaBar()
:
public string ManaBar() { return GraphicalBar(mana, maxMana); }
Jednoduché, že? Teraz je mág hotový, zostáva len naučiť arénu
zobrazovať manu v prípade, že je bojovník mág. Presuňme sa teda do
Arena.cs
.
Rozpoznanie typu objektu
Keďže sa nám teraz vykreslenie bojovníka skomplikovalo, urobíme si naň
samostatnú metódu PrintWarrior()
, jej parametrom bude daná
inštancia bojovníka:
private void PrintWarrior(Warrior w) { Console.WriteLine(w); Console.Write("Health: "); Console.WriteLine(w.HealthBar()); }
Teraz poďme reagovať na to, či je bojovník mág. Minule sme si povedali,
že na to slúži operátor is
:
private void PrintWarrior(Warrior w) { Console.WriteLine(w); Console.Write("Health: "); Console.WriteLine(w.HealthBar()); if (w is Mage) { Console.Write("Mana: "); Console.WriteLine(((Mage)w).ManaBar()); } }
Bojovníka sme museli na mága pretypovať, aby sme sa k metóde
ManaBar()
dostali. Samotný Warrior
ju totiž nemá.
To by sme mali, PrintWarrior()
budeme volať v metóde
Render()
, ktorá bude vyzerať takto:
using System.Threading; {CSHARP_OOP} class Arena { private Warrior warrior1; private Warrior warrior2; private RollingDie die; public Arena(Warrior warrior1, Warrior warrior2, RollingDie die) { this.warrior1 = warrior1; this.warrior2 = warrior2; this.die = die; } private void Render() { Console.Clear(); Console.WriteLine("-------------- Arena -------------- \n"); Console.WriteLine("Warriors: \n"); PrintWarrior(warrior1); Console.WriteLine(); PrintWarrior(warrior2); Console.WriteLine(); } private void PrintMessage(string message) { Console.WriteLine(message); Thread.Sleep(500); } public void Fight() { // The original order Warrior w1 = warrior1; Warrior w2 = warrior2; Console.WriteLine("Welcome to the Arena!"); Console.WriteLine("Today {0} will battle against {1}! \n", warrior1, warrior2); // swapping the warriors bool warrior2Starts = (die.Roll() <= die.GetSidesCount() / 2); if (warrior2Starts) { w1 = warrior2; w2 = warrior1; } Console.WriteLine("{0} goes first! \nLet the battle begin...", w1); Console.ReadKey(); // fight loop while (w1.Alive() && w2.Alive()) { w1.Attack(w2); Render(); PrintMessage(w1.GetLastMessage()); // attack message PrintMessage(w2.GetLastMessage()); // defense message if (w2.Alive()) { w2.Attack(w1); Render(); PrintMessage(w2.GetLastMessage()); // attack message PrintMessage(w1.GetLastMessage()); // defense message } Console.WriteLine(); } } private void PrintWarrior(Warrior w) { Console.WriteLine(w); Console.Write("Health: "); Console.WriteLine(w.HealthBar()); if (w is Mage) { Console.Write("Mana: "); Console.WriteLine(((Mage)w).ManaBar()); } } } {/CSHARP_OOP}
{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 { protected string name; protected int health; protected int maxHealth; protected int damage; protected int defense; protected 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 GraphicalBar(int current, int maximum) { string s = "["; int total = 20; double count = Math.Round(((double)current / maximum) * total); if ((count == 0) && (Alive())) count = 1; for (int i = 0; i < count; i++) s += "#"; s = s.PadRight(total + 1); s += "]"; return s; } public string HealthBar() { return GraphicalBar(health, maxHealth); } public virtual 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); } protected void SetMessage(string message) { this.message = message; } public string GetLastMessage() { return message; } } {/CSHARP_OOP}
{CSHARP_OOP} class Mage: Warrior { private int mana; private int maxMana; private int magicDamage; public Mage(string name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage): base(name, health, damage, defense, die) { this.mana = mana; this.maxMana = mana; this.magicDamage = magicDamage; } public string ManaBar() { return GraphicalBar(mana, maxMana); } public override void Attack(Warrior enemy) { // Mana isn't full if (mana < maxMana) { mana += 10; if (mana > maxMana) mana = maxMana; base.Attack(enemy); } else // Magic damage { int hit = magicDamage + die.Roll(); SetMessage(String.Format("{0} used magic and took {1} hp off", name, hit)); enemy.Defend(hit); mana = 0; } } } {/CSHARP_OOP}
{CSHARP_OOP} {CSHARP_MAIN_BLOCK} // creating objects RollingDie die = new RollingDie(10); Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die); Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45); Arena arena = new Arena(zalgoren, gandalf, die); // fight arena.Fight(); Console.ReadKey(); {/CSHARP_MAIN_BLOCK} {/CSHARP_OOP}
}
Hotovo :
Konzolová aplikácia
-------------- Arena --------------
Warriors:
Zalgoren
Health: [#### ]
Gandalf
Health: [####### ]
Mana: [# ]
Gandalf used magic and took 48 hp off
Zalgoren defended against the attack but still lost 33 hp
Aplikáciu ešte môžeme dodať krajší vzhľad, vložil som ASCIIart
nadpis Arena
, ktorý som vytvoril touto aplikáciou: http://patorjk.com/software/taag.
Navyše som zafarbil ukazovatele pomocou farby pozadia a popredia. Metódu na
vykreslenie ukazovateľa som upravil tak, aby vykresľovala plný obdĺžnik
miesto #
(ten napíšete pomocou Alt + 2
1 9). Výsledok môže vyzerať takto:
Konzolová aplikácia __ ____ ____ _ _ __ /__\ ( _ \( ___)( \( ) /__\ /(__)\ ) / )__) ) ( /(__)\ (__)(__)(_)\_)(____)(_)\_)(__)(__) Warriors: Zalgoren Health: ████████████████████ Gandalf Health: ████████████████████ Mana: ████████████████████ Gandalf used magic and took 48 hp off Zalgoren defended against the attack but still lost 33 hp
Kód máte v prílohe. Pokiaľ ste niečomu nerozumeli, skúste si článok prečítať viackrát alebo pomalšie, sú to dôležité praktiky.
V nasledujúcom cvičení, Riešené úlohy k 5.-8. lekcii OOP v C# .NET, si precvičíme nadobudnuté skúsenosti z predchádzajúcich lekcií.
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é 3x (74.26 kB)
Aplikácia je vrátane zdrojových kódov v jazyku C#