游戏AI之有限状态机(1)

发表于2016-06-07
评论1 8.5k浏览
一、游戏AI介绍
       游戏AI,是一个非常值得挑战的部分。一个游戏好不好玩,游戏AI占了很大的分量。游戏中人物看起来比较智能,会让玩家觉得不是在和电脑作战,而是和真真的有思想的人来决斗。当然,真要做到那一步,很难,不过我们可以使用各种各样的方法,来模人工智能。
        一般来说,人工智能有模式,条件模式,有限状态机,决策树,神经网络,遗传算法和模糊逻辑等等。而有限状态机,基本上是任何游戏都会用到的一种组织AI的方法。它可以为后面更加深奥的AI算法提供强力的支持,所以掌握好有限状态机的使用,对以后更深层次的游戏AI的学习,提供强大的助力。

二、有限状态机介绍
        有限状态机,简称FSM,在各个游戏中都会出现它的身影,所以对于游戏编程人员来说,了解并掌握有限状态机,是非常重要的。根据前辈们的经验总结,FSM具有如下的特点:
       编程快速简单,易于调试,很少的计算,直觉性和灵活性。
有限状态机的概念实际来源于数学,作为程序员我们不需要掌握数学上关于有限状态机的确切定义,我们只需要知道一个描述性的语言就可以了:
     (以下描述摘自游戏人工智能编程案例精粹)一个有限状态机是一个设备,或是一个设备模型,在有限个数量的状态下,可以根据给定的输入来进行不同状态间的切换,一个有限状态机在任何时候,都只能有一个状态。

三、有限状态机的实现方式
        有很多种方法来实现有限状态机,读者可能想到使用一串的if...else,或者加上枚举类型的switch来进行状态的判断。的确,这样的方法是最直观的,也是最简单的实现方法。在一个智能体的状态数量很少的情况下,使用这样的实现方法是可以的。但是,当一个智能体的状态有很多个的时候,在这样实现,代码就会显得很臃肿,就会跟上一长串的case语句或者if...else语句。读者可以自行试试,用这种方法实现的有限状态机,虽然可以工作,但是对于开发人员来说,将是一个噩梦。因为你不得不自己来维护这么一长串的代码,而且不易于调试和阅读。
        所以这里推荐使用的方法是使用状态设计模式来完成整个有限状态机的实现。(关于状态设计模式,是属于设计模式的范畴,读者可自行查阅相关的资料进行了解,推荐看Head First设计模式中关于状态设计模式的章节)
       现在我们来实现下,如何进行有限状态机的设计。
       首先,我们需要定义一个公用的基类,用以派生出不同的状态类。
   
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
#ifndef _State_
#define _State_
 
//包含头文件
#include"Role.h"
#include
using namespace std ;
 
class Role ;
 
//定义状态基类
class State
{
public:
    State();
    ~State();
 
public:
    virtual void Excute(Role*) = 0;
 
public:
    std::string getDescription() const ;
    void setDescription(std::string);
private:
    std::string   m_Description ;   //描述当前状态的字符串
};
#endif
        这里的Role是我自己定义的角色类,也就是拥有状态的对象。从基类中可以看出,我们可以通过调用状态的Excute方法,来执行当前状态对于传进的Role的影响。由于是将Role以参数的形式传递给State的,所以Role状态的变化实际上分装分离在State中来完成。也就是说,将Role不同状态之间的转化有状态本身来决定,而不是让Role自己来控制。状态对象只需要根据Role的某些属性来判断如何进行状态之间的转化。
         好了,有了状态基类,我们需要的就是派生出几个子类来分别表示不同的状态,一下是本次教程中定义的几个状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef _HangState_
#define _HangState_
 
//包含头文件
#include"State.h"
#include"Role.h"
 
//定义闲逛状态类
class HangState
    :public State
{
private:
    HangState();
    ~HangState();
 
public:
    static State* getInstance();
 
public:
    void Excute(Role*);
};
 
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef _HungryState_
#define _HungryState_
 
//包含头文件
#include"State.h"
#include"Role.h"
 
//定义饥饿状态类
class HungryState
    :public State
{
private:
    HungryState();
    ~HungryState();
 
public:
    static State* getInstance();
 
public:
    void Excute(Role*);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef _SleepState_
#define _SleepState_
 
//包含头文件
#include"State.h"
#include"Role.h"
 
//定义饥饿状态类
class SleepState
    :public State
{
private:
    SleepState();
    ~SleepState();
 
public:
    static State* getInstance();
 
public:
    void Excute(Role*);
};
 
#endif
      上面定义了HangState,表示闲逛状态,表现为角色在屏幕上不停的乱动。
      SleepState用以表示困乏状态,当角色感到困乏时,进入的状态。
      那么HungryStae自然就是角色感到饥饿时进入的状态啦。
       从上面不同状态的定义来看,我们大概就能猜出,角色应该具有的属性,像饥饿值,困乏值等等。所以,自然引出了Role的定义:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#ifndef _Role_
#define _Role_
 
//包含头文件
#include"State.h"
#include"MyPolygon.h"
 
class State ;
//预定义变量
#define MAX_SLEEP 1000
#define MAX_HUNGRY 2000
#define MIN_SLEEP 10
#define MIN_HUNGRY 20
#define RESTRANT_X 100
#define RESTRANT_Y 100
#define HOUSE_X 500
#define HOUSE_Y 100
 
 
//定义角色基类
class Role
{
public:
    Role();
    ~Role();
 
public:
    //获取困乏值
    int getSleepValue() const ;
    //设置困乏值
    void setSleepValue(int);
    //获取饥饿值
    int getHungryValue() const ;
    //设置饥饿值
    void setHungryValue(int);
    //设置当前状态
    void setState(State*) ;
    //获取当前状态
    State* getState() const ;
    //设置位置
    void setPosition(Vertex2D);
    void setPosition(int , int);
    //获取位置
    Vertex2D getPosition() const ;
    //设置速度
    void setSpeed(int);
    //获取速度
    int getSpeed() const;
 
public:
    void update();   //用于更新角色
 
private:
    State*   m_CurrentState ;        //当前状态
    int         m_HungryValue;            //饥饿值
    int         m_SleepValue;            //困乏值
    Vertex2D m_Position;            //角色位置
    int         m_Speed;                //角色速度
};
 
#endif
       好了,接下来就是,主角如何和这些不同的状态进行交互的。我们先来看看主角的Update方法,因为此方法用来更新主角,从而引起状态的改变:
1
2
3
4
void Role::update()
{
    m_CurrentState->Excute(this);
}
        哈哈!!!这个函数,很简单,只是调用了当前状态的Excute方法来执行,也就是说,将任务交给了当前状态来工作。好,那么我们继续跟踪到Excute方法。(我默认初始化给的是HangState状态)。
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
void HangState::Excute(Role* role)
{
    //判断是否饥饿
    if(role->getHungryValue()"" role-="">setState(HungryState::getInstance());
        return ;
    }
 
    //判断是否困乏
    if(role->getSleepValue()"" role-="">setState(SleepState::getInstance());
        return ;
    }
 
    //如果不困乏不饥饿,进行闲逛
    Vertex2D pos = role->getPosition();
    if(rand()%2 == 0)
        pos.x+= role->getSpeed();
    else
        pos.x-=role->getSpeed();
 
    if(rand()%2 == 0)
        pos.y +=role->getSpeed();
    else
        pos.y-=role->getSpeed();
 
    if(pos.x<0)
        pos.x = 0 ;
    if(pos.x>640)
        pos.x = 640 ;
    if(pos.y<0)
        pos.y=0;
    if(pos.y>480)
        pos.y=480;
 
    //设置位置
    role->setPosition(pos);
 
    //进行饥饿和困乏值下降
    role->setSleepValue(role->getSleepValue()-1);
    role->setHungryValue(role->getHungryValue()-1);
}
        看到没,上面的HangState的Excute方法,就简单的判断下主角的饥饿值和困乏值。当某个值低于最小值得时候,就发生相应的状态改变。如果没有发生状态的改变,那么就让主角在不停的跳动,来消耗体力。一旦主角的饥饿值低于最小值的时候,就将状态切换我HungryState,等下一次调用Role->update的时候,就是执行HungryState的Excute方法了。同样的如果主角的困乏值低于最小值的时候,就将状态切换为SleepState,等下一次调用Role->update的时候,就是执行SleepState的Excute方法。下面看看这两个方法做了什么:
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
void HungryState::Excute(Role* role)
{
    Vertex2D pos = role->getPosition();
 
    //判断是否在饭店内
    if(pos.x == RESTRANT_X&&pos.y==RESTRANT_Y)
    {
        //进行补充
        role->setHungryValue(role->getHungryValue()+1);
        if(role->getHungryValue() == MAX_HUNGRY)
            role->setState(HangState::getInstance());
    }
    else
    {
        //向饭店移动
        if(pos.x"role-">getSpeed();
        if(pos.x>RESTRANT_X)
            pos.x-=role->getSpeed();
        if(pos.y>RESTRANT_Y)
            pos.y-=role->getSpeed();
        if(pos.y"role-">getSpeed();
 
        //加点随机噪音
        if(rand()%2 == 0)
            pos.x+=rand()%2;
        else
            pos.x-=rand()%2;
 
        if(rand()%2 == 0)
            pos.y+=rand()%2;
        else
            pos.y-=rand()%2;
 
        //设置角色位置
        role->setPosition(pos);
    }
}
        主角先使用简单的追踪算法来移动到饭店位置,当到达饭店位置的时候,就进行体力的补充,从而提高饥饿值。同样的对于困乏状态,实现也是大同小异。
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
void SleepState::Excute(Role* role)
{
    Vertex2D pos = role->getPosition();
 
    //判断是否在饭店内
    if(pos.x == HOUSE_X&&pos.y==HOUSE_Y)
    {
        //进行补充
        role->setSleepValue(role->getSleepValue()+1);
        if(role->getSleepValue() == MAX_SLEEP)
            role->setState(HangState::getInstance());
    }
    else
    {
        //向饭店移动
        if(pos.x"role-">getSpeed();
        if(pos.x>HOUSE_X)
            pos.x-=role->getSpeed();
        if(pos.y>HOUSE_Y)
            pos.y-=role->getSpeed();
        if(pos.y"role-">getSpeed();
 
        //加点随机噪音
        if(rand()%2 == 0)
            pos.x+=rand()%2;
        else
            pos.x-=rand()%2;
 
        if(rand()%2 == 0)
            pos.y+=rand()%2;
        else
            pos.y-=rand()%2;
 
        //设置角色位置
        role->setPosition(pos);
    }
}
        好了,代码就介绍到这里。下面来看看,执行的结果吧:
         一开始启动程序,角色就会处于闲逛状态:


     过一会儿之后,当主角感到困乏的时候,就去房子里面睡觉


        随着时间的增长,困乏值也在慢慢的提高。但提高到极限的时候,就继续闲逛,直到饥饿:


        好了,今天的FSM就讲到这里了,希望对大家有帮助。
         完整的代码,请在点击打开链接这里进行下载。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引