Hľadáme nové posily do ITnetwork tímu. Pozri sa na voľné pozície a pridaj sa k najagilnejšej firme na trhu - Viac informácií.
IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

6. diel - C# - Aréna s bojovníkmi

V minulej lekcii, Bojovník do arény, sme si vytvorili triedu bojovníka.

Hraciu kocku máme hotovú z prvých lekcií objektovo orientovaného programovania. Dnes teda dáme všetko dohromady a vytvoríme v C# .NET funkčnú arénu. Tutoriál bude skôr oddychový a pomôže nám zopakovať si prácu s objektmi.

Potrebujeme napísať nejaký kód na obsluhu bojovníkov a výpis správ užívateľovi. Samozrejme ho nebudeme búšiť rovno do Program.cs, ale vytvoríme si objekt Arena, kde sa bude zápas odohrávať. Program.cs potom len založí objekty ao zvyšok sa bude starať objekt Arena. Pridajme k projektu teda poslednú triedu a to Arena.cs.

Trieda bude viac-menej jednoduchá, ako atribúty bude obsahovať 3 potrebné inštancie: 2 bojovníkov a hraciu kocku. V konštruktore sa tieto atribúty naplnia z parametrov. Kód triedy bude teda nasledujúci (komentáre si dopíšte):

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

}

Zamyslime sa nad metódami. Z verejných metód bude určite potrebná len tá na simuláciu zápasu. Výstup programu na konzole urobíme trochu na úrovni a tiež umožníme triede Arena, aby priamo ku konzole pristupovala. Rozhodli sme sa, že výpis bude v kompetencii triedy, pretože sa nám to tu oplatí. Naopak keby výpis vykonávali aj bojovníci, bolo by to na škodu (neboli by univerzálne). Potrebujeme teda metódu, ktorá vykreslí obrazovku s aktuálnymi údajmi o bicykli a životmi bojovníkov. Správy o útoku a obrane budeme chcieť vypisovať s dramatickou pauzou, aby bol výsledný efekt lepší, urobíme si pre takýto typ správy ešte pomocnú metódu. Začnime s vykreslením informačnej obrazovky:

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

Tu asi nie je čo riešiť, môžete si ešte obrazovku vyzdobiť farebne, keď budete chcieť. Vďaka zmazaniu konzoly pomocou Clear() docielime peknú informačnú obrazovku namiesto klasického konzolového textu, kde sa plná obrazovka roluje dole. Metóda je privátna, budeme ju používať len vo vnútri triedy.

Ďalšou privátnou metódou bude výpis správy s dramatickou pauzou:

private void PrintMessage(string message)
{
    Console.WriteLine(message);
    Thread.Sleep(500);
}

Kód je zrejmý až na triedu Thread, ktorá umožňuje prácu s vláknami. My z nej využijeme iba metódu Sleep(), ktorá uspí vlákno programu na daný počet milisekúnd. S vláknami budeme pracovať až na konci kurzu. Aby všetko fungovalo, musíme pridať using System.Threading; na začiatok súboru Arena.cs.

Obe metódy vlastne len vypisujú na konzole, pripadá mi zbytočné ich skúšať, presunieme sa teda už k samotnému zápasu. Metóda Fight() nebude mať žiadne parametre a nebude ani nič vracať. Vnútri bude cyklus, ktorý bude na striedačku volať útoky bojovníkov navzájom a vypisovať informačnú obrazovku a správy. Metóda by mohla vyzerať takto:

public void Fight()
{
    Console.WriteLine("Welcome to the Arena!");
    Console.WriteLine("Today {0} will battle against {1}! \n", warrior1, warrior2);
    Console.WriteLine("Let the battle begin...");
    Console.ReadKey();
    // fight loop
    while (warrior1.Alive() && warrior2.Alive())
    {
        warrior1.Attack(warrior2);
        Render();
        PrintMessage(warrior1.GetLastMessage()); // attack message
        PrintMessage(warrior2.GetLastMessage()); // defense message
        warrior2.Attack(warrior1);
        Render();
        PrintMessage(warrior2.GetLastMessage()); // attack message
        PrintMessage(warrior1.GetLastMessage()); // defense message
        Console.WriteLine();
    }
}

Kód vypíše jednoduché informácie a po stlačení klávesy prejde do cyklu s bojom. Jedná sa o while cyklus, ktorý sa opakuje, kým sú obaja bojovníci nažive. Prvý bojovník zaútočí na druhého, jeho útok vnútorne zavolá na druhom bojovníkovi obranu. Po útoku vykreslíme obrazovku s informáciami a ďalej správy o útoku a obrane pomocou našej metódy PrintMessage(), ktorá po výpise urobí dramatickú pauzu. To isté urobíme aj pre druhého bojovníka.

Presuňme sa do Program.cs, vytvorme patričné inštancie a zavolajme na aréne metódu Fight():

    // creating objects
    RollingDie die = new RollingDie(10);
    Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die);
    Warrior shadow = new Warrior("Shadow", 60, 18, 15, die);
    Arena arena = new Arena(zalgoren, shadow, die);
    // fight
    arena.Fight();
    Console.ReadKey();
    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);
        }

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

    }

using System.Threading;
    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()
        {
            Console.WriteLine("Welcome to the Arena!");
            Console.WriteLine("Today {0} will battle against {1}! \n", warrior1, warrior2);
            Console.WriteLine("Let the battle begin...");
            Console.ReadKey();
            // fight loop
            while (warrior1.Alive() && warrior2.Alive())
            {
                warrior1.Attack(warrior2);
                Render();
                PrintMessage(warrior1.GetLastMessage()); // attack message
                PrintMessage(warrior2.GetLastMessage()); // defense message
                warrior2.Attack(warrior1);
                Render();
                PrintMessage(warrior2.GetLastMessage()); // attack message
                PrintMessage(warrior1.GetLastMessage()); // defense message
                Console.WriteLine();
            }
        }
    }

Charakteristiky hrdinov si môžete upraviť podľa ľubovôle. Program spustíme:

Konzolová aplikácia
-------------- Arena --------------

Warriors health:

Zalgoren [##                  ]
Shadow [                    ]
Shadow attacks with a hit worth 19 hp
Zalgoren blocked the hit

Výsledok je celkom pôsobivý. Objekty spolu komunikujú, grafický život ubúda ako má, zážitok umocňuje dramatická pauza. Aréna má však 2 nedostatky.

  • V cykle s bojom útočí prvý bojovník na druhého. Potom však vždy útočí aj druhý bojovník, nehľadiac na to, či ho prvý nezabil. Môže teda útočiť už ako mŕtvy. Pozrite sa na screenshot vyššie, Shadow útočil ako posledný aj keď bol mŕtvy. Až potom sa vystúpilo z while cyklu. U prvého bojovníka tento problém nie je, u druhého musíme pred útokom kontrolovať, či je nažive.
  • Druhým nedostatkom je, že bojovníci vždy bojujú v rovnakom poradí, čiže tu "Zalgoren" má vždy výhodu. Poďme vniesť ďalší prvok náhody a pomocou kocky rozhodneme, ktorý z bojovníkov bude začínať. Keďže sú bojovníci vždy dvaja, stačí hodiť kockou a pozrieť sa, či padlo číslo menšie alebo rovné polovici počtu stien kocky. Teda napr. pokiaľ padne na desaťstennej kocke číslo do 5tich, začína 2. bojovník, inak začína prvý. Zostáva zamyslieť sa nad tým, ako do kódu zaniesť prehadzovanie bojovníkov. Iste by bolo veľmi neprehľadné opodmienkovať príkazy vo while cykle. Keďže už vieme, že v C# fungujú referencie, nie je pre nás problém urobiť si 2 premenné, v ktorých budú inštancie bojovníkov, nazvime ich jednoducho w1 a w2. Do týchto premenných si na začiatku dosadíme bojovníkov warrior1 a warrior2 tak, ako potrebujeme. Môžeme teda pri pozitívnom hode kockou dosadiť do w1 warrior2 a naopak, výsledkom bude, že začínať bude ten druhý. Kód cyklu sa takto vôbec nezmení a zostane stále prehľadný a jednoduchý, len namiesto warrior bude w.

Zmenená verzia vrátane podmienky, aby nemohol útočiť mŕtvy bojovník, by mohla vyzerať nejako takto:

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

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

    }

Program vyskúšajme.

Konzolová aplikácia
-------------- Arena --------------

Warriors health:

Zalgoren [###########         ]
Shadow [                    ]
Zalgoren attacks with a hit worth 27 hp
Shadow defended against the attack but still lost 9 hp, and died

Vidíme, že všetko je už v poriadku. Gratulujem vám, ak ste sa dostali až sem a tutoriály naozaj čítali a pochopili, máte základy objektového programovania a dokážete tvoriť rozumné aplikácie :)

V budúcej lekcii, Dedičnosť a polymorfizmus, sa pozrieme na objektovo orientované programovanie podrobnejšie. V úvode sme si hovorili, že OOP stojí na pilieroch: zapuzdrenie, dedičnosť a polymorfizmus. Prvé vieme už veľmi dobre a modifikátor private je nám známy. Ďalší dva nás čakajú nabudúce.


 

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 (65.55 kB)
Aplikácia je vrátane zdrojových kódov v jazyku C#

 

Predchádzajúci článok
Bojovník do arény
Všetky články v sekcii
Objektovo orientované programovanie v C# .NET
Preskočiť článok
(neodporúčame)
Dedičnosť a polymorfizmus
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
1 hlasov
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity