来来来,用 C++ 写个有限状态机(一)

〇、先暂停我简单说两句啊

有限状态机是一个很常用的技术,在流程控制和游戏AI中都比较实用,因为状态机编程简单又很符合直觉。本文是参考《Programming Game AI by Example》从零开始使用C++搭一个状态机的轮子,主要有以下几个点:

我会在搭建的过程中穿插一些C++知识的介绍,适合不甚了解C++或是状态机的小伙伴,手把手的编程教程。

  1. 游戏框架的实现
  2. 状态类以及游戏对象的状态转换
  3. 状态机实现
  4. 消息机制的实现

完整的代码在 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;
// 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 ),以及这些属性的阈值,用来在状态转移中起作用。随后定义的方法表明了这些属性如何变化,并为其他类查看这些属性暴露了接口。

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;
// 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() 方法。

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
// 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);
};