來來來,用 C++ 寫個有限狀態機(一)

〇、先暫停我簡單說兩句啊


一年前寫的文章了,之前更新在 github pages 上,疏於管理,現在覺得還是把這些文檔放在 CSDN 好了,自己看起來也方便。

2020.5.18 十元


有限狀態機是一個很常用的技術,在流程控制和遊戲AI中都比較實用,因爲狀態機編程簡單又很符合直覺。本文是參考《Programming Game AI by Example》從零開始使用C++搭一個狀態機的輪子,主要有以下幾個點:

我會在搭建的過程中穿插一些C++知識的介紹,適合不甚瞭解C++或是狀態機的小夥伴,手把手的編程教程。

  1. 遊戲框架的實現
  2. 狀態類以及遊戲對象的狀態轉換
  3. 狀態機實現
  4. 消息機制的實現

完整的代碼在 github 中給出。

一、

記得最開始工作時候也接觸過有限狀態機,當時是一個長長的用switch寫成的狀態機,理解它的時候真的很困難。

所以現在使用一套內置規則到狀態內部去,來控制狀態的轉換。

現在就來製作一個有限狀態機。

作爲一個關於使用狀態機創建一個智能體的實際案例,我們先模擬這樣一個場景。想象一個我們控制的角色“我”,在上海延安西路附近工作學習和玩耍,“我”相當的佛系,有錢不愁吃喝就去上學,自由自在地給技能樹加點,沒錢了就去實習賺外塊,渴了累了就近“啤酒阿姨”兩瓶啤酒……

但是,這個例子要全靠想象力了,因爲這是在控制檯中的文本演示。

[圖1 這個狀態機的示意圖,surface畫一下,忍一忍買pro7吧]

因此,這個例子中有四個位置:一個學校,可以讓“我”提高能力;一個實習公司,根據“我”的能力,給我發實習津貼;一個啤酒阿姨酒吧,讓“我”可以解除壓力;最後還有一個溫馨的家吼,能夠解除疲勞。

而這些位置每一個都代表了一個狀態,因爲我們是使用內置規則來控制狀態機的轉換,“我”在到達一個位置之後要幹什麼,都會由當前所處的狀態和一些屬性值來決定。

&&&&逐個介紹一下,要點的擴展寫成補充的blog放到前面,加鏈接。

BaseGameEntity類,用來作爲所有遊戲對象的基類,主要爲遊戲對象提供了一個ID,以及每一幀更新時調用的純虛函數Update。

class BaseGameEntity
{
private:
	int m_ID;
	// Should be static
	static int m_iNextValidID;
	//int m_iNextValidID;
	void SetID(int val);
public:
	BaseGameEntity(int id)
	{
		SetID(id);
	}
	virtual  ~BaseGameEntity(){}
	virtual void Update() = 0;
	int ID()const { return m_ID; }
}

使用一個枚舉類型管理所有可能到達的地點,sweetHome、school、company、beerLady 分別代表家、學校、實習公司、啤酒阿姨這四個地點。在 Me 類中,對“我”所特有的屬性進行了定義,如心情值( m_iMoodForDoingStuffs ),金錢數( m_iMoneyInCard ),能力( m_iAbilityLevel ),疲勞( m_iFatigue ),以及這些屬性的閾值,用來在狀態轉移中起作用。隨後定義的方法表明了這些屬性如何變化,併爲其他類查看這些屬性暴露了接口。

enum location_type
{
	sweetHome,
	school,
	company,
	beerLady,
};

class Me : public BaseGameEntity
{
private:
	State* m_pCurrentState;
	location_type m_Location;
	// should be early than those variables use them.
	const int Max_Mood = 5;
	const int TirednessThreshold = 5;
	const int LowMoodThreshold = 1;
	const int LowMoneyThreshold = 2000;
	// 
	int m_iMoneyInCard;
	int m_iMoodForDoingStuffs;
	int m_iAbilityLevel;
	int m_iFatigue;
	
public:
	Me(int ID);				
	void Update(); 
	void ChangeState(State *pNewState);
	// interface
	location_type Location()const { return m_Location; }
	void ChangeLocation(const location_type goal) { m_Location = goal; }
	int Ability()const { return m_iAbilityLevel; }
	void SetAbilityLevel(const int val) { m_iAbilityLevel = val; }
	void AddToAbility(const int val);
	int MoneyInCard()const { return m_iMoneyInCard; }
	void SetMoneyInCard(const int val) { m_iMoneyInCard = val; }
	void ThePayDay(const int val);
	bool Fatigued()const;
	bool Rested()const;
	void DecreaseFatigue(const int val) { m_iFatigue -= val; }
	void IncreaseFatigue(const int val) { m_iFatigue += val; }
	bool LowMood()const;
	void DecreaseMood(const int val) { m_iMoodForDoingStuffs -= val; }
	void IncreaseMood(const int val) { m_iMoodForDoingStuffs += val; }
	bool FeelPoor()const;
	void BuyTheBeer() { m_iMoodForDoingStuffs = Max_Mood; m_iMoneyInCard -= 500; }
};

接下來是狀態 State 類,這是一個純虛類(抽象類),作爲狀態對象的一個通用接口。類中所有方法都是虛函數,需要在繼承自 State 的類中實現具體邏輯。Enter() 方法在進入狀態時調用一次,隨後執行 Execute() 方法處理狀態的主要邏輯,在狀態退出時,會執行 Exit() 方法。

class State
{
public:
	virtual ~State(){}
	virtual void Enter(Me*) = 0;
	virtual void Execute(Me*) = 0;
	virtual void Exit(Me*) = 0;

};

在這裏使用單例模式實現每一個狀態,一切從簡,不考慮線程安全。分別針對不同的地點,定義在每個地點的狀態類,代碼如下:

// My States

class GoWorkAndEarnMoney:public State
{
private:
	GoWorkAndEarnMoney() = default;
public:
	static GoWorkAndEarnMoney* Instance();
	virtual void Enter(Me* pMe);
	virtual void Execute(Me* pMe);
	virtual void Exit(Me* pMe);
};

class GoSchoolAndStudy:public State
{
private:
	GoSchoolAndStudy() = default;
public:
	static GoSchoolAndStudy* Instance();
	virtual void Enter(Me* pMe);
	virtual void Execute(Me* pMe);
	virtual void Exit(Me* pMe);
};

class GoHomeAndSleep:public State
{
private:
	GoHomeAndSleep() = default;
public:
	static GoHomeAndSleep* Instance();
	virtual void Enter(Me* pMe);
	virtual void Execute(Me* pMe);
	virtual void Exit(Me* pMe);
};
 
class GoBar:public State
{
private:
	GoBar() = default;
public:
	static GoBar* Instance();
	virtual void Enter(Me* pMe);
	virtual void Execute(Me* pMe);
	virtual void Exit(Me* pMe);
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章