程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 一個多態性的游戲狀態機系統

一個多態性的游戲狀態機系統

編輯:C++入門知識


任何一款游戲產品,都需要在幾種界面之間進行轉換:logo、trailer、main menu、in-game、settings menu等等,並且會在這些轉換之間處理資源問題。對於實現這樣的轉換,不同的游戲做法有所差異,但基本上會實現一個游戲狀態機系統。狀態機系統在游戲開發中根深蒂固,以至於該系統應該是游戲引擎不可或缺的一個核心部件。

簡單游戲狀態機結構

狀態機的實現方法有很多。相對簡單的有switch-case方法,它通過對游戲狀態進行枚舉化來進行選擇判斷。下面的示例代碼展示了這一點:

[cpp] 
enum GameState 

    GAME_STATE_LOGO = 0, 
    GAME_STATE_TRAILER, 
    GAME_STATE_MAIN_MENU, 
    GAME_STATE_INGAME, 
    GAME_STATE_SETTINGS_MENU, 
}; 
 
void gameCycle( int gameState ) 

    switch( gameState ) 
    { 
        case GAME_STATE_LOGO: {...} 
        case GAME_STATE_TRAILER: {...} 
        case GAME_STATE_MAIN_MENU: {...} 
        case GAME_STATE_INGAME: {...} 
        case GAME_STATE_SETTINGS_MENU: {...} 
    } 

這就是一個相當簡單的游戲狀態機系統,實現起來很直接、簡潔。我們在幾年前的一個java引擎中就使用了這樣的一個狀態機系統(當然,實際代碼要比這復雜一些,但結構是這樣的)。它表現得很好,能夠滿足大多數的需求——有好幾個商業游戲都使用了這個結構。

可是,在那之後,我們在一個新的C++引擎中,卻放棄了這種方法。我們的理由主要有以下幾點:

1)該方法不是OO的,我們的引擎是完全OO的。
2)該系統難以維護——所有的狀態判斷都在gameCycle的switch-case中,我們每增加或者修改一個狀態,都需要在enum和gameCycle中增加新的代碼,這會導致大量的重新編譯。
3)大量的狀態邏輯被集中到了switch-case中,導致代碼臃腫,難以維護。
4)我們希望把每一個game state邏輯交給一個工程師來編寫,這讓我們很難做到。
5)“switch-case在OO中是一種‘壞味道’”思潮影響。

考慮到上面的幾個原因,我們開始探索新的實現方式,然後,我們就有了一個新的、基於多態性的游戲狀態機系統。


狀態機基本結構設計

 
State manager就是狀態管理器(後面簡稱manger),它聚合並管理多個game state(後面簡稱state)。注意,Manager只聚合state的基類指針,而state擁有自己的類體系。因此,manager通過多態的方式處理各種state。

該方法實際上實際上是一種state模式(如果對該模式感興趣,請參考GoF的《設計模式》)。這裡StateMgr相當於該模式的Context類,而GameState相當於該模式的State類。

我們的類初步設計如下:

[cpp]
class GameState 

public: 
    virtual ~GameState() {} 
 
    virtual void cycle() = 0; 
    virtual void draw( GraphicsContext& g ) = 0; 
}; 
 
class StateMgr 

public: 
    void addState( GameState* state ) 
    { 
        m_states.push_back( state ); 
    } 
 
     
    void cycle() 
    { 
        m_curState->cycle(); 
    } 
 
    void draw( GraphicsContext& g ) 
    { 
        m_curState->draw( g ); 
    } 
     
 
private: 
    std::set< GameState* >        m_states; 
    GameState*              m_curState; 
}; 

從代碼中可以很容易看出該系統的工作原理。

GameState是state的base class,提供了GameState::cycle和GameState::draw兩個方法,分別處邏輯更新和渲染兩種工作。該base class是抽象的——只允許完成具體工作的derived class進行實例化。

StateMgr就是manger類,它通過m_states保存所有狀態,並對當前狀態m_curState進行更新和渲染。StateMgr::addState方法用語增加新的游戲狀態。

我們看GameState的具體類的一個例子:

[cpp] 
class GameState_Logo : public GameState 

public: 
    GameState_Logo() 
    { 
        Init m_logoImage and m_logoPos... 
    } 
 
    virtual void cycle() 
    { 
        if( m_logoPos is not identical to the screen center ) 
        { 
            make m_logoPos close to the screen center... 
        } 
    } 
 
    virtual void draw( GraphicsContext& g ) 
    { 
        draw m_logoImage at m_logoPos... 
    } 
 
private: 
    Image*      m_logoImage; 
    Point2D     m_logoPos; 
}; 

上面的類處理進入游戲之後的logo界面。GameState_Logo的ctor初始化logo圖片和位置這兩個成員。GameState_Logo::cycle將logo的位置逐幀移動到屏幕中心。GameState_Logo::draw則在當前位置畫出logo圖片。

 
這樣一個結構設計的好處是什麼呢?

1)StateMgr只依賴GameState,和GameState的derived class沒有耦合。
2)增加任何一個新的state,都不會影響manager,不會導致額外的重新編譯。
3)state模式的全部優勢。
4)該方法是完全OO的。

壞處呢?

1)使用了virtual function抽象,增加了間接層開銷。
2)增加了大量的類源文件,實現起來不夠緊湊。

現在,我們已經有了基本的結構。接下來要做的,就是在這些state之間進行轉換。


游戲狀態轉換設計

游戲中的狀態轉換都會形成一個樹形結構——游戲狀態樹。下圖就是一個典型的游戲狀態樹:

在游戲中,某個時刻只有當前state在運行。因此,游戲將會在樹上進行狀態轉換。比如我們剛剛進入游戲之後,會進入logo界面,然後轉到trailer界面,接下來是主菜單,這幾步都是不可逆的。然後玩家可以選擇in-game(進入游戲)、credits(制作團隊介紹)和settings(設置)這三個狀態,並且可以從這三個狀態返回主菜單狀態。在in-game狀態下可以進入pause menu(暫停菜單)並返回。

此外,我們有時候需要在一種狀態下顯示另一種狀態。比如在pause menu中顯示暫停選項的時候仍然顯示游戲背景(用某種顏色的全屏幕半透明矩形覆蓋使其暗化,並且游戲邏輯此時不會更新)
這意味著給state增加一個parent pointer會很方便:

[cpp] 
class GameState 

 
// ...as above 
 
public: 
    void setParent( GameState* state ) { m_parent = state; } 
    GameState* getParent() { return m_parent; } 
private: 
    GameState*      m_parent; 
}; 

這樣,我們可以這樣實現pause menu的draw方法:

[cpp] 
void GameState_PauseMenu::draw( GraphicsContext& g ) 

    m_parent->draw( g ); 
    draw the transparent mask layer... 
    draw pause menu items... 

我們首先渲染parent,對於pause menu狀態來說,它的parent就是in-game狀態。然後渲染半透明覆蓋層。最後渲染pause menu的選項。

此外,parent pointer對於狀態的轉換也是非常方便的。

為了能夠方便地操縱游戲狀態在狀態樹上進行轉換,我們擴展manager類:

[cpp] 
class StateMgr 

 
// ...as above 
 
public: 
    enum StateOP 
    { 
        STATE_OP_PUSH = 0, 
        STATE_OP_POP, 
    }; 
     
public: 
    void changeState( GameState* newState, int op ) 
    { 
        if( op == STATE_OP_PUSH ) 
        { 
            newState->setParent( m_curState ); 
            m_curState = newState; 
        } 
        else if( op == STATE_OP_POP ) 
        { 
            m_curState = m_curParent->getParent(); 
        } 
    } 
}; 

我們增加了state操作方法StateMgr::changeState並通過兩個操作類型:push和pop,可以很方便地在狀態樹上移動, 

Loading狀態

以上設計有一個很大的問題,你能看出來嗎?似乎所有的state同時存在,這將導致大量的資源存在於內存中。就算是當進入到main menu狀態之後,我們再也無法返回trailer或者logo狀態,它們的資源也還駐留在內存裡。因此,我們需要把這些狀態劃分階段(phase),只讓當前一個phase內的所有state留在內存裡。當游戲從一個phase轉到另一個phase的時候,會釋放舊phase資源,然後載入新phase資源。這通過一個叫做GameState_Loading的類來實現。在釋放舊資源和載入新資源的過程中,GameState_Loading將接管局面,並顯示載入進度界面。我們先把目前的狀態樹劃分phase
整個狀態樹被劃分為4個phase:

logo(logo)
trailer(trailer)
main menu(main menu, credits, settings menu)
in-game(in-game, pause menu)

括號裡面的就是該phase所包含的狀態,會在一個loading過程中全部駐留內存。每一個phase實際上都形成一個子樹,通過一個stack結構和上面的push、pop操作進行轉換。我們擴展上面的類
[cpp] 
class GameState 

// ...as above 
 
public: 
    int getStateOP() const { return m_stateOP; } 
    int getNextPhase() const { return m_phaseToLoad; } 
 
protected: 
    int m_stateOP; 
    int m_phaseToLoad; 
 
}; 
 
class GameState_Loading : public GameState 

public: 
    enum Phase 
    { 
        PHASE_LOGO = 0, 
        PHASE_TRAILER, 
        PHASE_MAIN_MENU, 
        PHASE_INGAME, 
    }; 
 
public: 
    void setNextPhase( int phase ) { m_phaseToLoad = phase; } 
    GameState* getNextState() { return m_nextState; } 
 
    virtual void cycle() 
    { 
        free the old phase... 
        init the new phase frame by frame... 
        save the new states to StateMgr::m_states... 
 
        if( initialization is completed ) 
        { 
            m_nextState = default state of the phase 
            m_stateOP = StateMgr::STATE_OP_NEW_STACK; 
        } 
    } 
 
    virtual void draw( GraphicsContext& g ) 
    { 
        draw the progress interface... 
    } 
 
private: 
    int m_phaseToLoad; 
    GameState* m_nextState; 
}; 
 
class StateMgr 

 
// ...as above 
 
public: 
    enum StateOP 
    { 
        STATE_OP_NONE = -1, 
        STATE_OP_PUSH = 0, 
        STATE_OP_POP, 
        STATE_OP_LOAD, 
        STATE_OP_NEW_STACK, 
    }; 
public: 
    void cycle() 
    { 
        // ...as above 
 
        leaveFrame(); 
    } 
 
private: 
    void leaveFrame() 
    { 
        if( m_curState->getStateOP() != STATE_OP_NONE ) 
        { 
            if( m_curState->getStateOP() == STATE_OP_LOAD ) 
            { 
                GameState_Loading* state = new GameState_Loading; 
                state->setNextPhase( m_curState->getNextPhase() ); 
                m_curState  = state; 
            } 
            else if( m_curState->getStateOP() == STATE_OP_NEW_STACK ) 
            { 
                GameState_Loading* state = static_cast< GameState_Loading*>( m_curState ); 
                changeState( state->getNextState(), STATE_OP_PUSH ); 
                delete state; 
            } 
        } 
    } 
}; 

GameState_Loading類處理所有的狀態轉換工作,這當然包括舊資源釋放和新資源初始化,同時繪制loading界面。

StateMgr新增了兩個操作方式。StateMgr::STATE_OP_LOAD就是開始建立一個新的phase,也就是從舊phase進入loading狀態,然後進行資源載入和新phase中各個state的建立等工作,這些工作在GameState_Loading::cycle中逐幀完成。StateMgr::STATE_OP_NEW_STACK表示從當前loading狀態進入到新建立的phase的默認state中。

StateMgr::cycle方法中新增加調用一個新加入的方法StateMgr::leaveFrame。該方法用於在離開當前幀的時候做一些事情。在這裡我們主要處理state轉換。

GameState增加了兩個成員,m_stateOP用於告訴StateMgr是否需要轉換到另一個phase,默認值是StateMgr::STATE_OP_NONE——什麼也不做。m_phaseToLoad告訴StateMgr它要轉換到哪一個phase。這些phase都定義在GameState_Loading中。比如在logo狀態中需要轉換到trailer狀態,我們可以在GameState_Logo::cycle中寫:

m_stateOP = StateMgr::STATE_OP_LOAD;
m_phaseToLoad = GameState_Loading::PHASE_TRAILER;

StateMgr::leaveFrame就會建立一個loading狀態來進行狀態轉換。當GameState_Loading::cycle完成了初始化,它就會通過StateMgr::STATE_OP_NEW_STACK讓流程進入新的phase的默認state中,正如上面代碼所示。

(我在程序中使用了一些偽碼來避免陷入過多細節,目的是更好的表達出這個結構的思路。如果你非常需要了解該系統的具體實現,可以和我聯系)

 

改進方向

好了!我們已經完成了該系統的基本框架。讀者完全可以根據該框架實現一個自己的游戲狀態機,並取得良好的運行效果。但我還是要說,這和真正游戲中使用的工程級別代碼比,還差一些!下面我會指出一些設計上的改進和擴展,讓該系統更容易在游戲產品中使用。感興趣的讀者可以自行實現。

1 給GameState加上自定義“構造函數”和“析構函數”

如果能給state增加方法:

GameState::onActive
GameState::onUnactive

會讓很多事情事半功倍,且可以得到良好的結構和健壯性。 在StateMgr::changeState中進行state轉換(push和pop)的時候, 給即將停止的state調用onUnactive,給即將運行的state調用onActive,可以給這些state一個機會做一些構造和析構工作(比如釋放和申請一些小資源,或重新初始化一些數據等等)。我們的代碼就強烈地依賴這些方便的小方法。

2 增加state之間的界面過渡

很多游戲在界面過渡之間都使用了一些特效,最常見的就是淡入淡出效果。令人興奮的是,通過上面的狀態機系統增加這樣的過渡效果非常方便。比如我們自己設計了一個叫做FullScreenEffect的基類,通過設計不同的子類來完成不同的過渡效果。

提示:在StateMgr裡面合成該類的一個實例,然後在StateMgr::cycle和StateMgr::draw中調用FullScreenEffect::cycle和FullScreenEffect::draw方法,並通過一些標志來禁止和啟動StateMgr::m_curState的更新和渲染。

3 通過事件分發系統進行狀態改變通知

通過我們之前介紹的事件分發系統(http://blog.csdn.net/popy007/article/details/8242787)來通知系統進行state轉換是個很不錯的設計思路!

4 把StateMgr寫成一個singleton

StateMgr應該只有一個且可以被方便地訪問,寫成一個singleton吧!(關於singleton模式,可以參考GoF的著作)

5 給loading狀態增加一個資源載入管理器

在loading狀態中,我們有時候需要畫出當前的進度比例,這個比例如何計算出呢?很多游戲用的是假數據——只體現一個遞增的效果。但還有些用的是真實數據,對於真實數據來說,該機制和你游戲的資源管理系統有很大關系,這裡我提供一個簡單思路。

我們將需要載入或申請的所有資源進行分類,比如:

字符串
紋理
關卡數據
邏輯腳本
緩存
自定義回調函數
...

給這些資源定義一個通用的結構,並用一個ID來區分。然後這些資源就有了一個統一的表示結構,比如

struct Res;

然後建立一個(你喜歡的任何容器都可以)

std::list< Res >

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved