2013年12月19日 星期四

從遊戲的視角學C#

我接觸遊戲的開發有幾年的時間,其實不算長,不過倒是學習到很多曾經我較不足的部分,尤其是將Design Pattern運用在開發上。在C#的Library裡,微軟提供了許多的函式庫,對於開發軟體、遊戲、硬體控制、Web網路程式,都是非常好用。本文將要討論從遊戲的角度來看類別的運用。

首先,透過類別來分離出我們的遊戲的不同部分有關的理論。

遊戲中使用Class

一開始的遊戲結構上,沒有為了不同的遊戲部分裡的Events而使用Class。而是用了一個規則化的程式邏輯來寫這遊戲功能。在戰鬥循環動作中,讓Program類別只需打開Main()和去調用MainGame()。主要是初始者知道,開發遊戲裡涵蓋的基礎知識(移動世界各地,買東西..等。),這對於一般剛接觸用C#來開發遊戲的人來說,有許多不了解的地方。然後,我們需要一個類別來處理Battles,讓戰鬥流程不斷迴圈,就如同玩一款遊戲一樣,開始->玩遊戲->結速->再開始。現在需要打開一個全新的項目。把它叫做RpgTutorial。所以,進入“File - >New Project”。然後選擇“Console Application(控制台應用程式)”,並將其命名RpgTutorial。

現在,我們需要添加兩個類別,取名MainGame和Battle。右鍵單擊RpgTutorial中,選擇Add - >class,並將其命名為MainGame,並重複這個過程,叫它是Battle。現在,有三個基底類別的工作。讓Main()能呼叫MainGame類別。

namespace RpgTutorial
{
    class Program
    {
        static void Main(string[] args)
        {
            MainGame maingame = new MainGame();
        }
    }

}

MainGame沒有建構函數的權利,但MainGame建構函數將不會發送給它任何參數。現在MainGame需要一個遊戲循環,並在該循環將在某個時候調用Battle class。所以要製造一個do/ while循環。

namespace RpgTutorial
{
    class MainGame
    {
        Battle battle;
        string answer;

        public MainGame()
        {
            BasicGameLoop();
        }

        void BasicGameLoop()
        {
            do
            {
                battle = new Battle();
                Console.WriteLine("Do you want to play again?");
                answer = Console.ReadLine();
            }

            while (answer == "Y" || answer == "y");
        }
    }

}


從上面的程式結構可以了解遊戲的「基本迴路」怎麼回事。此時的戰鬥不接受任何參數。這所以運行中畫面會很快的改變,因為沒有控制。此外,宣告我Battle class 到第二個步驟,而不是第一個步驟。

所以,現在有三個class來處理程序的不同狀態。一個用於加載,一個用於main game和一個用於battle class。這取決於遊戲的形式和目標。

規劃

現在,我們的基本結構是到位,但一定 還有需要加強的地方。首先,我們就要有一個英雄,一個怪物。我們最好為這兩個角色建構兩個新的類別,取名叫Hero和Monster。

Hero絕對是一個物件,但不會只有一種。如果我們調用一個新的Hero在程式的循環,每次開始將刪除英雄的統計和刷新數據,不是一個好方式。有什麼好的方法來設置這些數據呢?怎麼樣呼叫初始化裡面的Hero類別的靜態方法?其實,初始化變數不屬於英雄,但是當創建它時可以拉進來。用靜態的方式更好。呼叫初始化maingame類別中,需要發送Initialize()到創造了進行更新的英雄物件。要定義hero的變數為public。

namespace RpgTutorial
{
    class Hero
    {
        public int CurrentHealth, MaxHealth, CurrentMagic;
        public int MaxMagic, Strength, Defense, Agility;
        public int Experience, Gold, AttackDamage;
        public string Identifier;
        public bool isAlive;

        public Hero(){}

        public static void Initialize(Hero hero)
        {
            hero.CurrentHealth = 18;
            hero.MaxHealth = 18;
            hero.CurrentMagic = 8;
            hero.MaxMagic = 8;
            hero.Strength = 10;
            hero.Defense = 3;
            hero.Agility = 6;
            hero.Experience = 0;
            hero.Gold = 0;
            Console.WriteLine("What is your Hero's name?");
            hero.Identifier = Console.ReadLine();
            hero.isAlive = true;
            hero.AttackDamage = hero.Strength;
        }
    }

}

在這裡,Hero將設立當初的初始值,但是因為靜態方法不屬於另一個Hero,讓初始值仍保有一開始的值。

要記得如何設置的參數為Hero類別:Initialize(Hero hero)。這不是要求一個新的物件,或者一個新的宣告,這就像一個empty,顯示它可以被傳遞給它的物件期望的方法。如果嘗試將不同類別的物件不止一個來自像myhero英雄類別,將會產生錯誤。

所以現在MainGame需要更新,呼叫Hero類別,並初始化Hero的變數。

namespace RpgTutorial
{
    class MainGame
    {
        Hero myhero;
        Battle battle;
        string answer;

        public MainGame()
        {
            myhero = new Hero();//創立Hero 類別
            Hero.Initialize(myhero);//傳送 myheroInitializeHero

            //可以變動的,不論初始化的是不是呼叫Hero
            BasicGameLoop();
        }

        void BasicGameLoop()
        {
            do
            {
                battle = new Battle();
                Console.WriteLine("Do you want to play again?");
                answer = Console.ReadLine();
            }

            while (answer == "Y" || answer == "y");
        }
    }

}

在註解裡,表非可以在不同的地方使用不同的名稱,同時仍然使用相同的物件。要注意的是一定要宣告Hero,之前的遊戲循環,沒有創建它的一個新的副本。現在myhero將從這裡開始了使用英雄副本,換句話說,這是將調用一個新的Hero。

Monster 類別

Monster類別需要有相同類型的變數作為Hero。不需要一個initialize方法,因為有Monster的多個副本。所以保持Monster類別簡單多了。

namespace RpgTutorial
{
    class Monster
    {
        public int CurrentHealth, MaxHealth, CurrentMagic;
        public int MaxMagic, Strength, Defense, Agility;
        public int Experience, Gold, AttackDamage;
        public string Identifier;
        public bool isAlive;

        public Monster()
        {
            CurrentHealth = 8;
            MaxHealth = 8;
            CurrentMagic = 0;
            MaxMagic = 0;
            Strength = 5;
            Defense = 3;
            Agility = 4;
            Experience = 5;
            Gold = 2;
            Identifier = "Monster";
            isAlive = true;
            AttackDamage = Strength;
        }
    }

}

正如你可以看到剛剛初始化權的構造函數中的變量。這樣一來,當要求它的一個新實例,它將被設置並準備好了。又一切都公開,因為我們需要的一切將我們的戰鬥類的內部使用。

寫死的程式碼

「寫死的程式碼」是一件很遭的情況。寫死的程式碼是把輸出或輸入的相關參數(例如:路徑、輸出的形式、格式) 直接寫死在原始碼中,而不是再次重複使用一個變數的值到程式中。在英雄職業,怪物的class,是寫死的程式碼在所謂認定的結果論裡。這不能真正避免了這一些流程,但要在程序中使用這些變數,而不是直接輸入一個數字英寸,寫死的程式碼不僅使你的程式不夠靈活,而且更難以改變。比方說,我們有一個球是像素25寬,像素 25高。而不是告訴該程式直接寫死值的大小25x25,反而會告訴它要檢查圖片的寬度和高度。所以這就是Design Pattern的用意,雖然這會程式結構分工的更細,但如果不這樣做,當決定改變球的尺寸30X30 時,必須通過整個程式的修改,並更改25X25到30X30。如果將程式的意圖改成告訴它來檢查圖片的寬度和高度,其實只要變數改變,並不會改變程式整個架構。

在一個大型的專案,如果開發一個巨大的商業遊戲,它決定了開始的基本值,之後想改變是非常困難且更費工和開發成本,如果已經發行的遊戲,這不是一個快速和容易解決的程式結構。在設計上,可以像是將參數寫在一個XML裡。寫死的程式碼是會造成日後開發的時間和成本,所以要避免這種寫法。

戰鬥類別

在這一點上,這應該是相當熟悉的。有一個Loop,檢查每一個正確的絛件,並顯示造成多大的損害。得到了特性上的資訊,可以用它來確定類別的東西造成多大的損害或者後續的動作,所以要作出一個建構函數來接受一個Hero和Monster戰鬥類別:

namespace RpgTutorial
{
    class Battle
    {
        public Battle(Hero hero, Monster monster){}
    }

}

現在有完整的存取狀態。另外,本來可以為角色設置health、magic等參數,但是這是很簡單的。現在來開始循環。首先介紹,然後我們循環,將放進新的方法Method,它會透過bool檢查是否存活,而不是health。

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

        public void BattleLoop(Hero hero, Monster monster)
        {
            do
            {
                //實現
            }

            while (hero.isAlive == true && monster.isAlive == true);
        }

    }

}

傳送物件到新的方法,如此一來就能存取這些狀態,現在需要一個方法去顯示狀態:
public 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
    );

}

再一次傳遞物件到新的方法,在請求時將會印出兩個角色狀態的方法。這是可以運行來仔細檢查並除錯,看看它是否運作正確。首先,需要確保在創建Main game class的new monster,然後需要更新battle類別同時接受myhero和monster。下面是更新MainGame和BattleClass:

namespace RpgTutorial
{
    class MainGame
    {
        Hero myhero;
        Battle battle;
        string answer;

        public MainGame()
        {
            myhero = new Hero();

            Hero.Initialize(myhero);
            BasicGameLoop();
        }

        void BasicGameLoop()
        {
            do
            {

                Monster monster = new Monster();
                battle = new Battle(myhero, monster);
                Console.WriteLine("Do you want to play again?");
                answer = Console.ReadLine();
            }

            while (answer == "Y" || answer == "y");
        }
    }
}

namespace RpgTutorial
{
    class Battle
    {
        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);
                Console.ReadLine();
            }

            while (hero.isAlive == true && monster.isAlive == true);
        }

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

}

到目前為止,呈現出一個menu,但真正從這裡算是簡單且順暢。所有要做的是一個printchoice然後再處理的攻擊,這是很基本的遊戲程式結構。但是還是要解決AI。一些更多的技巧和調整,一個相當實用的Battle loop。遊戲的開發,還有更多的元素,是要透過遊戲呈現,聲音,影像,動畫效果,攻擊,防禦,移動。在此介紹基本的遊戲設計觀。

-雲遊山水為知已逍遙一生而忘齡- 電腦神手

沒有留言:

張貼留言