您的位置:首页 > 大数据 > 人工智能

游戏开发中的人工智能(二):追逐和闪躲

2017-07-17 15:47 330 查看
接上文 游戏开发中的人工智能(一):游戏人工智能简介

本文内容:讨论基本的追逐和闪躲技术,以及进级的拦截技术。我们也谈及这些技术在砖块环境和连续环境中的变化。

追逐和闪躲

本章的焦点是追逐和闪躲,这是一个十分常见的问题。无论你开发的是太空战机射击游戏,策略模拟游戏,还是角色扮演游戏,游戏中的非玩家角色都会试着追逐或者逃离玩家角色。

追逐和闪躲由以下三部分组成:

追或逃的决策判断(后文谈论到状态机和神经网络时再来讨论)

开始追或逃(本章重点)

避开障碍物(第五章和第六章会再谈这个问题)

让追击者追逐猎物是最简单、最容易写而且也是最常用的方法就是在每次的游戏循环中,更新追击者的坐标,让追击者和猎物的坐标离得愈来愈近。这种算法不去管追击者和猎物各自行进的方向和速度。虽然这种做法有直接的效果,追击者会不断往猎物的位置移动,除非被障碍物挡住,但是,这种做法有其限制,稍后再加以讨论。

除了这种最简单的方法之外,还有其他方法可以用,审视你的游戏所需的条件。例如,游戏中如果整合了实时物理引擎,你就可以采用一定方法,考虑追击者及猎物的位置及速度,让追击者试着拦截猎物,而不是傻乎乎地一直追下去。在这种情况下,相对位置和速度的信息,可以作为某种算法的输入数据,由该算法求出适当的驱动力(如推力),把追击者引向猎物。不过,另外一种方法是利用势函数,以某种方式改变追击者的行为,使其去追逐猎物,或者更明确地讲是让猎物引起追击者的注意。同样,也可以用类似的势函数,让猎物逃离追击者,或者让追击者对猎物产生排斥感。第五章将介绍势函数。

本章我们要探索几个追逐和闪躲的方法,从最基本的方法开始。

在此我们将追逐和闪躲分为在连续环境中和砖块环境中。

砖块游戏中,游戏区会被分成不连续的砖块,而玩家位置会固定在某个砖块上。移动时都是以砖块为单位并且玩家前进的方向被限制(因为可能你的前方被砖块阻挡)。在连续环境中,则是以点坐标表示游戏区中的位置,玩家也可以往任何方向移动。

连续环境中的基本的追逐和闪躲

最简单的追逐算法就是根据猎物的坐标来修改追击者的坐标,使两者间的距离逐渐缩短。将此方法反着用则不再是缩短追击者和猎物间的距离,而是扩大该距离,则是闪躲方法。

基本追逐代码如下

// 例2-1:基本追逐算法:根据猎物的坐标来修改追击者的坐标

// x坐标
if(predatorX > preyX)
predatorX--;
else if(predatorX < preyX)
predatorX++;

// y坐标
if(predatorY > preyY)
predatorY--;
else if(predatorY < preyY)
predatorY++;


猎物的坐标是preyX,preyY,而追击者的坐标是predatorX ,predatorY 。游戏循环每运行一轮时就比较两者的x,y坐标。若追击者的x坐标大于猎物的x坐标,则递减追击者的x坐标,但如果追击者的x坐标小于猎物的x坐标,则递增追击者的x坐标。y坐标的调整逻辑也一样。最后的结果就是,每当游戏循环运行一轮后,追击者就会越接近猎物。

运用相同的方法,颠倒一下判断逻辑后,就可以实现基本闪躲效果。代码如下:

// 例2-2:基本闪躲算法:根据追击者的坐标来修改猎物的坐标

// x坐标
if(preyX> predatorX)
preyX++;
else if(preyX< predatorX)
preyX--;

// y坐标
if(preyY> predatorY)
preyY++;
else if(preyY< predatorY)
preyY--;


砖块环境中的基本的追逐和闪躲

无论是砖块环境还是连续环境,例2-1与例2-2所示范的技巧都适用。只不过在砖块环境中,x,y的坐标就是砖格的行、列编号,也就是说x,y坐标都是整数而在连续环境中,x,y的坐标可以是实数,构成游戏区域的笛卡尔坐标。

下面是砖块环境中的追逐实例:

// 例2-3:砖块环境中的基本追逐实例

// x坐标
if(predatorCol > preyCol)
predatorCol--;
else if(predatorCol < preyCol)
predatorCol++;

// y坐标
if(predatorRow > preyRow)
predatorRow--;
else if(predatorRow < preyRow)
predatorRow++;


下图时怪物追赶主角时所走的路径:



可以看出,怪物在基本追逐中会沿着对角线走向主角,直到XY坐标之一和主角相等(此例中是X坐标)。接着,怪物沿着另外一个坐标轴继续往主角方向向前,此例中为Y轴。可以看到,这种追逐显得很不自然也很不智能,比较好的做法是让怪物走直线去追赶主角(引出视线追逐)。后面我们会提到。

砖块环境中的闪躲实例:

// 例2-4:砖块环境中的基本闪躲实例

// x坐标
if(preyCol> predatorCol)
preyCol++;
else if(preyCol< predatorCol)
preyCol--;

// y坐标
if(preyRow> predatorRow)
preyRow++;
else if(preyRow< predatorRow)
preyRow--;


视线追逐

视线追逐又称为视线法,视线法主要是让追击者沿着猎物的直线方向前进,即让追击者永远面对着猎物当时位置前进。当猎物站着不动时,追击者所走的路径是直的,但是当猎物移动时,路径就不一定是直线了,可能是弯弯曲曲的。如下图所示。



在上图中,圆圈代表追击者,方块代表猎物。虚线图形指的是起点和中途的位置。在左边的场景中,猎物是不动的,因此追击者可以直线追击猎物。在右边的场景中,猎物不停的移动,追击者的方向也随之改变。游戏循环每运行一轮或经过一段时间,就必须重新计算追击者朝向猎物的新方向。

砖块环境中的视线追逐



观察上图可以发现,虽然基本追逐和视线追逐的路径距离是相等的,但是视线法看起来更自然、直接,看起来怪物更具有智能。所以,视线法的目标就是算出一条路径,让怪物看起来像是沿着直线走向玩家。

解决这个问题的方法是使用直线扫描转换(标准线段算法),这种算法通常是在图素环境中画线段。前面我在计算机图形学 学习笔记(一):概述,直线扫描转换算法:DDA,中点画线算法,Bresenham算法 中介绍过几种直线扫描算法。这里我们采用 Bresenham 算法。

计算巨人移动方向的 Bresenham 算法,会以起点(怪物位置的行和列)和终点(玩家位置的行和列)为数据,算出巨人要走的一连串步伐,使其能以直线走向玩家。每次怪物的猎物(此例是玩家)改变位置时,都要调用一次这个函数。一旦猎物移动了,前一次算出来的路径就无效了,必须再重新计算一次。例2-5到例2-8示范了如何使用 Bresenham 算法建立怪物走向目标的路径。

例2-5:BuildPathToTarget()函数

void ai_Entity::BuildPathToTarget(void)
{
int nextCol=col;
int nextRow=row;
int deltaRow=endRow-row;
int deltaCol=endCol-col;
int stepCol,stepRow;
int currentStep,fraction;
}


这个函数使用了 ai_Entity 类中存储的值,建立路径的起点和终点。col 和 row 的值是路径的起点位置即怪物当前的位置。endRow 和 endCol 是路径的终点坐标,也就是猎物的位置。该函数先将怪物当前的位置 col 和 row 赋给了 nextCol 和 nextRow 并计算出行方向上的增量 deltaRow 和 列上的增量 deltaCol 留待后面为 Bresenham 算法提供方便。然后声明了 行方向上的步伐 stepCol,列方向上的步伐stepRow,当前步伐的计数器 currentStep。

例2-6:路径初始设定

for(currentStep=0;currentStep<kMaxPathLength;currentStep++)
{
pathRow[currentStep]=-1;
pathCol[currentStep]=-1;
}

currentStep=0;
pathRowTarget=endRow;
pathColTarget=endCol;


在例2-6中可以看到,行和列的路径数组已初始化。每次猎物的位置改变后,这个函数就会被调用,所以在计算新值时必须把旧路径清除掉。

例2-7利用先前算出了 deltaRow 和 deltaCol 决定路径的方向。

//例2-7:路径方向计算

if(deltaRow<0)
stepRow=-1;
else
stepRow=1;
if(deltaCol<0)
stepCol=-1;
else
stepCol=1;
deltaRow=abs(deltaRow*2);
deltaCol=abs(deltaCol*2);
pathRow[currentStep]=nextRow;
pathCol[currentStep]=nextCol;
currentStep++;


下面是利用 Bresenham 算法计算怪物所走的路径。

例2-8:Bresenham 算法计算怪物所走的路径

if(deltaCol>deltaRow)
{
fraction=deltaRow*2-deltaCol;
while(nextCol!=endCol)
{
if(fraction>0)
{
nextRow=nextRow+stepRow;
fraction=fraction-deltaCol;
}
nextCol=nextCol+stepCol;
fraction=fraction+deltaRow;
pathRow[currentStep]=nextRow;
pathCol[currentStep]=nextCol;
currentStep;
}
}
else
{
fraction=deltaCol*2-deltaRow;
while(nextRow!=endRow)
{
if(fraction>=0)
{
nextCol=nextCol+stepCol;
fraction=fraction-deltaRow;
}
nextRow=nextRow+stepRow;
fraction=fraction+deltaCol;
pathRow[currentStep]=nextRow;
pathCol[currentStep]=nextCol;
currentStep;
}
}


在上面的函数中,我们计算每一点与终点之间的横轴与纵轴。然后比较两轴的长度,哪一个轴比较长,就往该方向前进,如果两轴等长,则往斜边前进。所以,一开始的 if 条件语句便以 deltaCol 和 deltaRow 的值来判断哪个轴更长。如果列轴更长,则执行 if 语句后的程序代码。如果行周更长,则执行 else 之后的程序代码。然后,这个算法将沿着较长的轴去走,算出沿着线段上的每一个点。

连续环境中的视线追逐

在连续环境中的视线算法,主要在于控制追击者转向力的启动时机与反向,使其随时保持着面向猎物的姿态。

算法思路:计算追击者自己和猎物之间的相对位置并凭借调整转向力的大小来保持追击者自身一直面对猎物的方向,然后向猎物追过去。

全局坐标系统和局部坐标系统:



下面例2-9所示的函数,会计算追击者自己和猎物之间的相对位置并凭借调整转向力的大小来保持追击者自身一直面对猎物的方向。这段函数会在每次物理引擎循环运行一轮时,就被重新执行一次。只有这样才能达到视线追击的效果

例2-9:视线追逐函数

void DoLineOfSightChase(void)
{
Vector u,v;         // u追逐者向量,v猎物向量
bool left = false;  // 是否需要向左转
bool right = false; // 是否需要向右转
u = VRotate2D(-Predator.fOrientation,
(Prey.vPosition-Predator.vPosition)); // 视线在局部坐标系中的向量
u.Normalize();      // 将得到的向量u标准化
if(u.x < -_TOL)     // 判断转动的方向
left = true;
else if(u.x > _TOL)
right = true;
Predator.SetThrusters(left, right); // 转动
}


例2-9的算法十分简单。一开始就定义了四个局部变量。u 和 v 是追击者与猎物的向量,它们所属的 Vector 类,是我们定义的类,该类提供了所有基本的向量运算,诸如向量加法、减法、内积、外积以及其他运算。另外另个局部变量 left 和 right 是一组布尔变量。它们代表该方向上的转向力是否有作用,在直线前进情况下。这两个变量的初始值都赋以 false。

在局部变量的定义之后,首先要计算追击者到猎物之间的视线:

u = VRotate2D(-Predator.fOrientation,
(Prey.vPosition-Predator.vPosition));


其中的
(Prey.vPosition-Predator.vPosition)
是以追击者与猎物的全局坐标计算两者之间的相对位置向量,而 VRotate2D( ) 函数将此向量转换成追击者的局部坐标。 VRotate2D( ) 函数需要两个参数,一个是局部坐标系统的基准点,另一个是全局坐标系统中的向量,它能将该向量转换成相对于局部坐标基准点的向量。

接着,我们使用 Normalize() 将得到的向量 u 标准化即转换成一个单位长度的向量。

有了从追击者指向猎物的单位向量 u,即可据此判断猎物是在追击者的左边、右边还是正前方并据此调整方向。

从追击者的局部坐标系来看,如果猎物的 x 坐标是负值,那么猎物位于追击者的右边,因此应该启动左边的转向力,调整追击者的前进方向。同理,如果猎物的 x 坐标值是正的,则位于追击者的左边,应该启动右边的转向力,调整追击者的方向。如果猎物的 x 坐标是0,则无需启动两侧的推进器,直接前进即可。

这个算法的结果如下图所示。下图显示的是追击者和猎物行走的路径。刚开始的时候,追击者位于画面的左下角而猎物位于右下角。猎物随时间往左上角直线移动。追击者的路径则是弯曲的,因为它会不断调整自己的方向保持朝着移动中的猎物前进。



拦截

之前讨论的连续环境中的视线追逐算法,可以有效地使追击者一直朝着猎物方向前进。但是,这种算法的缺点是直接朝着猎物方向前进,从空间或者时间的角度来看,不一定都是追上猎物的最短路径。比较合理的解决方法,是让追击者在猎物路径上的某个点予以拦截。这样从时空角度来看,可以让追击者以最短的时间或路径追到猎物。

拦截算法的基本原理:预测猎物未来的位置,然后直接到那个位置去,使追击者能和猎物同时到达统一位置。如下图所示。



追击者要做的第一步计算是求它自己和猎物间的相对速度。我们称为靠拢速度,就是猎物和追击者间的速度向量差:



第二步是要计算靠拢距离,也就是追击者和猎物间的相对距离,相当于两者当前位置的向量差:



接下来要计算靠拢时间,靠拢时间是以靠拢速率(追击者和猎物之间的相对移动速度)走完靠拢距离所需的平均时间:





例2-10:拦截函数

void DoIntercept(void)
{
Vector u,v;
Bool left=false;
Bool right=false;
Vector Vr,Sr,St; //新增
Double tc;       //新增

//新增
Vr=Prey.vVelocity-Predator.vVelocity;
Sr=Prey.vPosition-Predator.vPosition;
tc=Sr.Magnitude();
St=Prey.vPosition + (Prey.vVelocity * tc);

//将Prey.vPosition 改成了St
u=VRotate2D(-Predator.fOrientation,(St-Predator.vPositon));

//其余部分和视线追逐法的函数一样
u.Normalize();  // 归一划
if(u.x < -_TOL) // 判断转动的方向
left = true;
else if(u.x > _TOL)
right = true;
Predator.SetThrusters(left, right); // 转动
}


从例2-10中的程序注释,可以看出视线追逐函数与拦截函数之间的差别,仅仅在于目标点的改变。前者是以猎物本身为目标,后者是以猎物的未来未知为目标。所以新增的程序代码,基本上只是运用公式从靠拢速度、距离以及靠拢时间,计算出猎物的预测位置。

每当游戏循环或物理引擎运行一轮,都应该重新调用此函数,随时修正拦截点及拦截路径。

例子:

图2-11所示的场景是追击者和猎物,分别从画面的左下角和右下角出发。猎物以匀速走向左上方。同时,追击者计算出预测的拦截点并朝该点走去,同时持续更新预测的拦截点及其行进方向。图中的拦截点以一连串的点表示,分布在猎物的前方。最初,拦截点会随着追击者转向猎物而改变,然而由于猎物以匀速转动,方向确定后,拦截点就固定了。



过了一段时间后,追击者拦截住猎物,如图2-12所示。



连续环境中追逐和闪躲 示例代码下载

在VC 6++ 环境下可运行。

可以在Opention 菜单中选择追逐的方式(基本追逐,基本闪躲,视线追逐,潜在的追逐)或者选择是否显示向量和线条。

代码:AIDemo2-2

http://pan.baidu.com/s/1hswySRi
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: