〇、先暂停我简单说两句啊
有限状态机是一个很常用的技术,在流程控制和游戏AI中都比较实用,因为状态机编程简单又很符合直觉。本文是参考《Programming Game AI by Example》从零开始使用C++搭一个状态机的轮子,主要有以下几个点:
我会在搭建的过程中穿插一些C++知识的介绍,适合不甚了解C++或是状态机的小伙伴,手把手的编程教程。
- 游戏框架的实现
- 状态类以及游戏对象的状态转换
- 状态机实现
- 消息机制的实现
完整的代码在 github 中给出。
一、
记得最开始工作时候也接触过有限状态机,当时是一个长长的用switch写成的状态机,理解它的时候真的很困难。
所以现在使用一套内置规则到状态内部去,来控制状态的转换。
现在就来制作一个有限状态机。
作为一个关于使用状态机创建一个智能体的实际案例,我们先模拟这样一个场景。想象一个我们控制的角色“我”,在上海延安西路附近工作学习和玩耍,“我”相当的佛系,有钱不愁吃喝就去上学,自由自在地给技能树加点,没钱了就去实习赚外块,渴了累了就近“啤酒阿姨”两瓶啤酒……
但是,这个例子要全靠想象力了,因为这是在控制台中的文本演示。
[图1 这个状态机的示意图,surface画一下,忍一忍买pro7吧]
因此,这个例子中有四个位置:一个学校,可以让“我”提高能力;一个实习公司,根据“我”的能力,给我发实习津贴;一个啤酒阿姨酒吧,让“我”可以解除压力;最后还有一个温馨的家吼,能够解除疲劳。
而这些位置每一个都代表了一个状态,因为我们是使用内置规则来控制状态机的转换,“我”在到达一个位置之后要干什么,都会由当前所处的状态和一些属性值来决定。
&&&&逐个介绍一下,要点的扩展写成补充的blog放到前面,加链接。
BaseGameEntity类,用来作为所有游戏对象的基类,主要为游戏对象提供了一个ID,以及每一帧更新时调用的纯虚函数Update。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class BaseGameEntity { private: int m_ID; static 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 ),以及这些属性的阈值,用来在状态转移中起作用。随后定义的方法表明了这些属性如何变化,并为其他类查看这些属性暴露了接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| enum location_type { sweetHome, school, company, beerLady, };
class Me : public BaseGameEntity { private: State* m_pCurrentState; location_type m_Location; 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); 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() 方法。
1 2 3 4 5 6 7 8 9
| class State { public: virtual ~State(){} virtual void Enter(Me*) = 0; virtual void Execute(Me*) = 0; virtual void Exit(Me*) = 0;
};
|
在这里使用单例模式实现每一个状态,一切从简,不考虑线程安全。分别针对不同的地点,定义在每个地点的状态类,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
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); };
|