游戏开发中的人工智能(六):基本路径寻找及航点应用

版权声明:本文为Jurbo原创文章,转载请加上链接和作者名,标明出处。 https://blog.csdn.net/Jurbo/article/details/75427143

接上文:游戏开发中的人工智能(五):以势函数实现移动

本文内容:游戏开发人员使用很多技术在游戏环境中寻找路径。本章要谈几种方法,包括航点应用。


基本路径寻找及航点应用

寻找路径的问题有很多不同类型。没有一种解决方法可以适用各种类型的路径寻找问题。

解决办法和每个游戏特定的路径寻找的需求细节有关。例如,目的地会移动还是静止不动?有没有障碍物?障碍物是否会移动?地形是什么样的?最短路径解决办法是不是一定是最佳解决办法?

路径寻找问题也可能不需要到达某个特定的目的地,也许你只是想让某个游戏角色在游戏环境中,看似聪明的四处移动或探索(类似前面讲过的移动模式)。

由于路径寻找问题有如此众多的类型,只选一种解决方法并不恰当。例如,A* 算法虽然是许多路径寻找问题的良方,但是不适用于每种情况。

本章会探索某些技巧,让你在A* 算法不适用时使用。A*算法将会在第七章讲解。

基本的路径寻找

从最基本的层次来讲,路径寻找只是让某个游戏角色,从其最初位置移向所需到达的目的地的过程本质上,这一点和我们在第二章中谈过的基本追逐算法原理相同。例6-1 说明了如何利用此算法达到基本的路径寻找需求。

//例6-1:基本路径寻找算法

if(positionX > destinationX)
    positionX --;
else if(positionX < destinationX)
    positionX ++;

if(positionY > destinationY)
    positionY --;
else if(positionY < destinationY)
    positionY ++;

此例中,游戏角色的位置以 positionX 和 positionY 表示,目的地的位置以 destinationX 和 destinationY 表示。和第二章中的基本追逐算法一样,这个方法使走到目的地的路径显得不自然,游戏角色会一直走对角线,直到其 x 或 y 轴坐标和目的地位置的坐标相同为止。然后,会走水平路径或垂直路径,直到抵达目的地。图6-1 说明了可能的路径。

这里写图片描述

如图6-1 所示,游戏角色(三角形)会走一条很不自然的路径抵达目的地(圆形)。比较好的做法是走比较自然的实现路径,如同我们在第二章中谈到过的视线追逐函数,可用 Bresenham 线段算法达到这种效果。图6-2 是使用 Bresenham 线段算法所得的视线路径。

这里写图片描述

由图6-2 可知,视线追逐算法可以得到一条看起来比较自然的路径。

视线追逐算法和基本追逐算法都很简单,速度很快。但是,这两种方法在很多场景下却不实用。例如,游戏环境中有障碍物的时候,如图6-3 所示,此时就需要考虑其他事项了。

这里写图片描述

随机移动避开障碍物

随机移动时简单而有效的障碍物避开方法,但只在少数障碍物的环境中特别有效。如图6-4 所示,这个游戏环境中分布着少数的树木,就是采用随机移动技巧的好对象。

这里写图片描述

如图6-4 所示,玩家不在巨人的视线内。然而,由于环境中的障碍物很少,只要随意的移动一下巨人,玩家就会进入巨人的视线了。在此场景中,使用消耗大量CPU 资源的路径寻找算法就会过于浪费,用这种简单的算法刚刚好。例6-2 是随机移动避开障碍物的基本算法。

//例6-2:随机移动避开障碍物算法

if(玩家在视线内) 
{  
    采用直线路径走向玩家(可以使用基本追逐算法或者视线追逐)  
}  
else 
{  
    以随机方向移动  
}  

绕行障碍物

绕过障碍物是另一种相当简单的避开障碍物的方法。当你在策略游戏或角色扮演游戏中,找出一条绕过大型障碍物的路径时,比如绕过山区时,这种方法就很有效。

利用这种方法,计算机控制的角色会采用一种简单路径寻找算法,试着抵达目标。该角色会一直沿着路径走下去,直到碰上障碍物。此时,该角色就会转换成绕行状态。在绕行状态下,该角色会沿着障碍物的边缘路线行动,试着沿路径经过。图6-5 表示了以三角形表示的假象的计算机控制的角色绕行障碍物,视图抵达以方块表示的目的地的路径。

这里写图片描述

图6-5 除了显示绕行障碍物的路径以外,也透露出了绕行的问题之一:何时停止绕行。如图6-5 所示,障碍物的边缘会被当成绕行路径,但会绕的太多了,几乎是绕回了起点。我们需要采用一种方式,决定何时应该从绕行状态换回简单的路径寻找状态。完成此举的方法之一,是算出从开始绕行的点到所需抵达的目的地之间的线段。计算机控制的角色会一直保持在绕行状态,直到与该线相交,到了交点时,就会换回简单路径寻找状态,如图6-6 所示。

这里写图片描述

另一种方法是在前述的绕行方法内,整合视线算法(因为本身视线算法的原理也是利用 Bresenham 算法画线段)。基本上,在沿路的每一步中,我们都会用实现算法,确认是否可以采用直线的视线路径抵达目的地,此法如图6-7 所示。

这里写图片描述

如图6-7 所示,我们沿着障碍物的边缘前进,但每走一步,我们都会检查目的地是否在计算机控制角色的视线内。如果是,我们就从绕行状态切换到路径寻找状态。

以面包屑寻找路径

面包屑路径寻找方式可以让计算机控制角色看起来很聪明,因为是玩家在不知不觉间替计算机控制角色建立了路径。每次玩家走一步时,都会毫无所知地在游戏世界中留下看不见的标记即面包屑。当游戏角色碰到面包屑时,就能凭着面包屑一路走下去。游戏角色会跟着玩家的足迹,直到追上玩家。(玩家如果本身走了很多浪费时间的路,那非玩家NPC也会重复玩家走的路径,显得蠢蠢的。而且不适用于网络游戏,因为玩家太多了)

面包屑方法也是计算机控制角色成群移动的有效方式。不必让群体的每个成员,都以费事费力的路径寻找算法找路径,可以直接让成员跟着领头者留下的面包屑走。

如图6-8 所示,玩家每走一步就会被标上一个整数值。下图被标记了15步。此例中,巨人会在砖块环境中随机移动,直到他在邻近位置侦测到了面包屑为止。

这里写图片描述

当然,在实际游戏中,玩家看不见面包屑足记,那是给游戏软件 AI 使用的数据。例6-3 是我们记录和每个游戏角色有关的数据的类。

//例6-3:ai_Entity 类

#define kMaxTrailLength 15

class ai_Entity
{
    public:
        int row;
        int col;
        int type;
        int state;
        int trailRow[kMaxTrailLength];
        int traiCol[kMaxTrailLength];

        ai_Entity();
        ~ai_Entity();
}

一开始的 #define 命令,制定了要记录的玩家步数的最大值。然后,我们以 kMaxTrailLength 常量定义了 trailRow 和 trailCol 数组的阀值。trailRow 和 trailCol 数组存储的是玩家走过的15步的行、列坐标。

如例6-4 所示,我们一开始把 trailRow 和 trailCol 数组中的每个元素都指定为 -1,因为-1 不在砖块环境的坐标系统内。

//例6-4:初始化足迹数组

int i;
for(i=0;i<kMaxTrailLength;i++)
{
    trailRow[i]=-1;
    trailCol[i]=-1;
}

现在,我们可以记录实际玩家的足迹了。此种做法最合理之处就是修改玩家位置的函数,这里我们要用的是 KeyDown( )函数,如果有某个方向键被按下的事件发生,就会更改玩家的位置,如例6-5 所示。

//例6-5:记录玩家位置

void ai_World::KeyDown(int key)
{
    int i;

    if(key==kUpKey)
    {
        for(i=0;i<kMaxEntities;i++)
        {
            if(entityList[i].state==kPlayer)
            {
                if(entityList[i].row > 0)
                {
                    entityList[i].row--;
                    DropBreadCrumb();
                }
            }
        }
    }

    if(key==kDownKey)
    {
        for(i=0;i<kMaxEntities;i++)
        {
            if(entityList[i].state==kPlayer)
            {
                if(entityList[i].row < (kMaxRows-1) )
                {
                    entityList[i].row++;
                    DropBreadCrumb();
                }
            }
        }
    }

    if(key==kLeftKey)
    {
        for(i=0;i<kMaxEntities;i++)
        {
            if(entityList[i].state==kPlayer)
            {
                if(entityList[i].col > 0)
                {
                    entityList[i].col--;
                    DropBreadCrumb();
                }
            }
        }
    }

    if(key==kRightKey)
    {
        for(i=0;i<kMaxEntities;i++)
        {
            if(entityList[i].state==kPlayer)
            {
                if(entityList[i].col < (kMaxCols-1) )
                {
                    entityList[i].col++;
                    DropBreadCrumb();
                }
            }
        }
    }
}

例6-5 的 KeyDown( ) 函数会确认玩家是否按下四个方向键的其中之一。如果是,就会走遍 entityList 数组,搜寻玩家所控制的角色。如果找到了,就会确定新的移动位置是否位于此砖块领域的边界内。如果所要移动的位置合法,位置就会被更新。

下一步是实际记录位置,要调用的是 DropBreadCrumb( ) 函数,如例6-6 所示。

//例6-6:丢下面包屑

void ai_World::DropBreadCrumb(void)
{
    int i;

    for(i=kMaxTrailLength-1;i>0;i++)
    {
        entityList[0].trailRow[i]=entityList[0].trailRow[i-1];
        entityList[0].trailCol[i]=entityList[0].trailCol[i-1];
    }
    entityList[0].trailRow[0]=entityList[0].row;
    entityList[0].trailCol[0]=entityList[0].col;
}

DropBreadCrumb( ) 函数 会把玩家的当前位置加入到 trailRow 和 trailCol 数组中。这些数组含有最近玩家位置的清单。

DropBreadCrumb( ) 函数 一开始把最旧的位置从 trailRow 和 trailCol 数组中删除。我们只追踪 kMaxTrailLength 个位置,所以每次要新增位置时,都必须把最旧的删除,我们在第一个for 循环中做这件事。实际上,这个循环做的是把数组中的所有位置都移位,循环把最旧的位置删掉,让第一个数组元素可以存储当前玩家位置。接着,我们把玩家的当前位置存储在 trailRow 和 trailCol 数组的第一个元素内。

下一步是让计算机控制的角色可以侦测到玩家留下的面包屑足迹,并沿着该足迹走。图6-9 是巨人在砖块环境中向八个可能方向中的任何一个方向移动的情况。

这里写图片描述

例6-7 是巨人如何侦测并跟着面包屑足迹移动的程序。

//例6-7:跟着面包屑

for(i=0;i<kMaxEntities;i++)
{
    r=entityList[i].row;
    c=entityList[i].col;
    foundCrumb=-1;
    for(j=0;j<kMaxTrailLength;j++)
    {
        if( (r==entityList[0].trailRow[j]) && (c==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r-1==entityList[0].trailRow[j]) && (c-1==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r-1==entityList[0].trailRow[j]) && (c==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r-1==entityList[0].trailRow[j]) && (c+1==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r==entityList[0].trailRow[j]) && (c-1==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r==entityList[0].trailRow[j]) && (c+1==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r+1==entityList[0].trailRow[j]) && (c-1==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r+1==entityList[0].trailRow[j]) && (c==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
        if( (r+1==entityList[0].trailRow[j]) && (c+1==entityList[0].trailCol[j]) )
        {
            foundCrumb=j;
            break;
        }
    }

    if(foundCrumb>=0)
    {
        entityList[i].row=entityList[0].trailRow[foundCrumb];
        entityList[i].col=entityList[0].trailCol[foundCrumb];
    }
    else
    {
        entityList[i].row=entityList[i].row+Rnd(0,2)-1;
        entityList[i].col=entityList[i].col+Rnd(0,2)-1;
    }
    if(entityList[i].row<0)
        entityList[i].row=0;
    if(entityList[i].col<0)
        entityList[i].col=0;

    if(entityList[i].row >= kMaxRows)
        entityList[i].row=kMaxRows-1;
    if(entityList[i].col >= kMaxCols)
        entityList[i].col =kMaxCols-1;
}

我们一开始走遍 trailRow 和 trailCol 数组。这些数组存储的是玩家最近走过的行、列位置,即玩家留下的面包屑。我们的目标是找出计算机控制的巨人所在的相邻八个可能的位置是否含有面包屑。我们用八个 if 语句比较相邻砖块和 trailRow 及 trailCol 数组中的每个元素。for 循环是从数组元素 0 开始的,因为这是玩家最新留下的位置。如果找到面包屑,其对应的数组索引值就会存储在 foundCrumb 这个数组索引变量中。

然后,我们会让程序跳出 for 循环。因为这是最接近玩家位置的面包屑(因为 trailRow 及 trailCol 数组存储顺序是从最新的玩家位置到最旧的玩家位置)。从最近玩家位置开始搜寻,也可以确保巨人时跟着面包屑走向玩家,而不是越走越远。

一旦 for 循环结束,我们要检查是否找到了面包屑。foundCrumb 变量存储的是找到的面包屑数组索引值。如果没有找到面包屑,则其仍然为初始值 -1。如果 foundCrumb 大于或等于0,我们就把巨人的位置指定为 trailRow 及 trailCol 数组位于 foundCrumb 索引值的元素值。每次当巨人的位置需要更新时,都这样做一次,才能使巨人跟着玩家的足迹前进。

遵循路径走

路径寻找的问题,通常仅仅是被认为是从起点行走到所需到达的目的地的问题。然而,很多情况下,在游戏环境中,即使没有最终的目的地,也要让计算机控制的角色以仿真现实的方式移动(有点类似第三章讲的移动模式)。例如,在角色扮演游戏中,军队需要在城镇间的道路上巡逻,如下图:

这里写图片描述

上图中,标示为1的地形元素视为道路,标示为2的地形元素视为道路边界以外的区域。我们不想让巨人在道路上随机移动,这种结果看起来不自然,我们想让让巨人看起来像是沿着道路行走。因此,我们需要分析周围地形,决定巨人的路径。

在砖块环境中,在移动中通常有八种可能的方向供选择。我们会先检视这八个方向,再把不属于道路的部分剔除掉。然后,这个问题就编程了从剩下的方向中,决定出往某一个方向去行走。

例6-8说明了我们如何分析周围地形(分析可能的方向,把不属于道路的部分剔除掉)。

//例6-8:地形分析

int r;
int c;
int terrainAnalysis[9];
r=entityList[i].row;
c=entityList[i].col;

terrainAnalysis[1]=terrain[r-1][c-1];
terrainAnalysis[1]=terrain[r-1][c];
terrainAnalysis[1]=terrain[r-1][c+1];
terrainAnalysis[1]=terrain[r][c+1];
terrainAnalysis[1]=terrain[r+1][c+1];
terrainAnalysis[1]=terrain[r+1][c;
terrainAnalysis[1]=terrain[r+1][c-1];
terrainAnalysis[1]=terrain[r][c-1];

for(j=1;j<=8;j++)
{
    if(terrainAnalysis[j]==1)
        terrainAnalysis[j]=0;
    else
        terrainAnalysis[j]=10;
}

一开始我们定义了 terrainAnalysis 数组。这是我们存储计算机控制的巨人相邻的八个砖块地形数值的地方。我们的做法是对巨人当前的行、列位置做偏移。当这八个值都被存储之后,就进入 for 循环,确认是否每个值都是道路的一部分。如果不是道路的一部分,相对应的 terrainAnalysis 数组元素就指定为 0,否则就指定为10。

现在,我们已经把不属于道路的部分剔除掉,分析出了可能的方向。现在可以考虑到底要移往哪个方向。

不过我们想让巨人都朝着大致相同的方向前进(使其更智能),只在必要转弯时才转弯,到了要转换的时候,决定左转或右转,以能否完全改变方向为依据。因此我们要引入权重,权重可以使巨人都朝大致相同的方向前进(因为权值大的那个就是行走最多的那个方向)

就此范例来说,我们为每个方向分配了一个数字,让我们能记录当前方向,如图6-12 所示。

这里写图片描述

我们将以图6-12 的数字,记录每次更新巨人位置时移动的方向,这样就可以在更新巨人位置时,给先前的方向增加权重。例6-9说明了该做法。

//例6-9:方向分析

if(entityList[i].direction==1)
{
    terrainAnalysis[1]=terrainAnalysis[1]+2;
    terrainAnalysis[2]++;
    terrainAnalysis[5]--;
    terrainAnalysis[8]++;
}
if(entityList[i].direction==2)
{
    terrainAnalysis[1]++;
    terrainAnalysis[2]=terrainAnalysis[2]+2;
    terrainAnalysis[3]++;
    terrainAnalysis[6]--;
}
if(entityList[i].direction==3)
{
    terrainAnalysis[2]++;
    terrainAnalysis[3]=terrainAnalysis[3]+2;
    terrainAnalysis[4]++;
    terrainAnalysis[7]--;
}
if(entityList[i].direction==4)
{
    terrainAnalysis[3]++;
    terrainAnalysis[4]=terrainAnalysis[3]+2;
    terrainAnalysis[5]++;
    terrainAnalysis[8]--;
}
if(entityList[i].direction==5)
{
    terrainAnalysis[4]++;
    terrainAnalysis[5]=terrainAnalysis[5]+2;
    terrainAnalysis[6]++;
    terrainAnalysis[1]--;
}
if(entityList[i].direction==6)
{
    terrainAnalysis[2]--;
    terrainAnalysis[5]++;
    terrainAnalysis[6]=terrainAnalysis[6]+2;
    terrainAnalysis[7]++;
}
if(entityList[i].direction==7)
{
    terrainAnalysis[3]--;
    terrainAnalysis[6]++;
    terrainAnalysis[7]=terrainAnalysis[7]+2;
    terrainAnalysis[8]++;
}
if(entityList[i].direction==8)
{
    terrainAnalysis[1]++;
    terrainAnalysis[4]--;
    terrainAnalysis[7]++;
    terrainAnalysis[8]=terrainAnalysis[8]+2;
}

每个if 语句都会利用存储在 entityList[i].direction 中的当前方向,递增或递减 terrainAnalysis 数组的值。这样会让某些潜在的方向更能受青睐,同时让其他方向更不受青睐。

例如,第一个 if 语句会检查前一方向为1 的情况,也就是往左上方的移动。如果前一方向是1,我们就把 terrainAnalysis 数组下标为1 的元素的权重提高,做法是加上2以增加其值。当然,一直都往左上方移动是不可能的,也许已经走到了路的边缘。所以,我们也得考虑剩余的可能性。接下来两个最佳可能性是往上走或直接往左走,即我们也要替 terrainAnalysis 数组下标为 2 和8 的元素的值都增加1。元素 3、4、6、7则视为不相干的,而剩下的方向5 和当前方向完全相反,这个方向将是我们最后的选择,所以把terrainAnalysis 数组下标为 5 的元素权重减1。此例的说明如图6-13 所示。

这里写图片描述

下一步是根据权重,找出最佳方向,如例6-10所示。

//例6-10:选择一个方向

maxTerrain=0;
maxIndex=0;
for(j=1;j<=8;j++)
{
    if(terrainAnalysis[j] > maxTerrain)
    {
        maxTerrain=terrainAnalysis[j];
        maxIndex=j;
    }
}

如例6-10 所示,我们循环 terrainAnalysis 数组,找出可能方向中权重最高的一个。跳出 for 循环后,maxIndex 变量所含的数组索引值,就是代表权重最高的方向的元素。

例6-11 说明了我们如何使用 maxIndex 变量的值更新巨人的位置。

//例6-11:更新巨人位置

if(maxIndex==1)
{
    entityList[i].direction=1;
    entityList[i].row--;
    entityList[i].col--;
}
if(maxIndex==2)
{
    entityList[i].direction=2;
    entityList[i].row--;
}
if(maxIndex==3)
{
    entityList[i].direction=3;
    entityList[i].row--;
    entityList[i].col++;
}
if(maxIndex==4)
{
    entityList[i].direction=4;
    entityList[i].col++;
}
if(maxIndex==5)
{
    entityList[i].direction=5;
    entityList[i].row++;
    entityList[i].col++;
}
if(maxIndex==6)
{
    entityList[i].direction=6;
    entityList[i].row++;
}
if(maxIndex==7)
{
    entityList[i].direction=7;
    entityList[i].row++;
    entityList[i].col--;
}
if(maxIndex==8)
{
    entityList[i].direction=8;
    entityList[i].col--;
}

maxIndex 的值指出了巨人的新方向。我们对八个可能方向都以 if 语句加以判断。一旦找出所要的方向时,我们就更新 entityList[i].direction 的值,下一次巨人位置需要更新时,这个方向就会成为前一个方向。然后我们更新 entityList[i].row 和 entityList[i].col 的值。图6-14 是巨人沿着道路走的路径。

这里写图片描述

如图 6-14,巨人会持续沿着这条路走。

沿着墙走

开发游戏时,另一种有用的路径寻找方式就是沿着墙走。

沿着墙走更像是探索的技巧,适用于由许多小房间构成的游戏环境中,或者是类似迷宫的游戏环境中,让计算机控制的角色探索环境,以搜寻玩家、武器、燃料、宝物或任何游戏角色可以接触的东西。让计算机控制的角色在环境中随机移动,探索环境,是游戏开发人员最常用的办法。如图6-15。

这里写图片描述

在图6-15 的范例中,我们可以让巨人沿随机方向移动。不过比较好的做法是让巨人系统地探索整个环境。我们打算采用左侧移动法。即每次巨人总是尽量向其左边移动,这样就能对环境做出较为完整的探索工作。但是要注意,左侧移动法,并不能保证巨人会进入到游戏环境中的每个房间。

需要注意的是,巨人的左侧,不一定是玩家的左侧,图6-16 说明了这一点。

这里写图片描述

如图6-16 所示,巨人面对的是玩家的右侧,即标识为4 的方向。这也就是说方向2 是巨人的左侧,方向 6 是巨人的右侧,而 方向 8 是巨人的背后。采用左侧移动法时,巨人总是试着先往左边走。如果无法往左走,会试着往前走。如果也行不通,就会试着往右走。如果还是走不通,就会走回去(左侧移动法:左前右后)。在程序刚刚开始的时候,从玩家的观点来看,巨人会试着往上走。但是如图6-15 所示,有道墙挡住了去路,所以巨人必须改为直走,从巨人的角度来看,就是方向4。

左侧移动法如例6-12 所示。

r=entityList[i].row;
c=entityList[i].col;

if(entityList[i].direction==4)
{
    if(terrain[r-1][c]==1)
    {
        entityList[i].row--;
        entityList[i].direction=2;
    }
    else if(terrain[r][c+1]==1)
    {
        entityList[i].col++;
        entityList[i].direction=4;
    }
    else if(terrain[r+1][c]==1)
    {
        entityList[i].row++;
        entityList[i].direction=6;
    }
    else if(terrain[r][c-1]==1)
    {
        entityList[i].col--;
        entityList[i].direction=8;
    }
}
else if(entityList[i].direction==6)
{
    if(terrain[r][c+1]==1)
    {
        entityList[i].col++;
        entityList[i].direction=4;
    }
    else if(terrain[r+1][c]==1)
    {
        entityList[i].row++;
        entityList[i].direction=6;
    }
    else if(terrain[r][c-1]==1)
    {
        entityList[i].col--;
        entityList[i].direction=8;
    }
    else if(terrain[r-1][c]==1)
    {
        entityList[i].row--;
        entityList[i].direction=2;
    }
}
else if(entityList[i].direction==8)
{
    if(terrain[r+1][c]==1)
    {
        entityList[i].row++;
        entityList[i].direction=6;
    }
    else if(terrain[r][c-1]==1)
    {
        entityList[i].col--;
        entityList[i].direction=8;
    }
    else if(terrain[r-1][c]==1)
    {
        entityList[i].row--;
        entityList[i].direction=2;
    }
    else if(terrain[r][c+1]==1)
    {
        entityList[i].col++;
        entityList[i].direction=4;
    }
}
else if(entityList[i].direction==2)
{
    if(terrain[r][c-1]==1)
    {
        entityList[i].col--;
        entityList[i].direction=8;
    }
    else if(terrain[r-1][c]==1)
    {
        entityList[i].row--;
        entityList[i].direction=2;
    }
    else if(terrain[r][c+1]==1)
    {
        entityList[i].col++;
        entityList[i].direction=4;
    }
    else if(terrain[r+1][c]==1)
    {
        entityList[i].row++;
        entityList[i].direction=6;
    }
}

例 6-12 有四个 if 语句区块。我们必须为巨人面对的四个可能的方向,分别准备不同的 if 区块。因为巨人左方的砖块是什么,由巨人所面对的方向决定,如图6-17所示。

这里写图片描述

如图6-17 所示,如果以玩家的角度来看,巨人面对右侧,则其左侧就是位于其上的砖块。如果巨人面对上方,则其左侧的砖块,实际上就是左侧的砖块。如果其面对左侧,则面的砖块就是其左侧。如果面对下方,则右边的砖块才是巨人的左侧。

如第一个 if 区块所示,如果巨人面对方向 4,会先检测其左侧的砖块,也就是 terrain[r-1][c]。如果此位置的值为1,则表示无障碍物。此时,巨人的位置会被更新,更重要的是,其方向会更新为2。

如果检查terrain[r-1][c]时侦测到障碍物,则接着会检查巨人前方的砖块。如果也有障碍物,则会检查巨人右侧的砖块,最后是其背后的砖块。最后的结果就是巨人探索了整个游戏环境,如图6-18 所示。

这里写图片描述

由此可见,采用左侧移动法时,巨人会进入到游戏环境中的每个房间中。虽然这种做法概念上很简单,而且多数情况下非常有效,但并不能保证一切情况都适用。

航点导航

路径寻找是一项非常耗时,且耗用CPU资源的运算工作。减少这种困扰的方式之一,就是尽可能预先算好路径。航点导航减少这种困扰的做法就是,在游戏环境中置放节点,然后,使用预先计算好的路径,或者是简单的路径寻找方法在节点之间移动。

图6-19 说明了如何在一张由七个房间构成的简单地图上置放节点。

这里写图片描述

在图6-19 中,注意图中的每个节点,都至少应该能让另一个节点的实现看见。对于以这种方式构成的游戏环境而言,游戏控制角色能使用简单的视线算法,到达图中的任何地点(因为两点就能画条线,也是视线追踪的本质)。游戏软件 AI 只需要知道这些节点之间的关系。图6-20 说明了如何标识节点,并能让图中的每个节点能连在一起。

这里写图片描述

利用图6-20 的节点标示符号以及连线,现在我们就可以找出房间之间的路径了。例如,要从含有节点 A 的房间,走到含有 节点 E 的房间,就必须走过节点 ABCE。节点间的路径由视线追逐算法求得。图6-21 显示了由三角形表示的计算机控制的角色,抵达由方形表示的玩家控制角色的路径的过程。

这里写图片描述

计算机控制的角色首先计算哪一个节点和他当前的位置最接近,并且在视线内。在此例中,那个节点就是A,然后该角色会计算哪一个节点最接近玩家当前位置,而且在玩家视线之内,那就是节点 E。然后计算机会规划出从当前位置到节点 A 的路径,接着利用节点的连接关系,再从节点 A 走到节点 E,就此例而言就是 A->B->C->E。抵达到最后的节点 E 之后,再算出最终节点到玩家的视线路径。

说起来很简单,但计算机怎么知道要走哪些节点呢?我们利用简单的数据表格分析一下,图6-22 是最初空白的节点连接表

这里写图片描述

分析后,可以得到完成后的节点连接图,如图6-24 所示。

这里写图片描述

利用图6-24 完成的表格,就可以确认从任意点到其他任意点要走的路径。

总结:

本章讨论的每种方法都有其优缺点,而且使用范围往往比较局限。下一章将要介绍的A* 算法,可适用大部分的路径寻找问题也是游戏中极为常用的路径寻找算法。

猜你喜欢

转载自blog.csdn.net/Jurbo/article/details/75427143