在游戏中使用面向对象的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,并且通过实体指针访问实体数据和方法,状态切换也是通过状态子类进行的,即各个子类是独立的,这样能很方便的增加和删除状态,只要修改前后状态的切换就可以了。