任何一款游戲產品,都需要在幾種界面之間進行轉換: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 >