《AI for Game Developers》第九章 有限向量机(skiplow翻译)
第九章
有限向量机是一个包括多种预定义状态的抽象机器。一个有限向量机可以设置状态变化的条件,而实际的状况则决定了状态机的行为。
关于有限状态向量机的起源要追溯到最早的PC游戏程序。比如在Pac Man游戏中的幽灵就是使用了
有限状态机这一技术。他们可以自由的游走,追逐或者躲避玩家。在每一个状态下所具有的行为都不一样,而状态的转变则取决于玩家的具体的一些操作。比如当玩家吃了一个增加能量的药丸,鬼魂将从追逐玩家的状态转变成躲避玩家的状态。在接下来的章节我们还将讨论这个例子。
9.1状态机的基本模型
图9-1叫你去创建一个简单的有限状态机模型
Figure 9-1. Generic finite state machine diagram
在图9-1中,每一可能出现的状态被连接成一个圆,我们这里有4个可能的状态{Si,S1,S2,S3}。当然在一个有限状态机中每个状态间的转换需要一个诱因也可以说是一个条件。在这个例子中,转换的条件函数分别为{t1,t2,t3,t4,t5}。该有限状态机的初始位置为Si。当t1这个转换条件函数成立的时候则状态被转换成S1。这时候你可以非常清晰的看清楚不同状态转换之间需要的刺激(也就是需要什么转换条件)。有些情况下,如状态在S1时,只有t5条件函数成立的时候才能进行状态的转换,而S3和S2则有两种可能的转换条件。
我们已经给你展示过了简单的有限向量机模型。现在让我们来看一个更实际并且稍微复杂的例子。图9-2展示了一个可能在实际游戏中用到的有限性向量机。
Figure 9-2. Ghost finite state machine diagram
让我们仔细观察下图9-2,这个有限状态机模型描述的就是在Pac Man游戏中幽灵状态的转换。每一个椭圆代表的是一个可能的状态,我们总共有3个可能的状态:roam(自由游走),evade(躲避),chase(追逐)。箭头表示的是可能的状态的过渡。这个过渡给出了是转换状态还是保持原状的条件。
在本例中,电脑初始化幽灵为一个自由游走的状态。有两个条件可以引起状态的转换。第一个条件是幽灵变成蓝色(blue=true),当玩家吃到了能力提升的药丸是幽灵将变成蓝色,这个将导致幽灵将从游走状态转换为躲避状态。另一个可以改变状态的条件是幽灵看到了玩家(see=ture),此时幽灵将从游走状态转换成追逐状态,幽灵能够看到玩家并对玩家展开追逐。
图9-2还显示当幽灵变蓝后将一直处于躲避状态。否则如果看到玩家时幽灵状态将转换为追逐,如果幽灵没有看见玩家则继续它的游走状态。当玩家不在一个处于追逐状态的幽灵的视线内时,幽灵将转换为游走状态。
目前我们已经用图表展示了一个有限状态机模型的行为。接下来我们要来研究一下它的编码实现。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
switch (currentState)
{
case kRoam:
if (imBlue==true) currentState=kEvade;
else if (canSeePlayer==true) currentState=kChase;
else if (canSeePlayer==false) currentState=kRoam;break;
case kChase:
if (imBlue==true) currentState=kEvade;
else if (canSeePlayer==false) currentState=kRoam;
else if (canSeePlayer==true) currentState=kChase;break;
case kEvade:
if (imBlue==true) currentState=kEvade;
else if (canSeePlayer==true) currentState=kChase;
else if (canSeePlayer==false) currentState=kRoam;
break;
}
例9-1所示的程序代码可能不是最有效的解决方法,但是它能够为你展示如图9-2所示的一个有限状态机得编码实现。代码中有3个可转换的状态kRoam,kChase,和kEvade,接着来检测是否有状态转换的条件。请注意代码中imBlue条件是具有最高的优先级的,如果imBlue=true无论幽灵在什么情况下其状态都将转换成躲避(KEvade),一直到imBlue=false才可能转换成其他状态。
9.2有限状态机的设计
现在我们将讨论在一个真正的游戏中实现有限状态机的方法。有限状态机在游戏AI中非常有用。它能提供一个简单而且逻辑清晰的方法去控制游戏人工智能的一些行为。大部分游戏都使用了这个技术,只是开发者没有意识到而已。
我们将任务分成两部分。首先我们要讨论的是储存跟AI实体有关的信息的结构体,然后我们要讨论怎么设置状态转换间的函数。
游戏开发将运用到如C和C++一样的高级语言,我们将在与AI实体有关的类或结构体中储存信息。这个结构体中可以包括位置,健康度,力量,特殊技能,物品等很多东西。当然除了这些以外,这个结构体中还储存了AI的状态,还有决定AI行为的状态。例9-2给我们展示了一个简单的这种类型结构体的例子。
Example 9-2. Game AI structure
class AIEntity
{
public:
int type;
int state;
int row;
int column;
int health;
int strength;
int intelligence;
int magic;
};
在例9-2中,类中第一个成员变量代表的是实体的类型,比如可以是巨兽,人类,甚至一个航空母舰。接下来的一个成员变量是我们最关心的,因为它储存的是AI状态。接下来的其他的变量都储存了与AI实体有关的其他信息。
我们用几个全局变量表示状态,增加状态就只要增加全局变量就可以了,例9-3展示了几个定义好的全局变量。
Example 9-3. State constants
#define kRoam 1
#define kEvade 2
#define kAttack 3
#define kHide 4
我们已经完成了一个包含AI状态的类,接下来让我们来看看怎么向这个类中增加状态转换函数。
接下来的步骤我们将要去探究有限状态机的状态转换行为和状态转换函数,例9-4给我们展示了一个加入了状态转换行为和状态转换函数的AI实体类。
Example 9-4. Game AI transition functions
class AIEntity
{
public:
int type;
int state;
int row;
int column;
int health;
int strength;
int intelligence;
int magic;
int armed;
Boolean playerInRange();
int checkHealth();
};
我们在这个AI实体类中加入了两个函数。当然在真正的应用中会有很多的控制AI行为和改变AI状态的函数。在此例中两个转换函数足以示范怎么去改变AI状态。例9-5就展示了如何使用这两个转换函数去改变状态。
Example 9-5. Changing states
if ((checkHealth()<kPoorHealth) && (playerInRange()==false))state=kHide;
else if (checkHealth()<kPoorHealth)
state=kEvade;
else if (playerInRange())
state=kAttack;
else
state=kRoam;
在例9-5中的第一个if条件语句是判断这个AI实体的是否健康以及玩家是否在附近。如果AI实体的健康度低于某值并且玩家不在附近,则这个AI实体将进入隐藏状态。隐藏状态将一直持续,直到其健康度增加到一定程度。第二个if条件语句是判断AI实体的健康度是否低于某个值,如果能到达这个if条件语句则表示玩家在附近,由于玩家在附近,只是把自己藏起来可能没有什么实际作用,玩家很有可能会看到这个AI实体并进行追杀,所以此时AI实体应进入躲避状态来躲避玩家。第三个if条件语句是判断玩家是否在附近,如果到达了这个条件语句说明前面的两个条件语句都没有成立,也就是说此时AI实体的健康程度良好并且玩家就在附近,所以实体进入攻击状态。最后,如果以上三个条件判断语句都被判断为false则实体保持游走状态。
以上的内容介绍了一个基础的有限状态机的类,接下来我们将把这些概念运用到一个功能比较全的有限状态机中去。
9.3蚂蚁的例子
在我们这个例子的有限状态机模拟器中有两组不同的智能蚂蚁。次模拟器的目的是让蚂蚁去搜集食物,并把食物运回蚂蚁窝。在这个模拟器中蚂蚁的行为必须遵从一些规则。首先蚂蚁要在地图上自由游走并搜索食物,一旦蚂蚁找到了食物则立刻把食物运回家,但蚂蚁到蚂蚁窝把食物放下后,蚂蚁现在要开始寻找水源,一旦蚂蚁找到了水后它又可以开始去寻找食物。
当有食物被运到蚂蚁窝后,在蚂蚁窝的位置会出生一只蚂蚁。蚂蚁在寻找食物的路上会遇到些障碍,另外会有毒药随机产生在地图上,毒药对蚂蚁来说是会致命的。
图9-3呈现了一个描述每只蚂蚁行为及其变换的有限向量机。
Figure 9-3. Ant finite state machine diagram
如图9-3所示,每只蚂蚁都被初始化为搜寻状态。在这里会有两种状况能改变这个状态。当蚂蚁找到食物后则状态会变换成回蚂蚁窝或者它遇到了毒药状态转换为死亡。如果蚂蚁处在回蚂蚁窝这个状态,则有两种情况能改变它,一是找到了蚂蚁窝的位置并把食物放在了蚂蚁窝那么蚂蚁的状态会从会蚂蚁窝转换成干渴,那么蚂蚁就开始去找水源。如果蚂蚁在回蚂蚁窝的路上遇到了毒药,则其状态里面变成死亡。蚂蚁在干渴状态,需要去寻找水源,这里也同样有两种状况可以改变次状态,一是跟前面一样碰到了毒药状态被转换为死亡,第二中就是找到了水源,则蚂蚁进入初始状态也就是搜寻状态。
如你所见,蚂蚁们可以处在几个不同的状态而在相应状态则会去完成对应的工作。每一个状态代表的是一个期望中的行为。我们将用以前的描述规则来定义AI蚂蚁的状态。这将在 例9-6中展示。
Example 9-6. Ant states
#define kForage 1
#define kGoHome 2
#define kThirsty 3
#define kDead 4
第一个宏定义的是蚂蚁自由游走找寻食物的状态。每一个处在kForage状态的蚂蚁都将在地图上自由的游走去找寻食物。当一只蚂蚁找到了一块食物,则它的状态变换成kGoHome状态,在这个状态下蚂蚁忽略沿途中的食物自动返回蚂蚁窝。当蚂蚁没有碰到毒药成功返回蚂蚁窝后,它的状态将变成kThirsty。在这个状态跟kForage很相似,但是kForage状态下是去找食物,而kThirsty是去找水源。一旦蚂蚁找到了水源则其状态将转换成kForage。在这里我们看出所有的行为都是这样重复进行的。
现在我们已经描述了蚂蚁仿真系统中有限状态向量机的转换基础了。现在我们将要去看看能供我们使用的数据结构体。如例9-7所示,我们将使用C++的类来储存所有与蚂蚁状态有关的数据。
Example 9-7. ai_Entity class
#define kMaxEntities 200
class ai_Entity {
public:
int type;
int state;
int row;
int col;
ai_Entity();
~ai_Entity();
};
ai_Entity entityList[kMaxEntities];
例9-7中的类中包含了四个变量,第一个变量是type,表示的是AI实体的种类。
如果你还记得,前面我们所定义的蚂蚁系统中含有两组不同的蚂蚁,我们将要用例9-8的内容区别它们。
Example 9-8. Team constants
#define kRedAnt 1
#define kBlackAnt 2
第二个变量是state,这个变量储存的是蚂蚁当前的状态,这个值可以是例9-6中任意的一个值。4个值分别代表的是kForage, kGoHome, kThirsty和kDead四个状态。
最后的两个变量是row和col。这个两个变量可以描述在一个平面地图上蚂蚁所处的位置。
与前面的很多例子相同,我们将把环境地图设置在一个平面内,地图将有一个整数二维数组表示,例9-9展示的是常量二维数组的定义。
Example 9-9. Terrain array
#define kMaxRows 32
#define kMaxCols 42
int terrain[kMaxRows][kMaxCols];
在此我们总共定义了一个32*42大小的地图。每个二维数组储存的是此处地形的类型,如例9-10所示我们总共有6个不同的可能的类型。
Example 9-10. Terrain values
地图每格默认的类型是kGround,这代表的是一个空地。下一个类型是kWater,代表的是水源,如果蚂蚁处在kThirsty状态则就要去寻找它。接下来的两个类型是kBlackhome和kRedHome,代表两个不同的蚂蚁窝。再接下来的一个是kPoison,蚂蚁走到这状态将变换为kDead。最后一个类型是kFood,当蚂蚁走到这时,其状态会从kForage转换成kGoHome。#define kGround 1
#define kWater 2
#define kBlackHome 3
#define kRedHome 4
#define kPoison 5
#define kFood 6
当地图的大小和每块可能出现的地形确定了后,我们就可以用代码构造一个完整的地图了。如例9-11。
Example 9-11. Initializing the world
#define kRedHomeRow 5
#define kRedHomeCol 5
#define kBlackHomeRow 5
#define kBlackHomeCol 36
for (i=0;i<kMaxRows;i++)
for (j=0;j<kMaxCols;j++)
{
terrain[i][j]=kGround;
}
terrain[kRedHomeRow][kRedHomeCol]=kRedHome;
terrain[kBlackHomeRo][kBlackHomeCol]=kBlackHome;
for (i=0;i<kMaxWater;i++)
terrain[Rnd(2,kMaxRows)-3][Rnd(1,kMaxCols)-1]=kWater;
for (i=0;i<kMaxPoison;i++)
terrain[Rnd(2,kMaxRows)-3][Rnd(1,kMaxCols)-1]=kPoison;
for (i=0;i<kMaxFood;i++)
terrain[Rnd(2,kMaxRows)-3][Rnd(1,kMaxCols)-1]=kFood;
在例9-11中,我们首先把地图中的每一块的初始地形定义成kGround。然后我们设置两个蚂蚁窝的位置,一个红蚂蚁窝,一个黑蚂蚁窝。蚂蚁找到食物后,如果它是红蚂蚁则它会把食物搬到红蚂蚁窝,是黑蚂蚁就搬到黑蚂蚁窝。
例9-11最后有三个for循环,他们的作用是分配kWater、kPoison、kFood三个地形类型。
图9-4展示了我们初始化的世界地图,我们还没有在世界中增添智能蚂蚁,但是我们已经在地图上放置了食物,水源和毒药了。
Figure 9-4. Ant world
现在我们已经定义出了一个世界地图,接下来我们将要在世界地图中产生智能蚂蚁
我们现在要做的是产生一个新的智能实体。为了达到这个目的我们将在ai_Entity类中增加一个方法,例9-12展示的是增加了一个方法的ai_Entity类。
Example 9-12. ai_Entity class
class ai_Entity
{
public:
int type;
int state;
int row;
int col;
ai_Entity(); ~ai_Entity();
void New (int theType, int theState, int theRow, int theCol);
};
但要在世界中产生一个蚂蚁时这个方法将被调用,在最开始我们在世界中产生两只不同颜色的蚂蚁。当蚂蚁找到食物并把食物放到了蚂蚁窝中后,我们将调用此函数在蚂蚁窝的位置新生一个蚂蚁。例9-13展示了这个方法的具体实现。
Example 9-13. New ai_Entity
void ai_Entity::New(int theType, int theState, int theRow, int theCol)
{
int i;
type=theType;
row=theRow;
col=theCol;
state=theState;
}
这个方法实现起来非常简单,就是为ai_Entity初始化四个变量,这四格变量为 entity type(蚂蚁类型),state(状态),row(行),column(列)。现在我们去看一看例9-14,此例展示的是如何用这个方法实现在世界中增加智能蚂蚁。
Example 9-14. Adding ants
entityList[0].New(kRedAnt,kForage,5,5);
entityList[1].New(kRedAnt,kForage,8,5);
entityList[2].New(kBlackAnt,kForage,5,36);
entityList[3].New(kBlackAnt,kForage,8,36);
如例9-14所示,我们向世界中添加了4只蚂蚁。第一个参数代表的是蚂蚁的类型,这4只蚂蚁中有两只红蚂蚁两只黑蚂蚁。第二个参数是蚂蚁初始的状态。最后连个参数行和列,代表的是在地图中初始化的位置。
图9-5展示的是怎讲4只蚂蚁后的世界地图。
Figure 9-5. Populating the world
每一个在图9-5中的蚂蚁的初始状态都被设定为kForage。他们将在地图上自由游走去寻找食物。在本例中食物被标识为苹果。如果他们走到了毒药的位置上(也就是图中所示的骷髅头)蚂蚁的状态将变换为kDead,蚂蚁将死亡。蚂蚁在kThirsty状态是将要去寻找水源就是图中画有水的位置。
在前面的章节中我们已经成功的在世界中产生了4只蚂蚁,现在我们将向你展示这个仿真是怎么运转的。这是有限状态机实现的重要部分。回忆下以前的内容,有限状态机最基本前提是把不同的状态和不同的行为联系起来。我们将用编码去定义每个状态下所对应的行为。这一部分的代码是对状态变换的一个声明,以便检查不同的可能的状态。在真实的游戏中,这一段代码将在主循环中调用。例9-15展示了怎么使用转换声明。
Example 9-15. Running the simulation
for (i=0;i<kMaxEntities;i++)
{
switch (entityList[i].state)
{
case kForage:
entityList[i].Forage();
break;
case kGoHome:
entityList[i].GoHome();
break;
case kThirsty:
entityList[i].Thirsty();
break;
case kDead:
entityList[i].Dead();
break;
}
}
在例9-15中,我们用一个循环去遍历在实体列表中的每一个实体。我们使用转换声明去检查实体列表中每一个实体的状态。然后我们把每一个状态和一个行为函数连续起来,就向9-16所示,我们在ai_Entity类中增加了4个函数。
Example 9-16. ai_Entity class functions
class ai_Entity
{
public:
int type;
int state;
int row;
int col;
ai_Entity();
~ai_Entity();
void New (int theType, int theState, int theRow, int theCol);
void Forage(void);
void GoHome(void);
void Thirsty(void);
void Dead(void);
};
例9-16展示的是一个增加了4个行为函数的ai_Entity类。每一个函数表示的是一个状态具有的行为。
第一个新的函数是Forage,它是与kForage状态连续起来的。回想一下,这个状态下的行为是在世界中去寻找食物,当找到食物时状态转换成kGoHome,而当碰到毒药时状态转换成kDead。这个行为将在Forage函数中实现,如例9-17所示。
Example 9-17. Forage function
void ai_Entity::Forage(void)
{
int rowMove;
int colMove;
int newRow;
int newCol;
int foodRow;
int foodCol;
int poisonRow;
int poisonCol;
rowMove=Rnd(0,2)-1;
colMove=Rnd(0,2)-1;
newRow=row+rowMove;
newCol=col+colMove;
if (newRow<1) return;
if (newCol<1) return;
if (newRow>=kMaxRows-1) return;
if (newCol>=kMaxCols-1) return;
if ((terrain[newRow][newCol]==kGround) ||
(terrain[newRow][newCol]==kWater))
{
row=newRow;
col=newCol;
}
if (terrain[newRow][newCol]==kFood)
{
row=newRow;
col=newCol;
terrain[row][col]=kGround;
state=kGoHome;
do {
foodRow=Rnd(2,kMaxRows)-3;
foodCol=Rnd(2,kMaxCols)-3;
} while (terrain[foodRow][foodCol]!=kGround);
terrain[foodRow][foodCol]=kFood;
}
if (terrain[newRow][newCol]==kPoison)
{
row=newRow;
col=newCol;
terrain[row][col]=kGround;
state=kDead;
do {
poisonRow=Rnd(2,kMaxRows)-3;
poisonCol=Rnd(2,kMaxCols)-3;
} while (terrain[poisonRow][poisonCol]!=kGround);
terrain[poisonRow][poisonCol]=kPoison;
}
}
rowMove和columnMove两个变量表示方向,接下来的两个变量是newRow和newCol表示的是蚂蚁的新位置。最后4个变量foodRow,foodCol,poisonRow和poisonCol表示的是当食物或毒药被蚂蚁搬走或碰到后,会在以上变量的位置重新产生食物或毒药。
接下来我们计算蚂蚁的新位置,我们把rowMove和colMove设置为-1到+1之间的随机数,这就表示了蚂蚁可以朝八个相邻的点中走。如果rowMove和colMove都为0则表示蚂蚁停在原位置。
一旦我们的rowMove和colMove变量的值确定下来,我就分别把这两个值与当前row和colum相加去产生一个由newRow和newCol表示的新位置。然后我们检查这个新位置是否合法也就是是否可以走,如果不能走则跳出函数。
现在我们假定新位置是可以走的,接下来蚂蚁要走到这个位置,并且做一系列的判断。首先判断新位置的是否是kGround或kWater,如果不是,则表示蚂蚁的状态要发生转换了。最后我们还得把newRow和newCol两个变量的值赋给row和col。
接下来是有限状态机设计的主要部分。其中的if条件判断语句是为了判断在新位置上是否有食物,如果有食物则更新蚂蚁的位置及状态(状态由kForage变换为kGoHome),并把此位置上的食物抹去。食物抹去后则执行最后一个do-while循环,此循环是在地图上随机再产生一个食物,如果不这样的话当食物被蚂蚁搬光后就不会再产生新的蚂蚁了。
Forage函数的最后一个部分展示的是另一个状态的转换,当新位置被检查出有毒药后,更新蚂蚁的位置及状态(有kForage转换成kDead),然后我们利用一个do-while循环在世界地图随机位置产生一个毒药。
接下来我们来看看例9-16中向ai_Entity类中添加的第2个行为函数。函数名为GOHome与kGoHome状态相关联。跟前面提到的一样,当蚂蚁找到食物后状态立刻转换成kGoHome。这个状态直到蚂蚁回到蚂蚁窝或碰到毒药才会改变。例9-18展示了GoHome函数的实现。
Example 9-18. GoHome function
void ai_Entity::GoHome(void)
{
int rowMove;
int colMove;
int newRow;
int newCol;
int homeRow;
int homeCol;
int index;
int poisonRow;
int poisonCol;
if (type==kRedAnt)
{
homeRow=kRedHomeRow;
homeCol=kRedHomeCol;
}
else
{
homeRow=kBlackHomeRow;
homeCol=kBlackHomeCol;
}
if (row<homeRow)
rowMove=1;
else if (row>homeRow)
rowMove=-1;
else
rowMove=0;
if (col<homeCol)
colMove=1;
else if (col>homeCol)
colMove=-1;
else
colMove=0;
newRow=row+rowMove;
newCol=col+colMove;
if (newRow<1) return;
if (newCol<1) return;
if (newRow>=kMaxRows-1) return;
if (newCol>=kMaxCols-1) return;
if (terrain[newRow][newCol]!=kPoison)
{
row=newRow;
col=newCol;
}
else
{
row=newRow;
col=newCol;
terrain[row][col]=kGround;
state=kDead;
do {
poisonRow=Rnd(2,kMaxRows)-3;
poisonCol=Rnd(2,kMaxCols)-3;
} while (terrain[poisonRow][poisonCol]!=kGround);
terrain[poisonRow][poisonCol]=kPoison;
}
if ((newRow==homeRow) && (newCol==homeCol))
{
row=newRow;
col=newCol;
state=kThirsty;
for (index=0; index <kMaxEntities; index ++)
if (entityList[index].type==0)
{
entityList[index].New(type,
kForage,
homeRow,
homeCol);
break;
}
}
}
在GoHome函数中声明的变量和函数Forage中声明的很相似。不过在此函数中我们多了两个变homeRow和homeCol。这两个变量记录的是蚂蚁窝的位置,我将用他们去判断蚂蚁是否成功回到了蚂蚁窝。Index变量记录的是在世界中有多少只蚂蚁。poisonRow和poisonCol表示的是毒药被消耗后新产生毒药的位置。
我们首先要声明蚂蚁窝的位置。前面已经提到这里有两种蚂蚁红蚂蚁和黑蚂蚁而两种蚂蚁对应有两个蚂蚁窝,红蚂蚁窝和黑蚂蚁窝。他们的位置信息分别储存在kRedHomeRow, kRedHomeCol,kBlackHomeRow,和 kBlackHomeCol变量中。先判断蚂蚁的种类然后用全局变量kRedHomeRow, kRedHomeCol,kBlackHomeRow,和 kBlackHomeCol为homeRow及homeCol赋值。至此我们知道了蚂蚁窝的位置,我们就可以让蚂蚁向着蚂蚁窝的位置移动了。
回忆一下,这是不是很像第二章追逐算法的变种。如果蚂蚁当前位置的row比蚂蚁窝的row小则rowMove被设置为1反之设置成-1,如果相等则设置为0。对于col也是同样的道路。
然后我们分别用当前row和colunm与rowMove和colMove相加得到蚂蚁新的位置。
当我们得到新的位置时,通常我们要检查此位置是否是地图的边界,这是一个良好的习惯,但在这好像不是那么必须,因为我们始终朝向蚂蚁窝的位置移动,只要蚂蚁窝的位置设定的在地图范围内就没有问题。
函数中If条件判断的第一步是判断新位置上是否有毒药,如果没有则只是更新下蚂蚁的位置,如果此if语句对应的else语句执行了那么我们就知道蚂蚁是碰到毒药了。碰到毒药后先更新蚂蚁的位置,抹去此点的毒药,把蚂蚁的状态从kGoHome变换成kDead,最后我们用一个do-while循环在地图的随机点产生一个毒药。
GoHome函数的最后一个if语句是判断蚂蚁是否到达了蚂蚁窝,如果蚂蚁的新位置与家的位置相等我们更新蚂蚁的位置,然后把蚂蚁的状态从kGoHome变换成kThirsty。当下一次UpdateWorld函数执行时,蚂蚁将去执行一个新的行为。此if语句最后的部分是去产生一个新的蚂蚁实体,把它加入实体列表并设置其出色状态为kForage。
接下来我们将介绍的是在例9-16中添加的另一个行为函数Thirsty,它是与状态kThirsty相联系的。在前面我们已经提到,当蚂蚁成功的把食物搬运回家后期状态将转换成kThirsty。此状态将使蚂蚁自由游走在地图上去寻找水源。与kForage状态不同的是当蚂蚁找到水后不返回蚂蚁窝,而是装换状态到kForage再去寻找食物。而当蚂蚁遇到毒药时状态则转换成kDead,表示此蚂蚁已死亡。例9-19展示了Thirsty函数的实现。
Example 9-19. Thirsty function
void ai_Entity::Thirsty(void)
{
int rowMove;
int colMove;
int newRow;
int newCol;
int foodRow;
int foodCol;
int poisonRow;
int poisonCol;
rowMove=Rnd(0,2)-1;
colMove=Rnd(0,2)-1;
newRow=row+rowMove;
newCol=col+colMove;
if (newRow<1) return;
if (newCol<1) return;
if (newRow>=kMaxRows-1) return;
if (newCol>=kMaxCols-1) return;
if ((terrain[newRow][newCol]==kGround) ||
(terrain[newRow][newCol]==kFood))
{
row=newRow;
col=newCol;
}
if (terrain[newRow][newCol]==kWater)
{
row=newRow;
col=newCol;
terrain[row][col]=kGround;
state=kForage;
do {
foodRow=Rnd(2,kMaxRows)-3;
foodCol=Rnd(2,kMaxCols)-3;
} while (terrain[foodRow][foodCol]!=kGround);
terrain[foodRow][foodCol]=kWater;
}
if (terrain[newRow][newCol]==kPoison)
{
row=newRow;
col=newCol;
terrain[row][col]=kGround;
state=kDead;
do {
poisonRow=Rnd(2,kMaxRows)-3;
poisonCol=Rnd(2,kMaxCols)-3;
} while (terrain[poisonRow][poisonCol]!=kGround);
terrain[poisonRow][poisonCol]=kPoison;
}
}
Thirsty的开始部分与Forage函数很相似。两个表示移动方向的变量rowMove和colMove,两个表示新位置变量newRow和newCol。剩下的变量foodRow、foodCol、poisonRow和poisonCol分别代表的是食物或毒药被消耗后新产生食物或毒药的位置。
然后我们为rowMove和colMove设置-1到+1之间的随机整数,并与当前row和col相加得到新的位置坐标newRow和newCol,接下来的if语句是判断新位置是否出了地图边界,如果出了就跳出函数。
接下来的if语句是判断新位置是否有水源,如果有就更新蚂蚁的位置,抹去该位置上的水源,蚂蚁的状态转换成kForage然后用一个do-while循环在地图随机位置设置一个水源。
跟前面的一些行为函数一样,最后是判断新位置是否有毒药,如果有则更新蚂蚁位置,抹去该位置上的毒药,蚂蚁的状态转换成kDead然后用过一个do-whilr循环在地图随机位置设置一个毒药。
在完成了与kForage, kThirsty, kGoHome,和 kDead状态所关联的函数后,我们就可以观察不同的行为,看清楚有限状态机的在此仿真系统中的运用。
在图9-6中你可以看到,我们仅仅初始了4只蚂蚁,但不久后他们就在整个世界中泛滥了。
Figure 9-6. Population explosion
你还可以观察如例9-20改变食物、水源及毒药的数量后对蚂蚁繁殖的影响。
Example 9-20. Food, water, and poison regulation
#define kMaxWater 15
#define kMaxPoison 8
#define kMaxFood 20
如例9-20所示,我们可以通过改变一些全局变量很容易的去改变整个仿真系统。比如减少毒药的数量将会使蚂蚁人口迅速增加,当减少食物的数量是,蚂蚁数量增长的速度将会减慢。如果向仿真系统中增加状态及与状态关联的行为,那么此系统将会变得更加有趣。
9.4更多的信息
有限状态机是在游戏中使用非常多。每一本游戏设计类的书都会或多或少的设计这方面的内容。这里我们列出来了几个讨论有限向量机的资源
当你在英特网上输入超找“有限向量机”后你会找到成千上万的资源,当然你也可以查找“模糊状态机”,模糊状态机也是一种使用非常广泛的AI技术。我将在第12章提到。
//---------------------------------------这个中文版网上没找到 所以就决定自己翻译 初学者翻译的不好请见谅-----------------------------------
有限向量机