現在的位置: 首頁 > 綜合 > 正文

DirectX 9.0c遊戲開發手記之RPG編程自學日誌之3: Preparing for the Book (準備工作)(中)

2019年11月14日 ⁄ 綜合 ⁄ 共 9244字 ⁄ 字型大小 評論關閉

    本文由哈利_蜘蛛俠原創,轉載請註明出處!有問題請聯繫2024958085@qq.com

 

        上一期我們只講了第一章的第3節。這次爭取講到第6節。為了方便,我把本書的各小節標題再次列在下面:

1、 DirectX

2、 SettingUp the Compiler (設置編譯器)

3、 GeneralWindows Programming (一般的Windows編程)

4、 Understandingthe Program Flow (理解程序流)

5、 ModularProgramming

6、 Statesand Processes

7、 HandlingApplication Data (處理應用程序的數據)

8、 Buildingan Application Framework (構建一個應用程序框架)

9Structuring a Project (結構化一個項目)

10Wrapping UpPreparations (總結準備工作)

 

 

4部分:Understanding the Program Flow (理解程序流)

 

        下面仍舊是原文翻譯部分:

 

---------------------------------------------------------------------------------------------------------------------------------

 

        當沉浸在一個巨大的項目中時,我們要想不被各種各樣的「家務活」累垮實在是太難了,這些家務活包括在增加、修改或者刪除某項東西後進行的相應的代碼修正。這些瑣事佔去了太多本可以用於我們的遊戲開發的時間(一個字——不值當!)

        如果你一開始就對你所需要的東西了解得非常透徹的話,那麼你就有能力組織你的程序的操作流(稱為程序流,program flow)並且保證你可以輕鬆地進行修改。因為你已經謝了一個設計文檔(你當然寫了,難道不是嗎?)PS:此書的前一版在這之前花了很多的篇幅來介紹如何寫一個RPG的設計文檔。推薦去看一下。看來作者很懶,更新到第二版的時候原封不動地照抄……),因此剩下的沒什麼要做的了,只需要建立程序流的結構。

        一個典型的(遊戲)程序首先要初始化所有的系統和數據,然後進入主循環(main loop)。主循環是大多數事情發生的地方。依賴於正在發生的遊戲狀態(標題界面(title screen)、菜單界面(menu screen)、遊戲內操作(in-game-play)等到),你需要用不同的方式來處理輸入和輸出。

        下面列出的是你在一個標準的遊戲應用程序中你需要遵循的步驟:

1、  初始化各個系統(Windows,圖形,輸入,聲音等等)。

2、  準備數據(載入設置文件(configurationfiles))。

3、  安置默認的狀態(configurethe default state)(一般來說是標題界面)。

4、  開始主循環。

5、  確定狀態,然後通過獲取輸入、處理輸入然後輸出來進行處理。

6、  回到步驟5直到應用程序結束;那個時候轉到步驟7。

7、  清理數據(釋放內存資源等等)。

8、  釋放系統(Windows,圖形,輸入等等)。

 

        步驟1-3對於所有的遊戲來說都是典型的:建立整個系統,載入必需的支持文件(圖形,聲音等等),並且準備好進行實際的遊戲操作。你的應用程序會花費大多數時間來做遊戲內處理(步驟5),而這一步驟可以分解為三部分:幀前處理(pre-frame processing)、每幀處理(per-frame processing)和幀後處理(post-frame processing)。

        幀前處理處理的是小任務,例如取得當前的時間(對於基於時間的事件,例如同步(synching))以及其他的細節(例如更新遊戲的元素)。每幀處理處理的是更新物體(如果沒有在幀前階段中搞定的話)以及渲染圖形。而幀後處理處理的是剩下的功能,例如根據時間差進行同步,或者甚至顯示已經渲染好的圖形。

        Here』s a kicker for you. (Sorry,不知道怎麼翻譯……。)在你的遊戲中,你也許有多個每幀狀態:一個用於操控主菜單,一個操控遊戲內操作,等等。維護像那樣的多個狀態可能會導致一些混亂的代碼,但是利用一種稱為狀態處理(state-processing)的機制可以在一定程度上減輕負擔。你會在本章稍後的一節「應用程序狀態」中學到更多關於狀態處理的知識。

        清除數據和關閉系統(步驟7和8)釋放了系統以及你開始遊戲時分配的資源。圖形需要從內存中獲得釋放,應用程序窗口需要銷毀,如此等等。跳過這些步驟是絕對地當然不行的,因為這回讓你的系統處於一種奇怪的狀態,並且可能會導致系統崩潰!

        程序流中的每一個步驟都由一個相關的代碼塊來代表,所以那些代碼塊的結構越好,你的應用程序就越容易建立。為了幫助構建你的程序代碼,你可以使用一種常用的編程技術,叫做modular programming.

 

---------------------------------------------------------------------------------------------------------------------------------

 

        好啦,第4節翻譯完成了。應該還不錯吧?這一節的內容不多,也很容易理解,應該沒什麼問題了吧?那麼好的,我們下面進入第5節的內容!

 

 

5部分:Modular Programming

 

        這個標題我不知道怎麼翻譯,暫時翻譯成「模塊化編程」。下面仍舊是原文翻譯部分:

 

---------------------------------------------------------------------------------------------------------------------------------

 

        Modular programming是現今的編程中使用的很多技術——包括C++和COM——的基礎。Modular programming創建獨立的代碼組件,這些代碼組件是完全自給自足的;它們不需要外部的幫助,並且在很多情況下,可以用在大多數操作平台上。

        想像一個真正的modular programming系統,你用它編寫的一個程序可以在所有的現存的電腦上運行!你也許不需要等很長時間——這樣的事情已經快要來臨了(或者已經來臨了)。你可以將一個modular program看成一個C++類。它包含它自己的變數和函數。如果代碼寫得很合適,那麼這個類不需要外部的幫助。

        給定你的類,那麼任何應用程序都可以利用這個類的特徵,而這隻需要知道如何調用這些函數(通過利用函數原型)。調用一個類的函數很簡單,只需要實例化一個類,並且調用它的函數:

cClass MyClass;       // Instance the class
MyClass.Function1();       // Call a class function

        為了得到真正的模塊化功能(modularity),你的代碼必須保護其數據。要做到這一點很容易,因為使用C++,你可以把變數標記為protected。為了獲取到這些類變數的訪問權,你必須寫外部代碼可以使用的public函數。這實際上是COM的基礎。

        瞧一瞧一些代碼,它們說明了我正在談論的東東。這裡一個類包含一個計數器。你可以給這個計數器增值(increment)、設定它為某個特定值以及取得當前的計數器數值,這些都是通過使用如下的類:

class cCounter
{
private:
    DWORD m_dwCount;
 
public:
    cCounter() { m_dwCount = 0; }
 
    BOOL Increment() { m_dwCount++; returnTRUE; }
    BOOL Get(DWORD *Var) { *Var = m_dwCount;return TRUE; }
    BOOL Set(DWORD Var) {m_dwCount = Var;return TRUE; }
};

        cCounter類將變數m_dwCount設為私有的(private)。這樣,即使是派生類都不能夠訪問它。其他的函數都可以顧名思義。唯一的值得注意的函數是Get,它以指向一個DWORD變數的指針為參數。這個函數將當前的計數器數值存儲在那個變數里,並且返回TRUE(正如所有其他的cCounter類函數那樣——當然,除了構造函數以外)。

        剛才的那個是一個非常基本的modular programming的例子。一個更加複雜的例子是DirectX,它是完全地modular。如果你想僅僅使用DirectX的一個特徵,比如說DirectSound,那麼你只需要包含適當的DirectSound對象。DirectSound的運行並不依賴於其他的DirectX組件。

        在整本書中,我一直在採用modular編碼技術,尤其是為了創建一個遊戲庫核心,這些庫彼此不依賴。每一個庫都封裝了一系列它自己專屬的特徵——圖形庫僅僅處理圖形,輸入庫僅僅處理輸入,等等。為了使用這些庫,只需要在你的項目中包含它們,然後披荊斬棘前進吧!

 

---------------------------------------------------------------------------------------------------------------------------------

 

 

6部分:States and Processes

 

        下面仍舊是原文翻譯部分:

 

---------------------------------------------------------------------------------------------------------------------------------

 

        努力地優化你的程序流應該成為你從一開始就最優先考慮的事情之一。當應用程序代碼比較小的時候,也許你還能夠很容易地進行管理。然而,一旦該應用程序變得龐大起來,那麼它就會變得越來越難處理了,甚至進行最小的改動都需要重寫很多代碼。

        想想這種情景——你的遊戲項目正在進行中,然後你決定在遊戲中增加一個新的功能,亦即任何時候用戶按下I鍵時,就會打開一個物品展示界面。這個物品展示界面只會在玩遊戲的時候才會出現,而在主菜單界面的時候是不會出現的。這意味著你必須插入一些代碼來完成以下的事情:檢測什麼時候用戶按下了I鍵,並且當按下I鍵的時候,代碼要渲染物品展示界面而不是正常的遊戲畫面。

        如果你把自己限制在這樣的思路——使用單獨一個函數根據用戶正在做什麼(例如正在觀察主菜單或者正在玩遊戲)來渲染每一個顯示界面——里的話,你很虧就會意識到渲染函數會變得超級龐大和複雜,因為它必須包括遊戲能夠處於的所有狀態(states)。

 

 

1.6.1 應用程序狀態

 

        我剛才提到了狀態了嗎?那麼好吧,啥叫狀態呢?狀態操作狀態(stateof operation)的簡稱,表示你的應用程序正在著手執行的過程(process)。你的遊戲的主菜單是一個狀態,而遊戲中(in-game-play)狀態也是一個狀態。而你想在遊戲中添加的物品展示也是一個狀態。

        當你把各種各樣的狀態添加進你的應用程序的時候,你需要提供一種方法來決定如何根據當前的操作狀態(它會隨著項目的執行而發生改變)來處理這些狀態。在每一幀中決定你的應用程序需要處理哪個狀態會導致如下所示的恐怖代碼:

switch(CurrentState){
  case STATE_TITLESCREEN:
    DoTitleScreen();
break;
 
     case STATE_MAINMENU:
       DoMainMenu();
       break;
 
     case STATE_INGAME:
       DoGameFrame();
       break;
}

        哎呦喂!你可以看出來像上面的那種方法是行不通的,尤其是當你的遊戲有巨多的狀態來處理時,並且如果你試圖在每一幀中處理狀態的話,情況還會更糟!相反,你可以使用一種我喜歡稱之為state-based programming(基於狀態的編程,簡稱SBP)的技術。本質上,SBP基於一個狀態棧對程序的執行進行分流(引導)。(文:Inessence,
SBP branches (directs) execution based on a stack of states.
)每一個狀態代表一個對象或者一族函數。當你需要函數的時候,你可以把它們添加進這個棧里。當你用完這些函數的時候,將它們從棧中移除。你可以在下圖中看到這一點。(覺得有點小問題:圖中寫的是First InFirst
Out
,但是我覺得既然是stack,應該是Last InFirst Out。)

        你通過使用一個狀態管理器(state manager)來添加、移除以及處理狀態。當一個狀態被添加進來的時候,它被推進了這個棧,於是當狀態管理器運作的時候就有了當前的控制權。一旦彈出來後(Once popped),最頂上的狀態被丟棄,使得第二高的狀態成為下一個被處理的狀態。

        由於上述原因,你需要實現一個狀態管理器來接受指向函數(它們代表狀態)的指針。將一個狀態推到棧上(Pushing a state)就是將它的函數指針添加到棧上。調用這個處理棧上的最頂端的狀態的狀態管理器是你的事情,。狀態管理器實際上非常容易實現,所以讓我來給你展示一個狀態管理器的例子:(下面用到了linked list的知識,學過數據結構的同學應該很熟悉的。)

class cStateManager
{
  // A structure that stores a function pointer and linked list
  typedef struct sState {
    void  (*Function)();
    sState *Next;
  } sState;

  protected:
    sState *m_StateParent; // The top state in the stack
                           // (the head of the stack)

  public:
    cStateManager() { m_StateParent = NULL; }

    ~cStateManager() 
    {
      sState *StatePtr;

      // Remove all states from the stack
      while((StatePtr = m_StateParent) != NULL) {
        m_StateParent = StatePtr->Next;
        delete StatePtr;
      }
    }

    // Push a function on to the stack
    void Push(void (*Function)())
    {
      // Don't push a NULL value
      if(Function != NULL) {
        // Allocate a new state and push it on stack
        sState *StatePtr = new sState;
        StatePtr->Next = m_StateParent;
        m_StateParent = StatePtr;
        StatePtr->Function = Function;
      }
    }

    BOOL Pop()
    {
      sState *StatePtr = m_StateParent;

      // Remove the head of stack (if any)
      if(StatePtr != NULL) {
        m_StateParent = StatePtr->Next;
        delete StatePtr;
      }

      // return TRUE if more states exist, FALSE otherwise
      if(m_StateParent == NULL)
        return FALSE;
      return TRUE;
    }

    BOOL Process()
    { 
      // return an error if no more states
      if(m_StateParent == NULL)
        return FALSE;

      // Process the top-most state (if any)
      m_StateParent->Function(); 

      return TRUE;
    }
};

        你可以看到這個類很小,但是不要因此被愚弄了。有了cStateManager對象,你就可以持續地根據需要往裡面添加狀態,而在幀渲染函數中,你只需要調用Process函數,然後就可以確保正確的函數被調用了。這裡是一個例子:

cStateManager SM;

// Macro to ease the use of MessageBox function
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// State function prototypes - must follow this prototype!
void Func1() { MB("1"); g_StateManager.Pop(); }
void Func2() { MB("2"); g_StateManager.Pop(); }
void Func3() { MB("3"); g_StateManager.Pop(); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  SM.Push(Func1);
  SM.Push(Func2);
  SM.Push(Func3);
  while(SM.Process() == TRUE);

}

        用前面的小程序,你可以跟蹤三個狀態,每一個狀態顯示一個帶有數字的消息盒子。每一個狀態將自己從棧中彈出,並且讓下一個狀態做好準備,直到最終所有的狀態都已經用完,然後程序就退出了。非常簡潔,對吧?

        將前面的代碼想像成是嵌入在每幀的消息泵(message pump)中的。比如說你需要給用戶顯示一個消息,但是,該死!你正在遊戲中界面常式當中(you』re in the middle of the in-game screen routines)。沒問題!只需要把消息顯示函數放在棧上,然後在你下一次處理遊戲的某一幀時調用處理函數就OK了!

1.6.2 Processes

 

        繼續前進,請允許我向你介紹另外一個簡化每幀的函數調用的技術。如果你正在使用分離的組件來操控中間函數(medial functions)(稱為processes),例如輸入、網路和聲音處理,那麼你可以不單獨地調用它們,而是可以創建一個對象來一次性操控它們的全部。

class cProcessManager
{
  // A structure that stores a function pointer and linked list
  typedef struct sProcess {
    void  (*Function)();
    sProcess *Next;
  } sProcess;

  protected:
    sProcess *m_ProcessParent; // The top state in the stack
                               // (the head of the stack)

  public:
    cProcessManager() { m_ProcessParent = NULL; }

    ~cProcessManager() 
    {
      sProcess *ProcessPtr;

      // Remove all processes from the stack
      while((ProcessPtr = m_ProcessParent) != NULL) {
        m_ProcessParent = ProcessPtr->Next;
        delete ProcessPtr;
      }
    }

    // Add function on to the stack
    void Add(void (*Process)())
    {
      // Don't push a NULL value
      if(Process != NULL) {
        // Allocate a new process and push it on stack
        sProcess *ProcessPtr = new sProcess;
        ProcessPtr->Next = m_ProcessParent;
        m_ProcessParent = ProcessPtr;
        ProcessPtr->Function = Process;
      }
    }

    // Process all functions
    void Process()
    { 
      sProcess *ProcessPtr = m_ProcessParent;

      while(ProcessPtr != NULL) {
        ProcessPtr->Function();
        ProcessPtr = ProcessPtr->Next;
      }
    }
};

        這個簡單的對象很像之前的那個cStateManager對象,但是有一個大大的不同。這個cProcessManager對象僅僅增加process;它不會移除它們。這裡有一個使用cProcessManager類的例子:

cProcessManager PM;

// Macro to ease the use of MessageBox function
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Processfunction prototypes - must follow this prototype!
void Func1() { MB("1"); }
void Func2() { MB("2"); }
void Func3() { MB("3"); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  PM.Add(Func1);
  PM.Add(Func2);
  PM.Add(Func3);
  PM.Process();
  PM.Process();

}

        注意到每次Process函數被調用的時候,棧上的所有processes都被調用了(如下圖所示)。這對於頻繁地調用函數來說非常有用。你可以針對不同的情形設計不同的process manager——例如,一個操控輸入和網路處理,而另一個操控輸入和聲音。

---------------------------------------------------------------------------------------------------------------------------------

 

        好啦,這一期就到此結束啦!內容有點多啊,而且對於沒有學過數據結構的同學來說可能有點難。那麼大家就好好消化消化吧,咱們下期再見吧!

 

        注意:前面的兩個例子實際上是完整的代碼,大家建立Win32項目類型的解決方案然後把代碼放進去編譯就OK了。注意在項目屬性中要把字符集改變為Multi-byte類型,否則會出錯。


抱歉!評論已關閉.