在游戏中使用面向对象的FSM

本文介绍如何在游戏开发中运用面向对象的方式实现有限状态机(FSM),详细讲解了状态基类、状态子类和状态机类的设计与实现,以及它们之间的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在游戏中使用面向对象的FSM

以前一直是用switch的状态机,因为J2ME没办法用太多类,现在改做c++了,终于可以试一试面向对象的状态机了,代码果然简洁了好多。参考的是《Programming Game AI by Example》第2章,大约改了下。首先要定义一个状态基类:

template <class entity_type> class State { public: State(){} virtual ~State(){} //executed when entity enter this state virtual void OnEnter(entity_type*) = 0; //entity update this state virtual void Update(entity_type*, float dt) = 0; //entity render in this state virtual void Render(entity_type*, float dt) = 0; //executed when entity exit this state virtual void OnExit(entity_type*) = 0; };

这是一个模板类,因为状态要对应不同的所有者,比如游戏状态的所有者是游戏类Game,而主角状态这里的entity_type就是主角类Hero。总之,状态对于状态拥有者实体是依赖关系。因为实体有很多种状态,所以要定义具体的状态子类,比如主菜单状态是游戏状态类的子类:

class GSMainMenu: public State<Game> { private: GSMainMenu(){} GSMainMenu(const GSMainMenu&); GSMainMenu& operator=(const GSMainMenu&); public: static GSMainMenu* GetInstance(); public: virtual void OnEnter(Game* pGame); virtual void Update(Game* pGame, float dt); virtual void Render(Game* pGame, float dt); virtual void OnExit(Game* pGame); };

状态子类采用了单件模式,所以状态子类中不能存放对于拥有者实体来说是自有的数据,比如有很多士兵,那么士兵的状态子类中就不能存放生命值,只能存放在实体中,在状态类中通过实体的指针去访问。我觉得状态采用单件更清晰些,数据就在实体中维护吧,当然也许每个实体有一个对应的状态实例也有合适的使用之处。

然后需要定义状态机类:
template <class entity_type> class StateMachine { public: explicit StateMachine(entity_type* pOwner) :m_pOwner(pOwner), m_pCurrentState(NULL), m_pPreviousState(NULL), m_pGlobalState(NULL) { } virtual ~StateMachine(){} //set first state, will call OnEnter void SetFirstState(State<entity_type>* pState) { m_pCurrentState = pState; m_pCurrentState->OnEnter(m_pOwner); } //set global state void SetGlobalState(State<entity_type>* pState) { m_pGlobalState = pState; } //call this to update the FSM void Update(float dt) const { if(m_pGlobalState) m_pGlobalState->Update(m_pOwner, dt); if(m_pCurrentState) m_pCurrentState->Update(m_pOwner, dt); } //call this to do render void Render(float dt) const { if(m_pGlobalState) m_pGlobalState->Render(m_pOwner, dt); if(m_pCurrentState) m_pCurrentState->Render(m_pOwner, dt); } //change to a new state void ChangeState(State<entity_type>* pNewState) { assert(pNewState!=NULL && "<StateMachine::ChangeState>: trying to change to NULL state>"); //auto-set previous state m_pPreviousState = m_pCurrentState; //exit the previous state m_pCurrentState->OnExit(m_pOwner); //change to the new state m_pCurrentState = pNewState; //on enter the new state m_pCurrentState->OnEnter(m_pOwner); } //change state back to the previous state void RevertToPreviousState() { assert(m_pPreviousState!=NULL && "<StateMachine::RevertToPreviousState>: previous state is NULL>"); ChangeState(m_pPreviousState); } //returns true if the current state's type is equal to the type of the //class passed as a parameter. bool IsInstate(const State<entity_type>& st) const { return typeid(*m_pCurrentState) == typeid(st); } State<entity_type>* GetCurrentState() const{return m_pCurrentState;} State<entity_type>* GetGlobalState() const{return m_pGlobalState;} State<entity_type>* GetPreviousState() const{return m_pPreviousState;} private: //a pointer to the owner entity of this FSM entity_type* m_pOwner; State<entity_type>* m_pCurrentState; State<entity_type>* m_pPreviousState; State<entity_type>* m_pGlobalState; };

状态机类也是模板类,道理一样,每种实体必须有一个对应的状态机类,状态机和实体是聚合关系,状态机保存了实体的指针,从而让状态可以访问实体。状态机和状态既有聚合关系也有依赖关系,状态机维护了当前状态,上一个状态,以及一个全局状态,从而对当前及全局状态进行Update和Render,并且通过 ChangeState管理状态的切换,切换状态时要调用旧状态的OnExit和新状态的OnEnter,设置第一个状态比较特殊,所以我加了 SetFirstState,只有对新状态的处理,原书中是直接设置,这样不会调用OnEnter了。

最后看实体怎么使用状态机,比如有一个Game类,首先他需要拥有一个状态机实例(也是聚合关系):
class Game
{
StateMachine<Game>* m_pFSM;
};

这个实例可以在Game构造时构造好:
Game::Game(void)
{
m_pFSM = new StateMachine<Game>(this);
}
析构时删除
Game::~Game()
{
delete m_pFSM;
}
游戏初始化时可设置第一个状态:
m_pFSM->SetFirstState(GSLogo::GetInstance());//这里是显示Logo

在Game Update和Render时分别调用状态机的Update和Render即可
void Game::Update(float dt)
{
...
m_pFSM->Update(dt);
...
}

void Game::Render(float dt)
{
...
m_pFSM->Render(dt);
...
}

那么状态的切换呢?是在状态子类中进行的,比如游戏从主菜单进入关卡,在主菜单状态的Update逻辑中:
void GSMainMenu::Update(Game* pGame, float dt)
{
...
if(Start按钮按下)
{
pGame->GetFSM()->ChangeState(GSLevelLoading::GetInstance());
}
...
}
游戏类有GetFSM方法让状态类得到状态机,切换状态时直接调用相应状态类的GetInstance得到唯一的状态实例

总结:
1)实体拥有一个状态机,实体通过状态机的Update和Render来实现不同状态下的逻辑和渲染
2)状态机指向他的拥有者实体,并且聚合了状态,状态机通过Update和Render来让当前状态执行逻辑和渲染,并且状态机提供了统一的状态切换流程,即先Exit前一状态,然后设置当前状态,并Enter新状态
3)每个实体对应一个状态基类(通过模板化),以及若干子类(通过继承模板基类),状态子类是实体状态的具体实现者,状态子类进行实际的Update和 Render,并且通过实体指针访问实体数据和方法,状态切换也是通过状态子类进行的,即各个子类是独立的,这样能很方便的增加和删除状态,只要修改前后状态的切换就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值