前言:
之前的工作中使用有限狀態機模型管理遊戲流程,趁這段工作空窗期總結一下狀態機的相關知識。
一.狀態機相關概念:
有限狀態機(Finite State Machine),以下簡稱FSM,是一種描述離散狀態的數學模型,在FSM模型中,可以將系統分解成有限數目的離散穩定,相互獨立又有聯繫的狀態。FSM中最基本的三點:
(1)狀態的定義;
(2)狀態的遷移;(在FSM中,一般通過事件使狀態發生遷移)
(3)伴隨狀態遷移而產生的動作;
除了這最基本的三點,還有其他的擴展,如:狀態監護條件,狀態進入或離開動作,狀態機嵌套(子狀態機),並行狀態機,狀態歷史記錄(深層,淺層記錄)等等。
二.在遊戲中的應用
這裏只談棋牌遊戲類型,其本質是在遊戲規則的約束下,通過一系列事件來改變自身的狀態,直到遊戲結束。以麻將遊戲爲例,其狀態機圖類型如1.0圖(簡化版):
圖1.0 麻將狀態機圖
橢圓形爲狀態,例如:遊戲開始,搖色子定莊家,閒家下注等;
方形爲驅動狀態變遷的事件,例如:定時器事件,下注完成事件等;
注意到,狀態的拓撲結構是靜態的,而驅動狀態變遷的事件發生是動態的。意思是遊戲一旦開始跑起來,當前狀態的變遷路徑是固定的,A狀態只能變遷到B狀態或C狀態,而不能變遷到D狀態;而事件何時發生,能不能發生則和當局遊戲過程,玩家的選擇相關。
因此,遊戲維護人員只要看遊戲的狀態拓撲結構就能知道整個遊戲的流程是怎麼樣的。另外,當我們把遊戲的狀態拓撲關係放在配置文件中,例如用XML文件來配置遊戲的狀態變遷關係,則當需要改變遊戲流程時可以只改配置文件,而無需改動代碼。一個例子:如果現在不需要閒家下注,則只需要改配置文件,將搖色字定莊家狀態的下一個變遷狀態定爲搖色字定財神狀態就可以了。
三.現有的FSM工具
(1)UML狀態機圖:
在設計階段,可以藉助UML狀態機圖進行建模,畫出即將要實現的狀態機原形。上面的圖1.0麻將狀態機就是一個簡單的UML狀態機圖。(我畫圖的軟件沒找到標示事件的圖形,所以用一個矩形代替了事件的圖形。)
UML狀態機圖提供了描述一個狀態機的所有元素的語法,例如:狀態,事件,狀態轉移,監護條件,起始狀態和結束狀態等。具體的語法可以查閱相關的資料
(2)開源庫:boost::statechart
boost的statechart庫爲FSM開發提供了很多好用的特性,它能夠方便的將UML狀態機圖轉化爲可執行形代碼,即statechart支持FSM的很多語法,包括嵌套狀態機,深層/淺層歷史記錄,事件延遲等。具體的使用可以查閱對應的文檔,這裏就不多講述了。
四.自造輪子
有時不想在項目中引入第三方庫,或者第三方庫使用成本比較高,那麼就得考慮自己實現一個滿足使用需求的庫。自己實現的庫一般以滿足項目使用需求就可以,所以能夠做到夠輕量級,且使用和學習成本不高。
之前在做棋牌遊戲的項目中,寫過一個FSM,不能稱之爲庫吧,畢竟只是實現了最基礎的功能。這個FSM的設計基於對象而不是面向對象,即並沒有把FSM中的狀態,轉移,事件等都抽象成對象,但是整個FSM被封裝成一個類,並且能夠作爲基類被繼承。
(一)FSM基類
FSM的基類只提供狀態機最基本的功能點:狀態,狀態遷移,遷移動作。FSM有一個當前的狀態,和一個路由表。路由表由多個路由項組成,每個路由項表示:一個源狀態在特定事件的驅動下遷移到目的狀態,並伴隨着遷移動作的發生。路由項定義如下:
//狀態機路由關係,指一個源狀態在一個事件的觸發下遷移到另一個狀態,並伴隨着某個動作的發生
template<class _TypeEvent , class _TypeStatus , class _TypeParam>
struct RouteRelation{
RouteRelation(_TypeStatus srcS , _TypeStatus destS , _TypeEvent event , std::function<void (_TypeParam)> act) : srcStatus(srcS),
destStatus(destS),transitionEvent(event),transitionAction(act)
{
}
virtual ~RouteRelation() {}
_TypeStatus srcStatus ; //源狀態
_TypeStatus destStatus ; //目的狀態
_TypeEvent transitionEvent ; //導致狀態發生變遷的事件ID
std::function<void (_TypeParam)> transitionAction ; //狀態變遷時發生的動作
};
注意到:
(1)這是一個模板類,則我們能夠自定義事件的類型_TypeEvent,狀態的類型_TypeStatus,還有一個模板參數_TypeParam則是遷移動作的參數類型,遷移動作的函數原型爲:std::function<void (_TypeParam)>,這種方式使得我們可以自定義遷移動作的函數調用形式。
(2)其虛構函數是虛析構函數。一般來說,作爲基類的類才需要虛析構函數,沒錯,這裏就是想讓這個模板類能夠被繼承,下面會講到讓這個類能夠被繼承的好處。另外一個,有了虛函數,我們才能對這個類類型使用dynamic_cast。
定義了路由表項後就可以定義FSM了,前面說了,FSM管理着一個路由表和一個當前狀態,其定義如下:
//對_TypeEvent類型的要求:必須有==操作
//對_TypeStatus類型的要求:必須有hash函數和==操作
template<class _TypeEvent , class _TypeStatus , class _TypeParam>
class FSM{
protected:
typedef std::shared_ptr<const RouteRelation<_TypeEvent , _TypeStatus,_TypeParam>> P_C_ROUTERELATION ;
typedef std::unordered_multimap<_TypeStatus , P_C_ROUTERELATION> ROUTETABLE ;
public:
virtual ~FSM(){}
protected:
//當前狀態
_TypeStatus m_curStatus ;
//源狀態到路由表的映射,一對多的關係,因爲一個源狀態可以在不同的事件驅動下變遷到不同的目的狀態
ROUTETABLE m_routeTable ;
};
模板參數就不說了,和上面的路由項一樣。路由表是一對多的關係,即一個源狀態可以根據不同的事件跳到不同的目的狀態,基於此,用unordered_multimap來存儲路由關係。
需要注意的是,路由表中存儲的是路由項的指針,一方面是節省內存空間;另一方面更重要,只有存儲指針,我們才能使用C++的多態特性,即路由表裏不僅能存儲上面定義的路由項類RouteRelation,也能存儲其子類。
接着,就可以添加FSM類的操作了,構造函數,析構函數,註冊,事件通知等。事件通知一般有同步和異步兩種方式,這裏我們用的是同步方式,如果需要用異步方式,則需重新實現。整個FSM代碼:
//對_TypeEvent類型的要求:必須有==操作
//對_TypeStatus類型的要求:必須有hash函數和==操作
template<class _TypeEvent , class _TypeStatus , class _TypeParam>
class FSM{
protected:
typedef std::shared_ptr<const RouteRelation<_TypeEvent , _TypeStatus,_TypeParam>> P_C_ROUTERELATION ;
typedef std::unordered_multimap<_TypeStatus , P_C_ROUTERELATION> ROUTETABLE ;
public:
FSM(_TypeStatus startStatus , _TypeStatus endStatus) : m_curStatus(startStatus),m_startStatus(startStatus),m_endStatus(endStatus){}
virtual ~FSM(){}
//返回false表明該路由關係已經註冊;返回true註冊成功
bool Register(P_C_ROUTERELATION pRouteRelation)
{
if(IsInRouteTable(pRouteRelation))
return false ;
m_routeTable.insert(std::make_pair(pRouteRelation->srcStatus , pRouteRelation)) ;
return true ;
}
/*以同步方式執行事件通知,可以通過覆蓋該函數實現不同的通知方式
* 狀態變遷返回true,否則返回false
*/
virtual P_C_ROUTERELATION Notify(_TypeEvent event , _TypeParam param)
{
P_C_ROUTERELATION NullSp ;
if(m_curStatus == m_endStatus)
return NullSp;
ROUTETABLE::size_type count = m_routeTable.count(m_curStatus) ;
if(count > 0)
{
ROUTETABLE::const_iterator it = m_routeTable.find(m_curStatus) ;
if(it != m_routeTable.end())
{
for(; count > 0 ; --count)
{
if((it->second) && (it->second->transitionEvent == event))
{
//狀態變遷
m_curStatus = it->second->destStatus ;
//執行狀態轉移動作
if(it->second->transitionAction)
it->second->transitionAction(param) ;
return it->second ;
}
}
}
}
return NullSp ; //返回空的指針
}
//將當前狀態置爲開始狀態
void ReSetCurStatus(){m_curStatus = m_startStatus ;}
_TypeStatus getCurStatus() const {return m_curStatus ;}
_TypeStatus getStartStatus() const {return m_startStatus ;}
_TypeStatus getEndStatus() const {return m_endStatus ;}
protected:
bool IsInRouteTable(P_C_ROUTERELATION pRouteRelation)
{
ROUTETABLE::size_type count = m_routeTable.count(pRouteRelation->srcStatus) ;
if(count > 0)
{
ROUTETABLE::const_iterator it = m_routeTable.find(pRouteRelation->srcStatus) ;
if(it != m_routeTable.end())
{
for(;count > 0 ; --count)
{
//源狀態和事件確定目的狀態,所以這兩個項一樣的話則標明已經有該路由關係
if((it->second->srcStatus == pRouteRelation->srcStatus) && (it->second->transitionEvent == pRouteRelation->transitionEvent))
return true ;
++it ;
}
}
}
return false ;
}
//開始和結束狀態
const _TypeStatus m_startStatus ;
const _TypeStatus m_endStatus ;
//當前狀態
_TypeStatus m_curStatus ;
//源狀態到路由表的映射,一對多的關係,因爲一個源狀態可以在不同的事件驅動下變遷到不同的目的狀態
ROUTETABLE m_routeTable ;
};
(二)實際項目中的使用
前面說了,FSM基類只提供了最基本的功能,如果項目中需要用到其他功能,例如在進入一個新狀態時,需要執行一個進入動作,則可以通過擴展路由項類RouteRelation和FSM類實現,這就是前面說的把這兩個類設計成能被繼承的原因。接下來利用FSM來實現一個遊戲狀態機GameFSM。
首先,定義事件類型,狀態類型,參數類型等:
enum emGFsmEventId{
emGFsmEInvalid = -1,
emGFsmEAuto = 0,
emGFsmETimer,
emGFsmEGameBegin
};
enum emGFsmStatusId{
emGFsmSInvalid = -1,
emGFsmSGameBegin = 0,
emGFsmSMakeNt,
emGFsmSRandCard,
emGFsmSGiveNextToken
};
struct GFsmParam{
GFsmParam():transitionParam(0),enterParam(0){}
GFsmParam(LPARAM tP , LPARAM eP) : transitionParam(tP),enterParam(eP){}
LPARAM transitionParam; //轉移動作參數
LPARAM enterParam; //進入動作參數
};
然後擴展路由項,加了一個進入狀態後的動作:
//FSM的路由關係中只定義了轉移動作,如果需要其他動作可以通過繼承基類來擴展,例如想擴展一個進入新狀態後的動作
struct GameRouteRelation : public RouteRelation<emGFsmEventId , emGFsmStatusId , GFsmParam>
{
GameRouteRelation(emGFsmStatusId srcS , emGFsmStatusId destS , emGFsmEventId event , std::function<void (GFsmParam)> tranAct ,
std::function<void (GFsmParam)> enterAct):RouteRelation(srcS , destS , event , tranAct),enterAction(enterAct){}
std::function<void (GFsmParam)> enterAction; //進入新狀態後執行的動作
};
最後,通過繼承FSM來定義GameFSM:
typedef std::shared_ptr<const GameRouteRelation> P_C_GAMEROUTERELATION ;
class GameFSM : public FSM<emGFsmEventId , emGFsmStatusId , GFsmParam>{
public:
GameFSM(emGFsmStatusId startStatus , emGFsmStatusId endStatus) :
FSM(startStatus , endStatus){}
bool Register(emGFsmStatusId srcStatus , emGFsmStatusId destStatus , emGFsmEventId event ,
std::function<void (GFsmParam)> transitionAction = nullptr,std::function<void (GFsmParam)> enterAction = nullptr)
{
return FSM::Register(std::make_shared<GameRouteRelation>(srcStatus , destStatus , event , transitionAction , enterAction)) ;
}
virtual P_C_ROUTERELATION Notify(emGFsmEventId eventId , GFsmParam param)
{
P_C_ROUTERELATION pCRouteRelation = __super::Notify(eventId ,param) ;
if(pCRouteRelation)
{
P_C_GAMEROUTERELATION pCGameR = std::dynamic_pointer_cast<const GameRouteRelation>(pCRouteRelation) ;
//執行新狀態的進入動作
if(pCGameR)
{
if(pCGameR->enterAction)
pCGameR->enterAction(param) ;
}
}
return pCRouteRelation ;
}
};
基類的Notify調用後會返回發生狀態遷移的路由項基類對象指針,因此我們可以通過dynamic_cast轉換成我們定義的擴展路由項,這就是爲什麼路由表要保存路由項指針的第二個原因了。
由於我們保存的是智能指針,std::shared_ptr,所以不能直接用dynamic_cast,而是用std::dynamic_pointer_cast進行轉換。得到路由項之後,就可以調用進入狀態動作函數了。這就是擴展的GamFSM的Notify需要做的事。