当前位置:编程学习 > 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,对

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,