饭后温柔

汉堡与老干妈同嚼 有可乐味
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

讲给自己听:gems中的一个状态机实现

Posted on 2011-10-08 01:43  饭后温柔  阅读(501)  评论(0编辑  收藏  举报

打算做一个基于策略的状态机,不知道直觉准不准.先准备前期的知识.以下是<游戏编程精粹3>中实现的状态机.

状态机 = 状态 + 机. 

machine驱动state.

一般的,对于类似state概念的判断,我们简单的使用if-else或者switch来判断.那当我们可以有很多状态时,if-else或switch搭成的那一大坨代码中的每个判断分支里,就会看到很多相似的代码,比如这些状态所依附的对象,或者相似的计算语句等.很熟悉的:

 1 if(state == a) 
2 {
3 if(event_c) { do_c(); state = c; }
4 if(event_b) { do_b(); state = b; }
5 }
6 else if(state == b)
7 {
8 if(event_a) { do_a(); state = a; }
9 if(event_b) { do_b(); state = b; }
10 }
11 else if(state_c)
12 {
if(event_a) { do_a(); state = a; }
13 if(event_c) { do_c(); state =c; }
14 } else {
15 //do nothing
16 }

在程序的每个逻辑循环中这段代码都会被调用.

这就是个简单的状态机,这里的判断分支已经包含了对进入每个状态后的处理,然后再根据结果或者其他规则把当前状态设为某一个可行值.于是状态就可以根据逻辑保持跳转,每次跳转都意味着我们处理了一些事情.所以这整段if-else就像一台machine,驱动了state的跳转.不过一个术语而已,很贴切.

想起术语就想起微软一些奇怪的东西,比如网络编程的iocp,说白了就是个线程池,干嘛扯了这么长的一串单词让人发怵.其他的什么敏捷编程,驱动开发,结对编程, 我看了一两段已经要嗑药了,他们的口气明明在搞基,不是在编程啊.太把自己当一回事生活就没意思了.发发牢骚,也许是我的层次还没到.

当状态和事件很多时,这段if-else明显会变成一大坨东西.每次要加什么状态事件都得在这里改.然后if-else判断到某一个具体状态,平均需要花费n/2的时间,n是状态个数.

c里可以通过定义一些宏和利用其他机制(消息或回调函数等)把过程简化,<游戏编程精粹1>有c的一个状态机实现.

这里是用c++来实现的,面向对象方式.

首先上面的c代码,state是全局的,至少也是某个模块全局可见的.state往往依附于对象,每个对象都维护自己的state应该是蛮清楚的,所以我们会将state组合到对象中.显然state可以直接声明为对象内的简单枚举值,整型值,那么有没有必要将state实现为一个类呢?看看第3行:

 if(event_c) { do_c(); state = c; }

{ do_c(); state = c; }内的2个语句必然是联系在一起的.那么do_c()是否可以和state_c挂钩呢?即是说state_c内有一个do_c()函数.

c代码中的do_c()是由if-else这个语句调用.我们实现面向对象的状态机的话,这段if-else结构显然要被我们抽象为一个machine类了.machine肯定是至少要模拟if-else的控制结构的类了,但是具体的do_c()是否应该在machine内实现呢?

state内实现do_c(),语义不明确.因为do_c()是实际的宿主要做的事情,那这个state必然要知道很多关于宿主的细节才能do_c()吧.这个解决办法是给state传入一个函数指针.当状态切换时,回调这个函数即可.而machine内实现do_c(),取决于我们是怎么看待machine和宿主关系的.

照我理解,machine应该只是实现一个控制结构,而具体do_c()要干的事情应该跟他没关系,如果machine作为一个构件组合在宿主对象中,确实是这样.跟state情况一样,machine也要知道很多关于do_c()的细节才行.更关键的是,machine要实现很多个类似do_c()的函数,而machine本身的类型也会有很多,会有负责机关状态的machine,也会有负责怪物状态的machine...它们要实现一系列do_c()函数族,每个系列当然依据各自的状态都有自己的一堆do_c().我们是可以把do_c()的函数指针传入machine的,但问题来了,显然我们需要传入多个函数指针,因为machine内有多个状态,为了判断何时该使用哪些函数指针,if-else又来了,而这正是我们需要解决的问题.我觉得machine内使用do_c()的思路杂,可能还是state内调用do_c比较明了,因为一个state只可能对应一个do_c().

前面说过在c代码的每个逻辑循环中if-else代码都会被调用.machine取代if-else后,也应该有个update()之类的函数,供循环中调用.当状态跳转设定好后,我们将在update()中启用这个状态,如state_a->go(),go()函数内将会调用do_c().是的,machine需要知道state,而前面我们已经决定不采用把machine组合到宿主内的手法.事实上,if-else结构确实是应该发生在宿主内部的事.因为在每个逻辑循环中,应该是每个对象都得运行if-else一次,宿主本身确实就是一个状态机machine,当外部事件触发时,宿主自己决定状态怎么跳转,然后怎么处理对应的更新.以面向对象的方式看,当一个事件发生后,大家(各个对象)最好各管各的,不要都得让外部处理,那样我们要处理很多交集,分支,变成一锅粥了.这就是前面所说的看待machine和宿主关系的用意,宿主如果继承自machine,那就省事了.确定后,流程是这样的:

[event触发] --> [machine根据规则设定当前state] --> [外部循环调用machine->update()] --> [调用state->go()] --> [state回调do_c()]

当我们需要添加一个新的state进去时,只需要在machine(即宿主对象)内添加该state,再给该state实现相应的处理函数do_x(),然后将do_x()函数指针传入新state中即可.利用模板,我们甚至不必写新的state类.最后,在machine中添加跳转逻辑就行了.这里第一步[event触发]可能会用到if-else结构,比如简单的按键检查,但event事件大家都可以做成观察者模式或回调函数实现,所以这一步同样也是可以避免多级判断的.

需要注意的是,以上分析我们是建立在这么一个假设上的:宿主内的state可以根据需要添加,而宿主本身的machine固定的.如果宿主需要面对选择多个状态机之一的情况,那采取继承自machine的手法就不行了.但多状态机的行为我认为是可以用单状态机模拟的.究竟是否需要及复杂性比较是个难题.

首先抽象出了状态类.state只需保存函数指针即可,因为c++成员函数指针需要搭配类实例,所以还需要保存一个类指针.使用模板是必然的,回避多个类的成员函数指针所对应的类实例的问题.state的实现:

<state.h>

View Code
#ifndef _fsm_state_h
#define _fsm_state_h

// System Includes
#include <assert.h>

//==================================================================================================
// CState

// CState Class
class CState
{
public:
// Destructor
virtual ~CState() {}

// State Functions
virtual void ExecuteBeginState()=0;
virtual void ExecuteState()=0;
virtual void ExecuteEndState()=0;
};

//==================================================================================================
// CStateTemplate

// CStateTemplate Class
template <class T>
class CStateTemplate : public CState
{
protected:
typedef void (T::*PFNSTATE)(void);
T *m_pInstance; // Instance Pointer
PFNSTATE m_pfnBeginState; // State Function Pointer
PFNSTATE m_pfnState; // State Function Pointer
PFNSTATE m_pfnEndState; // State Function Pointer

public:
// Constructor
CStateTemplate() : m_pInstance(0),m_pfnBeginState(0),m_pfnState(0),m_pfnEndState(0) {}

// Initialize Functions
void Set(T *pInstance,PFNSTATE pfnBeginState,PFNSTATE pfnState,PFNSTATE pfnEndState)
{
// Set Instance
assert(pInstance);
m_pInstance=pInstance;
// Set Function Pointers
assert(pfnBeginState);
m_pfnBeginState=pfnBeginState;
assert(pfnBeginState);
m_pfnState=pfnState;
assert(pfnBeginState);
m_pfnEndState=pfnEndState;
}

// State Functions
virtual void ExecuteBeginState()
{
// Begin State
assert(m_pInstance && m_pfnBeginState);
(m_pInstance->*m_pfnBeginState)();
}
virtual void ExecuteState()
{
// State
assert(m_pInstance && m_pfnState);
(m_pInstance->*m_pfnState)();
}
virtual void ExecuteEndState()
{
// End State
assert(m_pInstance && m_pfnEndState);
(m_pInstance->*m_pfnEndState)();
}
};

#endif

每个状态需在machine初始化好时设置好(传入实例及成员函数指针),对应上述代码中的Set()函数.我们想传入多少个函数指针依你的喜好了.一般来说从跳转到一个状态直到离开时,我们会有3个阶段:初始化-->维持阶段-->离开处理.为这3个阶段各准备一个函数传入既可.

以下将machine成为FSM.这是FSM的实现:

<fsm.h>

View Code
#ifndef _fsm_fsm_h
#define _fsm_fsm_h

// Local Includes
#include "State.h"

// FSM Class
class CFSM
{
protected:
CState *m_pCurrentState; // Current State
CState *m_pNewState; // New State
CStateTemplate<CFSM> m_StateInitial; // Initial State

public:
// Constructor
CFSM();

// Destructor
virtual ~CFSM() {}

// Global Functions
virtual void Update();

// State Functions
bool IsState(CState &State);
bool GotoState(CState &NewState);

virtual void BeginStateInitial() {}
virtual void StateInitial() {}
virtual void EndStateInitial() {}
};

#endif

<fsm.cpp>

View Code
// Local Includes
#include "FSM.h"

// Constructor
CFSM::CFSM()
{
// Initialize States
m_StateInitial.Set(this,BeginStateInitial,StateInitial,EndStateInitial);
// Initialize State Machine
m_pCurrentState=static_cast<CState*>(&m_StateInitial);
m_pNewState=0;
}

//======================================================================================================
// Global Functions

// Update
void CFSM::Update()
{
// Check New State
if (m_pNewState)
{
// Execute End State
m_pCurrentState->ExecuteEndState();
// Set New State
m_pCurrentState=m_pNewState;
m_pNewState=0;
// Execute Begin State
m_pCurrentState->ExecuteBeginState();
}
// Execute State
m_pCurrentState->ExecuteState();
}

//======================================================================================================
// State Functions

// Is State
bool CFSM::IsState(CState &State)
{
return (m_pCurrentState==&State);
}

// Goto State
bool CFSM::GotoState(CState &NewState)
{
// Set New State
m_pNewState=&NewState;
return true;
}

原始的FSM自带了3个状态m_pCurrentState,m_pNewState, m_StateInitial.m_pCurrentState记录当前状态,每次逻辑循环若无跳转则执行之,若有新状态则执行旧状态的离开函数和新状态的初始化函数,并设置m_pCurrentState为新状态. m_StateInitial则作为全局初始化用,重载那3个函数即可,当然你也可以不用.