一个多态性的游戏状态机系统
任何一款游戏产品,都需要在几种界面之间进行转换: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,对
补充:软件开发 , C++ ,