Friday, May 24, 2013

Learn to Program with Csharp From a Gaming Perspective

Hello and welcome to Lesson 11 of Learn to Program with C#: From a Gaming Perspective

This lesson is a carry over from our previous lesson.  We're still working with our RpgTutorial game and hopefully you tried making 3 new methods.  One to print our choices, one to process the choices and one to do the damage.

3 new Methods
So your methods may differ greatly from mine.  That's quite alright.  Depending on how you figure damage should be calculated, when things should happen, you'll get different answers.  I'll go ahead and list mine now.
        string userchoice;
        int damage;

        Random rand;

First I added 3 new variables to our class.
        string PrintChoice()
        {
            string choice;
            Console.WriteLine();
            Console.Write(@"
_____________________
Please choose an action:
(A)ttack:
(D)efend:
_____________________");
            Console.WriteLine();
            choice = Console.ReadLine();
            return choice;
        }
Now we have our PrintChoice() method.  I made it return a string at the same time to keep our battle loop clean.  The WriteLines() just keep our output looking nice.
        void ProcessChoice(string choice,Hero hero, Monster monster)
        {
            switch (choice)
            {
                case "A":
                case "a":
                    Console.WriteLine();
                    Console.WriteLine("{0} attacks!",hero.Identifier);//print what it is
                    DealDamage(hero, monster);//send it to our damage method
                    monster.CurrentHealth -= damage;//take it from the monster
                    Console.WriteLine("{0} hits the {1} for {2}hp of damage"
                        , hero.Identifier, monster.Identifier, damage);//show result
                    break;
                case "D":
                case "d":
                    Console.WriteLine();
                    Console.WriteLine("{0} defends!", hero.Identifier);
                    //we will make defending more beneficial later
                    //Many times you will see this shown like below:
                    //To Do: Add logic to make defending beneficial
                    break;
                default:
                    Console.WriteLine("I'm sorry, I didn't recognize that.");
                    Console.WriteLine();//if the choice isn't valid
                    choice = PrintChoice();//the we'll keep asking till
                    Console.WriteLine();//they get it right
                    ProcessChoice(choice,hero,monster);
                    break;
            }
        }
Here our ProcessChoice() method uses the userchoice string and translates it into an action.  As noted Defend just outputs a string right now.  Default will keep looping till we get a valid choice.  Attack shows us attacking, calls the DealDamage() method, and subtracts the health from the monster.  We are feeding this method a string, and our two characters.  Mainly so that we can pass them onto the DealDamage() method.
        int DealDamage(Hero hero, Monster monster)
        {
            int max;
            int min;
            rand = new Random();
            max = hero.AttackDamage - monster.Defense;
            if (max <= 0)
            {
                max = 1;//right now this isn't a concern, but this is a bit
                //of future proofing our damage mechanism
            }
            min = (int)(hero.AttackDamage * .8) - monster.Defense;//**I'll explain this one**
            if (min <= 0)
            {
                min = 1;
            }
            damage = rand.Next(min, max);//calculate the damage
            return damage;//send it on back
        }
So our DealDamage method takes both characters and uses the hero's attackdamage and the monster's defense to figure out how much damage should be done.  max and min are just local variables, so there's no need to have them anywhere but inside of our method.  This line however:
min = (int)(hero.AttackDamage * .8) - monster.Defense;
might be confusing to you.  Without the (int) in front of this, we would get an error.  Why?  because the compiler doesn't know for sure wether we want to keep min an int or wether we are trying to make it a double (not a valid option by the way).  This is what is known as a cast.  The goal here is to use 80% of the attack damage (rounded down of course because that's how int's roll) so that we have a good min and max to work with. 

All of this boils down to design choice though.  You could use 90% or calculate it some other way entirely.  This is the kind of stuff that makes or breaks your game.  Does it make sense this way?  Is it more fun another way?  Do we want to have a method to determine if the hero misses or not, depending on agility?  This is the kind of thing that gives your game it's character.

If you add these methods to our game and put PrintChoice and ProcessChoice into the do loop like below the game will run, albeit one sided.

                PrintStatus(hero, monster);               
                userchoice = PrintChoice();
                Console.WriteLine();
                ProcessChoice(userchoice, hero, monster);
                Console.ReadLine();
                Console.Clear();//This clears our screen so the next turn is a fresh screen
           

What's missing?
There's still a lot of things we can add.  We can add spells (make some new classes for them, with new options and a new print out menu), we can add a hit/miss method, we can make defend more beneficial, but what do we need?  Well first we need a new method to determine if isAlive is false, and then we need some AI or else our game is just a bully beating up a helpless monster so before we tackle AI let's make a method that takes health and decides if the character is alive or not.  Let's make this one more universal and just pass it an int and return a bool.  Try it on your own first and compare.
        bool CheckHealth(int health)
        {
            bool alive;
            if (health > 0)
            {
                alive = true;
            }
            else
            {
                alive = false;
            }
            return alive;
        }

So that's a pretty simple method which is useable by either the hero or the monster which is ideal.  When we add it to our loop the whole thing looks like this:
namespace RpgTutorial
{
    class Battle
    {
        string userchoice;
        int damage;

        Random rand;

        public Battle(Hero hero, Monster monster)
        {
            Console.WriteLine("{0} is facing a {1}.", hero.Identifier, monster.Identifier);
            BattleLoop(hero, monster);
        }

        public void BattleLoop(Hero hero, Monster monster)
        {
            do
            {
                PrintStatus(hero, monster);               
                userchoice = PrintChoice();
                Console.WriteLine();
                ProcessChoice(userchoice, hero, monster);
                monster.isAlive = CheckHealth(monster.CurrentHealth);
                Console.ReadLine();
                Console.Clear();//This clears our screen so the next turn is a fresh screen
            }
            while (hero.isAlive == true && monster.isAlive == true);
        }

        void PrintStatus(Hero hero, Monster monster)
        {
            Console.Write(@"
********************************
         HP/MaxHP   MP/MaxMP
{0}:   {1}/{2}hp    {3}/{4}mp
{5}: {6}/{7}hp      {8}/{9}mp
********************************
", hero.Identifier, hero.CurrentHealth, hero.MaxHealth, hero.CurrentMagic, hero.MaxMagic,
 monster.Identifier, monster.CurrentHealth, monster.MaxHealth, monster.CurrentMagic, monster.MaxMagic);
        }

        string PrintChoice()
        {
            string choice;
            Console.WriteLine();
            Console.Write(@"
_____________________
Please choose an action:
(A)ttack:
(D)efend:
_____________________");
            Console.WriteLine();
            choice = Console.ReadLine();
            return choice;
        }
        void ProcessChoice(string choice,Hero hero, Monster monster)
        {
            switch (choice)
            {
                case "A":
                case "a":
                    Console.WriteLine();
                    Console.WriteLine("{0} attacks!",hero.Identifier);//print what it is
                    DealDamage(hero, monster);//send it to our damage method
                    monster.CurrentHealth -= damage;//take it from the monster
                    Console.WriteLine("{0} hits the {1} for {2}hp of damage"
                        , hero.Identifier, monster.Identifier, damage);//show result
                    break;
                case "D":
                case "d":
                    Console.WriteLine();
                    Console.WriteLine("{0} defends!", hero.Identifier);
                    //we will make defending more beneficial later
                    //Many times you will see this shown like below:
                    //To Do: Add logic to make defending beneficial
                    break;
                default:
                    Console.WriteLine("I'm sorry, I didn't recognize that.");
                    Console.WriteLine();//if the choice isn't valid
                    choice = PrintChoice();//the we'll keep asking till
                    Console.WriteLine();//they get it right
                    ProcessChoice(choice,hero,monster);
                    break;
            }
        }
        int DealDamage(Hero hero, Monster monster)
        {
            int max;
            int min;
            rand = new Random();
            max = hero.AttackDamage - monster.Defense;
            if (max <= 0)
            {
                max = 1;//right now this isn't a concern, but this is a bit
                //of future proofing our damage mechanism
            }
            min = (int)(hero.AttackDamage * .8) - monster.Defense;//**I'll explain this one**
            if (min <= 0)
            {
                min = 1;
            }
            damage = rand.Next(min, max);//calculate the damage
            return damage;//send it on back
        }
        bool CheckHealth(int health)
        {
            bool alive;
            if (health > 0)
            {
                alive = true;
            }
            else
            {
                alive = false;
            }
            return alive;
        }
    }
}



No comments:

Post a Comment