游戏开发中的人工智能(三):移动模式

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

本文内容:许多游戏中经常出现固定模式的移动,比如守卫的巡逻行为,宇宙飞船的降落等。开发者可以将移动模式技术应用于特定行为的程序的编写中。


移动模式

本章主题是移动模式。移动模式是制造智能行为幻觉的简单方式。基本上,计算机控制的角色会根据一些预先定义好的模式移动,使其看起来好像是在执行复杂而绞尽脑汁的策略。

实现移动模式的标准做法是选取想要的模式,再将控制数据填入某个数组或多个数组。控制数据由特定的移动指令组成,比如向前移动再转弯,借此迫使计算机控制的物体或角色按所需模式移动。利用这些算法,你可以建立圆形、方形、蛇形、曲线以及任何类型的模式将之编制成一组精确的移动指令。

标准移动模式算法

标准移动模式算法使用控制指令(编码过的指令清单或数组),指示计算机控制的角色,在每一轮游戏循环中如何移动。每当循环运行一轮时,数组将编入索引值,以便处理下一组移动指令。

例3-1 给出了一组典型的控制指令。

//例3-1:控制指令数据结构

ControlData
{
    double turnRight; 
    double turnLeft;  
    double stepForward;
    double stepBackward;
};

此例中,turnRight 和 turnLeft 中存放的是右转或左转的角度值。如果是在砖块环境中,角色能够前进的方向有限,则turnRight 和 turnLeft 的意义就是向右或者向左转一格。stepForward 和 stepBackward 是向前或向后的距离或者是砖块数。

这个控制结构也可以包含其他指令,比如开火、丢炸弹、放出干扰雷达的金属片、加速、减速、以及其他许多适合你的游戏的行为。

通常,你可以事先定义出控制结构体类型的全局数组或者一组数组,以便储存模式数据。设定这些模式数组初值的数据,可以从数据文件中加载或者直接编写在游戏程序中。这与你的编码风格以及游戏所需的条件有关。

直接在游戏程序中初始化模式数组,如例3-2 所示。

//例3-2:模式的初始化

Pattern[0].turnRight=0;
Pattern[0].turnLeft=0;
Pattern[0].stepForward=2;
Pattern[0].stepBackward=0;

Pattern[1].turnRight=0;
Pattern[1].turnLeft=0;
Pattern[1].stepForward=2;
Pattern[1].stepBackward=0;

Pattern[2].turnRight=10;
Pattern[2].turnLeft=0;
Pattern[2].stepForward=0;
Pattern[2].stepBackward=0;

Pattern[3].turnRight=10;
Pattern[3].turnLeft=0;
Pattern[3].stepForward=0;
Pattern[3].stepBackward=0;

Pattern[4].turnRight=0;
Pattern[4].turnLeft=0;
Pattern[4].stepForward=2;
Pattern[4].stepBackward=0;

Pattern[5].turnRight=0;
Pattern[5].turnLeft=0;
Pattern[5].stepForward=2;
Pattern[5].stepBackward=0;

Pattern[6].turnRight=0;
Pattern[6].turnLeft=10;
Pattern[6].stepForward=0;
Pattern[6].stepBackward=0;
…

此例中,这个模式会指示计算机控制的角色向前移2个单位的距离,再向前移2个单位的距离,向右转10度,再向右转10度,向前移2个单位的距离,再向前移2个单位的距离,然后再向左移10度。这个特定的模式会让计算机控制的角色,以蛇行模式前进。

为了处理这个模式,必须有一个控制该模式数组的索引值,每当游戏循环运行一次时,就递增一次。并且,在每次循环中,都必须读取并执行该模式数组当前索引值对应的控制指令。例3-3 是这些步骤在程序中的概略写法。

//例3-3:运行模式数组

void GameLoop(void)
{
    …
    Object.orientation += Pattern[CurrentIndex].turnRight;
    Object.orientation -= Pattern[CurrentIndex].turnLeft;
    Objetct.x += Pattern[CurrentIndex].stepForward;
    Object.x -= Pattern[CurrentIndex].stepBackward;

    CurrentIndex++;
    …
}

基本的算法非常简单,操作细节会因游戏结构而有所不同。

编写好几个不同模式,存放在不同数组中也是很常见的做法,然后让计算机随机选取一个模式来使用,或者按照游戏中某些其他决策逻辑来决定。这样的技巧可以强化智能的错觉,让计算机控制的角色行为有更多的变化。

砖块环境中的移动模式

对砖块环境中的移动模式来说,所要采用的方法,与第二章中讨论砖块环境中视线追逐时所用的方法类似。在视线追逐中,我们采用 Bresenham 的直线扫描转换算法,事先算出了起点和终点间的距离。

本章也是用 Bresenham 的线段算法,计算不同的移动模式。如同第二章所讲的,需要将行和列的坐标的位置存储在一组数组内。然后,以不同的模式移动计算机控制的角色(此例为巨人),再走遍这些数组。

本章的路径比只有起点和终点的情况复杂的多。路径将由好几条线段构成。每条新线段的起始处就是前一条线段的终止处。你必须确保最后一条线段的终点,是第一条线段的起点,才能让巨人在周而复始的模式中移动。

可以算出四条线段,完成矩形移动模式。在第二章的视线函数中,每次执行时都会清除坐标的路径数组内容。然而,就此例而言,每条线段只是整个模式的一套线段而已。因此,每次要计算线段时,我们不必对路径数组初始化,只需要把新的线段路径添加到前一条线段路径之后。

此例中,在计算模式之前,我们要先初始化坐标数组,例3-4 是初始化坐标路径数组的函数。

//例3-4:初始化路径数组

void InitializePathArrays(void)
{
    int i;
    for(i=0;i<kMaxPathLength;i++)
    {
        pathRow[i]=-1;
        pathCol[i]=-1;
    }
}

如例3-4 所示,我们把两个数组的每个元素都初始化为-1。我们采用-1是因为-1在砖块环境中不是有效的坐标。在多数砖块环境中,左上角的坐标是(0,0),从该点开始,行和列会递增到整个砖块地图的大小。因此把路径数组中还未用到的元素赋值为-1。

例3-5 是修改后的 Bresenham 视线追踪算法,用于计算线段。

//例3-5:修改后的 Bresenham 视线追踪算法,用于计算线段

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

    for(i=0;i<kMaxPathLength;i++)
    {
        if((pathRow[i]== -1) && (pathCol[i]== 1))
        {
            currentStep=i;
            break;
        }
    }
    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++;
    if(currentStep >= kMaxPathLength)
        return;
    if(deltaCol>deltaRow)
    {
        fraction=deltaRow*2-deltaCol;
        while(nextCol != endCol)
        {
            if(fraction >= 0)
            {
                nextRow += stepRow;
                fraction =fraction-deltaCol;
            }
            nextCol=nextCol+stepCol;
            fraction=fraction+deltaRow;
            pathRow[currentStep]=nextRow;
            pathCol[currentStep]=nextCol;
            currentStep++;
            if(currentStep >= kMaxPathLength)
                return;
        }
    }
    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(currentStep >= kMaxPathLength)
                return;
        }
    }
}

整体来说,这个算法与第二章中的例2-8视线移动算法很类似。主要的差别是把初始化路径数组的程序代码,换成了一段新的程序代码。此例中,要让每条新线段都附加到前一条线段之后,所以每次这个函数被调用时,不必初始化路径数组。新加的一段程序代码是要判断在哪里把线段加上去。这就是用-1 初始化路径数组的地方。找到第一个碰到数值为-1之处,这里就是新线段的起点。利用例3-6的函数,现在就可以计算出第一个模式。我们打算采用简单的矩形巡逻模式。图3-1 是我们所想要的模式。

这里写图片描述

如图3-1 所示,我们把矩形模式的四个角标识出来,再标出想要移动的方向。利用这些信息,就能用例3-5 的 BuildPathSegment( ) 函数建立巨人的移动模式。例3-6 是初始化矩形模式的内容。

//例3-6:矩形模式

entityList[1].InitializePathArrays();
entityList[1].BuildPathSegment(10,3,18,3);
entityList[1].BuildPathSegment(18,3,18,12);
entityList[1].BuildPathSegment(18,12,10,12);
entityList[1].BuildPathSegment(10,12,10,3);
entityList[1].NormalizePattern();
entityList[1].patternRowOffset=5;
entityList[1].patternColOffset=2;

在例3-6中,首先调用 InitializePathArrays( ) 函数初始化路径数组。然后,利用图3-1 所示的坐标,计算出四条组成矩形模式的线段。当每条线段都被计算出来并存储在路径数组后,就调用 NormalizePattern( ) 将模式标准化,用相对坐标表示它而不是绝对坐标。这样做,标准化的模式才不会在游戏领域中和特定的起点位置绑在一起。一旦把模式建立起来并标准化后,就能再任何游戏当中使用。例3-7 是 NormalizePattern( ) 函数。

//例3-7:标准化函数

void ai_Entity::NormalizePattern(void)
{
    int i;
    int rowOrigin=pathRow[0];
    int colOrigin=pathCol[0];
    for(i=0;i< kMaxPathLength;i++)
    {
        if((pathRow[i]==-1) && (pathCol[i]==-1))
        {
            pathSize=i-1;
            break;
        }
    }
    for(i=0;i<pathSize;i++)
    {
        pathRow[i]=pathRow[i]-rowOrigin;
        pathCol[i]=pathCol[i]-colOrigin;
    }
}

如例3-7 所示,想要将模式标准化,只需要把存储在模式数组中的所有位置都减去起始位置即可。这样以相对坐标做出的模式,就能在任何游戏领域中使用了。

现在,模式已经建立了起来,我们可以通过数组,让巨人以矩形模式行走。注意到最终线段的最后两个坐标,就是最初线段最先的两个坐标。这样可以确保巨人重复的以矩形模式行走。

我们也可以利用 BuildPathSegment( ) 函数建立任何数目的模式。只需要求出所需模式的顶点坐标,然后计算出每条线段即可。 例3-8 是利用两条线段,建立起简单的来回巡逻模式。

//例3-8:简单巡逻模式

entityList[1].InitializePathArrays();
entityList[1].BuildPathSegment(10,3,18,3);
entityList[1].BuildPathSegment(18,3,10,3);
entityList[1].NormalizePattern();
entityList[1].patternRowOffset=5;
entityList[1].patternColOffset=2;

在例3-8 中,巨人只会在坐标(10,3)和(18,3)之间来回走动。对那些在城堡大门前巡逻,或者保护桥梁附近区域的任务而言,这种模式就很有用。巨人只会一直重复使用这个模式,直到敌人出现在视线内时就切换到追逐或攻击状态。

例3-9是一个比较复杂的模式,该示例建立了一个由八条线段组成的模式。

//例3-9:复杂巡逻模式

entityList[1].BuildPathSegment(4,2,4,11);
entityList[1].BuildPathSegment(4,11,2,24);
entityList[1].BuildPathSegment(2,24,13,27);
entityList[1].BuildPathSegment(13,27,16,24);
entityList[1].BuildPathSegment(16,24,13,17);
entityList[1].BuildPathSegment(13,17,13,13);
entityList[1].BuildPathSegment(13,13,17,5);
entityList[1].BuildPathSegment(17,5,4,2);
entityList[1].NormalizePattern();
entityList[1].patternRowOffset=5;
entityList[1].patternColOffset=2;

例3-9中建立了一个复杂的模式,把地形的元素也考虑进来了。巨人沿着河的西岸开始走,跨越北边的桥、巡逻到南方、跨越南方的桥、然后再回到北方的起始点。接着巨人会重复这个模式。图3-2展示了这个模式,还标示了建构这个模式的顶点。

这里写图片描述

虽然图3-2 所采用的模式方法,可以做出既长又复杂的模式,但这些模式看起来既冗长又易被猜透。

因此,我们将要介绍加入随机因素的移动模式。这个模式矩阵会用到二维数组,它将引导巨人沿着预先定义的路径。模式数组中的每个元素的值不是0就是1,只有在模式数组中元素的值为1时,巨人才可以移到相对应的行和列坐标。

实现这类移动模式首先要做的是,建立一个模式矩阵。如例3-10 所示,一开始将模式矩阵的初始值全都赋为0。

3-10:模式矩阵的初始化

for(i=0;i<kMaxRows;i++)
{
    for(j=0;j<kMaxCols;j++)
        pattern[i][j]=0;
}

整个模式矩阵都赋成0后,就可以把想要的移动模式坐标设为1了。在这里将使用 Bresenham 视线法的另一种算法,建立另一种模式。并不把行、列坐标储存在路径数组中,而是沿着线段,把模式矩阵中相对应行、列坐标设为1。然后,就可以多次调用 BuildPatternSegment( ) 函数,建立复杂的模式。例3-11 是建立这个模式的程序。

//例3-11:建立模式

BuildPatternSegment(3,2,16,2);
BuildPatternSegment(16,2,16,11);
BuildPatternSegment(16,11,9,11);
BuildPatternSegment(9,11,9,2);
BuildPatternSegment(9.2.3.6);
BuildPatternSegment(3,6,3,2);

每次调用 BuildPatternSegment( ) 函数时,该函数都会使用 Bresenham 线段算法,在模式矩阵中建立新线段。函数参数中的前两个是起点的行、列坐标,而后两个是终点的行、列坐标。线段中的每个点,在模式矩阵中都成为1。这个模式以图3-3 来说明。

这里写图片描述

图3-3标示出了模式矩阵中每个含有1的点。这些点是巨人可以行走之处。现在巨人在行进路线的点上,有很多有效的方向可以走。因此,巨人就不会重复来回走动,也不容易被预测。

每次更新巨人的位置时,需要检查模式数组周围的八个元素,找出有效的移动点,如例3-12所示。

//例3-12:沿着模式矩阵走

void ai_Entity::FollowPattern(void)
{
    int i,j;
    int possibleRowPath[8]={0,0,0,0,0,0,0,0};
    int possibleColPath[8]={0,0,0,0,0,0,0,0};
    int rowOffset[8]={-1,-1,-1,0,0,1,1,1};
    int colOffset[8]={-1,0,1,-1,1,-1,0,1};

    j=0;
    for(i=0;i<8;i++)
    {
        if(pattern[row+rowOffset[i]][col+colOffset[i]]==1)
        {
            if(!((row+rowOffset[i])==previousRow) && ((col+colOffset[i])==previousCol)))
            {
                possibleRowPath[j]=row+rowOffset[i];
                possibleColPath[j]=col+colOffset[i];
                j++;
            }
        }
    }
    i=Rnd(0,j-1);
    previousRow=row;
    previousCol=col;

    row=possibleRowPath[i];
    col=possibleColPath[i];
}

例3-12 中,一开始检查模式矩阵中巨人当前位置周围的八个点。当发现其值为1时,就把该坐标存储到 possibleRowPath 和 possibleColPath 数组中。每个点都检查过后,可以从找到有效点的数组中,随机选取新的坐标点。最后的结果就是每次巨人抵达模式矩阵中的顶点时,不会每次都转向同一个方向。

我们注意到,例3-12 的 rowOffset 和 colOffset 这两个变量的用途是为了避免写八个条件语句。利用一个循环,把这些值加到行和列的位置就可以走遍那八个相邻的砖块。例如,头两个元素加到当前的行和列位置之后就是当前位置的左上角砖块。

移动巨人时还需要考虑巨人之前的位置也位于有效移动数组中。如果更新巨人位置时选了那个点,就会造成意外的来回移动。因此,一定要利用 previousRow 和 previousCol 变量 追踪巨人先前的位置。然后建立有效移动数组时就可以将之前的点排除在外。

仿真物理环境中的移动模式

在上一章的连续环境的视线追踪中,我们介绍了利用物理驱动力追逐猎物的方法。这一章中,我们将继续以第二章在连续环境中,追逐和拦截场景作为基础,在仿真物理环境中实现路径移动模式。修改后的实例程序叫 AIDemo3-2,可以让计算机控制的载具能按预先定义好的模式移动。

控制结构

在仿真物理环境中,你无法使计算机控制的载具向前或向后走一步,也无法让其向左转或者向右转。你必须为物理引擎提供控制力信息,才能让计算机控制的载具,可以按照你想要的模式实际运行。

算法思路:

计算机从模式数组中选取第一组指令,然后施加到控制中的载具。每轮仿真运算时,物理引擎会处理这些指令,知道该组指令中指定的条件满足为止。此时,模式数组中的下一组指令就会被选出并执行。这个过程将一直重复,知道模式数组走完或者模式因某种原因而中断为止。

例3-13的程序代码是我们为此例设定的模式控制数据结构体。

//例3-13:移动模式控制 数据结构体

struct ControlData
{
    bool PThrusterActive;
    bool SThrusterActive;
    double dHeadingLimit;
    double dPositionLimit;
    bool LimitHeadingChange;
    bool LimitPositionChange;
}

控制数据会因为你的游戏中将要模拟的东西及其底层物理模型如何运作,而有所改变。此例中,我们控制的载具方位,仅仅由两侧的推进器施加的力控制。因此,我们在配置时,只需要考虑两种控制力,据此实现某种移动模式。

因此,例3-13 的数据结构体,包含了两个布尔成员变量 PThrusterActive 和 SThrusterActive ,指的就是哪个推动器应该启动。dHeadingLimit 和 dPositionLimit ,用于决定每组控制数据应该实施多久。例如,dHeadingLimit 指载具方向的改变。如果你想要载具转45度角,就把 dHeadingLimit 设成 45。如果把 LimitHeadingChange 设为 true,则每轮仿真循环中,执行指定的模式指令时也会检查 dHeadingLimit 的值。指令继续运行前,如果载具的方向和上次方向相比已经转好了,就会把下一条指令取出来。

dPositionLimit 存储的是这组指令运行前,相对于载具位置要移动的距离。如果 LimitHeadingChange 设为 true,则每轮仿真循环中,载具的相对位置会和 dPositionChange 来比较,以决定是否要从模式数组中取出下一组指令。

因为我们这里的位置和方向的改变采用相对值,因此也需要记录这些改变,才能从一组模式指令中转换到下一组。我们定义了另一个结构体,存储了载具从一组模式指令到下一组指令,其状态的变化。例3-14就是这个结构体。

//例3-14:记录状态改变的结构体

struct StateChangeData
{
    Vector InitialHeading;
    Vector InitialPosition;
    double dHeading;
    double dPosition;
    int CurrentControlID;
}

InitialHeading 和 InitialPosition 存储了从模式数组中选出,当前被控制的载具的方向和位置。每次模式数组的索引值增加时,就会取出新一组指令,而这两个成员变量就必须更新。接下来两个成员变量 dHeading 和 dPosition 存储了仿真运算过程中,当前这种模式指令实施时,方向和位置的变化。CurrentControlID 存储了模式数组中当前的索引值,指出当前正在执行的模式控制指令是哪一组。

定义模式

就此例而言,我们设立了三个模式。第一个是方形模式,第二个是蛇形模式,第三个是任意形状的模式。

对于方形和蛇形模式来说,我们建立了两个全局数组,名叫 PatrolPattern 和 ZigZagPattern,如例3-15 所示。

//例3-15:模式数组声明

#define _PATROL_ARRAY_SIZE 8
#define _ZIGZAG_ARRAY_SIZE 4

ControlData PatrolPattern[_PATROL_ARRAY_SIZE];
ControlData ZigZagPattern[_ZIGZAG_ARRAY_SIZE];

StateChangeData PatternTracking;

我们定义了一个全局变量 PatternTracking,用于记录这些模式执行时在位置和方向上的改变。

例3-16 和3-17 表示了这两个模式以适当的数据初始化。

//例3-16:方形巡逻模式初始化

PatrolPattern[0].LimitPositionChange=true;
PatrolPattern[0].LimitHeadingChange=false;
PatrolPattern[0].dHeadLimit=0;
PatrolPattern[0].dPositionLimit=200;
PatrolPattern[0].PThrusterActive=false;
PatrolPattern[0].SThrusterActive=false;

PatrolPattern[1].LimitPositionChange=false;
PatrolPattern[1].LimitHeadingChange=true;
PatrolPattern[1].dHeadLimit=90;
PatrolPattern[1].dPositionLimit=0;
PatrolPattern[1].PThrusterActive=true;
PatrolPattern[1].SThrusterActive=false;

PatrolPattern[2].LimitPositionChange=true;
PatrolPattern[2].LimitHeadingChange=false;
PatrolPattern[2].dHeadLimit=0;
PatrolPattern[2].dPositionLimit=200;
PatrolPattern[2].PThrusterActive=false;
PatrolPattern[2].SThrusterActive=false;

PatrolPattern[3].LimitPositionChange=false;
PatrolPattern[3].LimitHeadingChange=true;
PatrolPattern[3].dHeadLimit=90;
PatrolPattern[3].dPositionLimit=0;
PatrolPattern[3].PThrusterActive=true;
PatrolPattern[3].SThrusterActive=false;

PatrolPattern[4].LimitPositionChange=true;
PatrolPattern[4].LimitHeadingChange=false;
PatrolPattern[4].dHeadLimit=0;
PatrolPattern[4].dPositionLimit=200;
PatrolPattern[4].PThrusterActive=false;
PatrolPattern[4].SThrusterActive=false;

PatrolPattern[5].LimitPositionChange=false;
PatrolPattern[5].LimitHeadingChange=true;
PatrolPattern[5].dHeadLimit=90;
PatrolPattern[5].dPositionLimit=0;
PatrolPattern[5].PThrusterActive=true;
PatrolPattern[5].SThrusterActive=false;

PatrolPattern[6].LimitPositionChange=true;
PatrolPattern[6].LimitHeadingChange=false;
PatrolPattern[6].dHeadLimit=0;
PatrolPattern[6].dPositionLimit=200;
PatrolPattern[6].PThrusterActive=false;
PatrolPattern[6].SThrusterActive=false;

PatrolPattern[7].LimitPositionChange=false;
PatrolPattern[7].LimitHeadingChange=true;
PatrolPattern[7].dHeadLimit=90;
PatrolPattern[7].dPositionLimit=0;
PatrolPattern[7].PThrusterActive=true;
PatrolPattern[7].SThrusterActive=false;
//例3-17:蛇形模式初始化

ZigZagPattern[0].LimitPositionChange=true;
ZigZagPattern[0].LimitHeadingChange=false;
ZigZagPattern[0].dHeadLimit=0;
ZigZagPattern[0].dPositionLimit=100;
ZigZagPattern[0].PThrusterActive=false;
ZigZagPattern[0].SThrusterActive=false;

ZigZagPattern[1].LimitPositionChange=false;
ZigZagPattern[1].LimitHeadingChange=true;
ZigZagPattern[1].dHeadLimit=60;
ZigZagPattern[1].dPositionLimit=0;
ZigZagPattern[1].PThrusterActive=true;
ZigZagPattern[1].SThrusterActive=false;

ZigZagPattern[2].LimitPositionChange=true;
ZigZagPattern[2].LimitHeadingChange=false;
ZigZagPattern[2].dHeadLimit=0;
ZigZagPattern[2].dPositionLimit=100;
ZigZagPattern[2].PThrusterActive=false;
ZigZagPattern[2].SThrusterActive=false;

ZigZagPattern[3].LimitPositionChange=false;
ZigZagPattern[3].LimitHeadingChange=true;
ZigZagPattern[3].dHeadLimit=60;
ZigZagPattern[3].dPositionLimit=0;
ZigZagPattern[3].PThrusterActive=true;
ZigZagPattern[3].SThrusterActive=false;

方形模式中第一组指令对应数组元素 PatrolPattern[0],通知载具向前移200个单位的距离。此例中没有施加转向力,载具的向前推力已经启动而且维持常量。下一组模式指令时数组元素 PatrolPattern[1],通知载具启动左侧推进器右转,知道载具的方向已转过90度。元素 PatrolPattern[2] 的指令和元素 PatrolPattern[0]相同,以此类堆,最后就是数组中有八组指令,让载具在方形模式下运行。

蛇形控制数据和方形控制数据类似,载具首先往前移一点,然后转弯,再往前移一点,然后再转弯。只是,这次的转弯是从右到左,而且转弯的角度是限制在60度,而不是90度,最后的结果就是载具将以蛇形模式移动。

执行模式

此例中,我们在 Initialize( )函数中对模式初始化。当程序启动时,这个函数就会被调用。在这个函数内,我们也会调用名为 InitializePatternTracking( ) 的函数,对 PatternTracking 结构做初始化,如例3-18 所示。

//例3-18: InitializePatternTracking()函数

void  InitializePatternTracking()
{
    PatternTracking.CurrentControlID=0;
    PatternTracking.dPosition=0;
    PatternTracking.dHeading=0;

    PatternTracking.InitialPosition=Craft2.vPosition;
    PatternTracking.InitialHeading=Craft2.vVelocity;
    PatternTracking.InitialHeading.Normalize();
}

无论何时,调用 InitializePatternTracking() 函数之后,该函数就会把计算机控制的载具 Craft2 的当前位置和速度向量复制一份,存储在这个状态改变数据结构中。CurrentControlID 是指定模式数组中,当前的元素索引值。此时指定为0,表示第一个元素,此外位置和方向的最初修正值都先指定为0.

接下来,我们又定义了一个DoPattern( )函数,参数是指向模式数组的指针以及该数组中的元素数目。每次运行一轮仿真运算循环时,这个函数都必须被调用,实施模式控制数据并走过模式数组。

此例中,我们是在 UpdateSimulation( )函数中调用 DoPattern( ) 函数,如例3-19所示。

//例3-19:UpdateSimulation()

void UpdateSimulation()
{
    …

    if(Patrol)
    {
        if(!DoPattern(PatrolPattern,_PATROL_ARRAY_SIZE))
            InitializePatternTracking();
    }
    if(ZigZag)
    {
        if(!DoPattern(ZigZagPattern,_ZIGZAG_ARRAY_SIZE))
            InitializePatternTracking();
    }

    …

    Craft2.UpdateBodyEuler(dt);

    …
}

此例中,我们用两个全局变量和两个布尔量指出要执行哪个模式。如果 Patrol 设置为 true ,则使用方形模式。如果 ZigZag 设为 true,则使用蛇形模式。

使用这些标号时,如果有必要,可以中断某个模式。例如,如果在执行巡逻模式的过程中,其他逻辑运算侦测到了敌方载具进入了巡逻区域,你可以把 Patrol 标号设为 flase,而把 Charse 标号设为 true。这样可以让计算机控制的载具停止巡逻,开始追击敌人。

物理引擎处理所有作用在载具上的力和力矩之前,DoPattern( ) 函数必须先被调用,否则模式指令就不会包含在力和力矩的计算中。从 if 语句中可知,DoPattern( ) 返回的是布尔值。如果DoPattern( ) 函数返回 false,就表示指定模式已经走完。在这种情况下,这个模式会重新初始化,使得载具可以继续以该模式行动下去。

DoPattern 函数

//例3-20:DoPattern()

bool DoPattern(ControlData *pPattern,int size)
{
    int i=PatternTracking.CurrentControlID;
    Vector u;

    //检查模式数组中的下一组指令是否需要取出
    if( (pPattern[i].LimitPositionChange && (PatternTracking.dPosition >= pPattern[i].dPositionLimit)) || (pPattern[i].LimitHeadingChange && (PatternTracking.dHeading >= pPattern[i].dHeadingLimit)) )
    {
        InitializePatternTracking();
        i++;
        PatternTracking.CurrentControlID=i;
        if(PatternTracking.CurrentControlID >= size)
            return false;
    }

    //计算这组指令开始运行后方向上的改变
    u=Craft2.vVelocity;
    u.Normalize();
    double P;
    P=PatternTracking.InitialHeading * u;
    PatternTracking.dHeading=fabs(acos(p)*180/pi);

    //计算这组指令开始运行后位置上的改变
    u=Craft2.vPosition-PatternTracking.InitialPosition;
    PatternTracking.dPosition=u.Magnitude();

    //求出转向力系数
    double f;
    if(pPattern[i].LimitHeadingChange)
    {
        f=1-PatternTracking.dHeading / pPattern[i].dHeadingLimit;
    }
    else
        f=1;
    if(f<0.05)
        f=0.05;

    //依照当前这组指令的赋值施加转向力        
    Craft2.SetThrusters(pPattern[i].PThrusterActive,pPattern[i].SThrusterActive,f);
    return true;
}

DoPattern( ) 函数所做的第一件事是复制模式数组当前索引值 CurrentControlID 到一个临时变量 i 中以备后用。

接着,这个函数检查位置或方向改变值,是否已经到达这组控制指令的限制值。如果是,就把记录结构体重新初始化,使得下组指令可以进入。此外,模式数组的索引值会递增并测试指定模式是否已经到达尾端。如果是,这个函数就会在此时返回 false,否则会继续处理这个模式。

下一段程序代码是计算当前这组指令开始执行后,载具在方向上的改变。载具的方向是从其速度向量中取得的。要把方向的改变换算成角度。此例中,要先把速度向量复制到临时向量 u 中,然后将之转换成单位向量。接着,把存储在模式记录结构体中的最初方向向量和代表当前方向的单位向量 u 做内积,将结果放在标量变量 P 中。接着,使用向量内积的定义,注意到这两个向量都是单位向量,就可以取 P 的反余弦函数,计算出两个向量间的夹角。这样就会得到径度单位的角度,然后乘以180,再除以 pi,才会得到角度。

下一段程序代码是计算当前这组指令开始执行后载具在位置上的改变。求位置的变化是算出载具当前位置,然后再和存储在模式记录结构体中最初位置之间的向量,取差值。最后的向量数值就是距离的变化值。

接着,这个函数会求出适当的转向力系数,用以施加底层物理模型所定义的载具最大可用传动推力。求推力系数的步骤是让1减去方向变化值和所需方向变化值的比值。这个系数会把 SetThrusters( )函数传递给刚体 Craft2 ,而这个函数会把最大可用转向力乘以此系数,再把所得的推力,赋给载具左侧或右侧的推进器。

结果

方形路径:

这里写图片描述

蛇形路径:

这里写图片描述

任意路径:

这里写图片描述

代码下载

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

代码:AIDemo3-2

http://pan.baidu.com/s/1hswySRi

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值