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

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

2017-07-19 15:38 274 查看
接上文: 游戏开发中的人工智能(四):群聚

本文内容:靠势能移动在游戏 AI 程序中还算相当新颖。这个方法的最优越的地方在于可以同时处理追逐、闪躲、成群结队和避免碰撞等行为。我们专门研究的这个势函数叫做 Lenard-Jones 势函数。

以势函数实现移动

势函数的优点:

1、只用一个函数处理追逐和闪躲,不再需要先前介绍过的算法所牵涉到的其他条件和控制逻辑,也可以替我们处理避开障碍物的问题

2、操作起来很简单。我们唯一要做的就是计算两个单位(此处即计算机控制的单位以及玩家)之间的驱动力,然后将该驱动力施加到计算机控制单位的前端,作为转向力。

势函数的缺点:

在游戏里的单位和对象数量增多时,一旦彼此互动起来,势函数算法将会耗用大量的CPU资源。

什么是势函数

势函数属于物理学原理,我们主要使用势函数控制游戏里单位的行为。例如,我们可以使用势函数,建立成群结队的单位,仿真群体移动,处理追逐和闪躲,以及避开障碍物问题。我们专门研究的势函数叫做Lenard-Jones势函数。



物理学中,Lenard-Jones 势能代表的是,分子间吸引和排斥的势能。这里的 U 代表的是原子内的势能,和分子的间隔距离 r 成反比。A 和 B 是参数,与 m 和 n 这两个指数一样。如果我们取该势函数的导数(derivative),就可得到一个代表某力的函数。这个力函数根据这两个分子的接近程度,产生引力和斥力,就我们的情况而言,分子指的就是游戏中正在行动的单位。就是这种可以表示引力和斥力的能力,能让我们受益。通过调节参数,可以转化引力与斥力,这样就可以实现追逐和闪避了。除了追逐和闪避之外,使用斥力进行障碍物躲避,使用引力形成群体等。

图5-1 是指数 n 和 m 取不同值时,该势函数所画出的三条曲线。



追逐/闪躲

要以势能实现追逐或闪躲行为,我们只需在 AIDemo2-2(参见第二章)程序中加入一些程序代码。在那个范例程序中,我们在连续环境中模拟追击者和猎物这两个单位。函数 UpdateSimulation( ) 负责在游戏循环每运行一轮时,处理和玩家之间的互动并更新每个单位的状态。我们打算在该函数内加入两行,如 例5-1 所示。

//例5-1:追逐/闪躲 范例中的UpdateSimulation()

void UpdateSimulation()
{
double dt=_TIMESTEP;
RECT r;

//玩家控制Craft1
Craft1.SetThrusters(false,false);

if(ISKeyDown(VK_UP)
Craft1.ModulateThrust(true);
if(ISKeyDown(VK_DOWN)
Craft1.ModulateThrust(false);
if(ISKeyDown(VK_RIGHT)
Craft1.setThrusters(true,false);
if(ISKeyDown(VK_LEFT)
Craft1.setThrusters(false,true);

//做Craft2的AI
…

if(PotentialChase)
DoAttractCraft2();

//更新每台载具的位置
Craft1.UpdateBodyEuler(dt);
Craft2.UpdateBodyEuler(dt);

//更新屏幕
…
}


比较代码,可以发现,我们多加了一次 if 检查,看 PotentialChase 标号是否设为 true。 如果是,我们就执行计算机控制的单位 Craft2 的 AI,只是现在改用势函数了。DoAttractCraft2( ) 替我们做这件事。

基本上,该函数所做的就是用势函数算出两个单位间的引力和斥力,再把所得结果当成转向力施加到计算机控制的单位上。例5-2 是 DoAttractCraft2( ) 的函数。

例5-2:DoAttractCraft2()

void DoAttractCraft2()
{
// 对Craft2施加Lenard-Jones势能所得到的力
Vector  r = Craft2.vPosition - Craft1.vPosition;
Vector  u = r;

u.Normalize();

double  U, A, B, n, m, d;

A = 1000;    //引力强度
B = 31000;   //斥力强度
n = 2;       //引力衰减
m = 4;       //斥力衰减
d = r.Magnitude()/Craft2.fLength; //考虑到尺度伸缩的目的
U = -A/pow(d, n) + B/pow(d, m);   //这里实际上求出的是势能,后面就把这个势能当做力的大小了

Craft2.Fa = VRotate2D( -Craft2.fOrientation, U * u);    // U*u给力一个方向,然后通过旋转坐标轴后,在把该力加到单位上

Craft2.Pa.x = 0;
Craft2.Pa.y = Craft2.fLength / 2;

Target = Craft1.vPosition;
}


此函数里的程序代码以非常简单的方式实现了 Lenard-Jones 势函数。进入该函数之后,首先计算的是 Craft1 和 Craft2 之间的位移向量,做法就是取两者之间位置的向量差值。所得结果存储在向量 r 内,并将其复制到向量 u 内,以备后用。注意,u 也被换算成单位向量了。

接着,声明了几个局部变量对应 Lenard-Jones 势函数的各个参数。变量的命名正好直接对应先前讨论的参数。唯一多出来的新参数是 d。d 代表的是分隔距离 r 除以该单位的长度,这样得到的分隔距离就是以该单位的长度为单位换算出来的结果。

除了把 r 做除法运算得到 d 之外,其他参数都以某些常数值带入。其中
U = -A/pow(d, n) + B/pow(d, m);
这一行,会算出实际施加到计算机控制的单位的转向力。我们算出的实际上是力,但 U 是标量,根据此力是引力或斥力,会取负值或者正值。为了取得该力的向量,我们将之乘以单位向量 u,其方向是沿着连接两个单位的作用线。然后,所得结果会转换成固定在 Craft2 之上的局部坐标系之中,使其能为转向力。此转向力将施加在 Craft1 的前端,使其向前或远离目标 Craft1。

执行此修改后的追逐程序时,我们会看见计算机控制的单位,根据我们定义的参数追逐或闪躲玩家控制的单位。图5-2 是调整参数后产生的某些结果。



在图5-2(A)中:追击者朝猎物前进,当猎物和它擦身而过时(追过了),它会绕回来。当追击者太接近时,会突然转一下,以维持两单位间的分隔距离。

在图5-2(B)中:我们减少引力分量的强度(A参数的值减少一点),其结果就很像我们在第二章中提到的拦截算法。

在图5-2(C)中:我们增强引力的强度(A参数的值增加一点),其结果很像基本视线算法。

在图5-2(D)中:我们减少引力,增加斥力,并调整指数参数,结果计算机控制的单位就会逃离玩家。

避开障碍物

我们可以利用 Lenard-Jones 函数的斥力性质处理障碍物。就此而言,我们要把引力强度 A 这个参数设为 0,只留下斥力分量。然后,我们可以调整参数 B,决定斥力强度,以及指数 m 来调整衰减程度(例如,斥力的影响半径)。这样就能让我们有效地模拟圆形刚体。当计算机控制的单位控制这些物体之一时,斥力就会产生,迫使该单位远离该物或者绕过该物。

斥力的值是分隔距离的函数。当该单位靠近该物时,此力或许还算小,转弯就会使渐进的。然而如果该单位很靠近了,斥力就会变大,迫使该单位紧急转弯。

在 AIDemo5-1 中,我们在场景中做了好几个随机放置的圆形物体。然后,做了一个计算机控制的单位,使其随机选取路径。如图5-3所示。



黑圆点代表障碍物,而弯曲的路线就是计算机控制的单位在通过这个场景时留下来的轨迹。从图像可知,对于那些有一定距离的物体而言,该单位避开时的转弯很缓和。而当该单位发现和某物很接近时,就会采取断然转弯的策略。

游戏循环每运行一轮时,都会绕过存储在数组中的所有障碍物,对每个障碍物而言,都会计算其和该单位之间的斥力。计算过程如例5-3所示。

//例5-3:避开障碍物

void DoUnitAI(int i)
{
int j;
Vector Fs;
Vector Pfs;
Vector r,u;
double U,A,B,n,m,d;
Fs.x=Fs.y=Fs.z=0;
Pfs.x=0;
Pfs.y=Units[i].fLength/2.0f;

…

if(Avoid)
{
for(j=0;j<_NUM_OBSTACLES;j++)
{
r=Units[i].vPosition-Obstacles[j];
u=r;
u.Normalize();

A=0;
B=13000;
n=1;
m=2.5;
d=r.Magnitude() / Units[i].fLength;
U= -A/pow(d,n) + B/pow(d,m);

Fs+=VRotate2D( -Units[i].fOrientation,U*u);
}
}
Units[i].Fa=Fs;
Units[i].Pa=Pfs;
}


这里展示的斥力计算,本质上和追逐范例中使用的相同。然而,此例中将参数 A 设为0。此外,斥力计算是针对每个障碍物而言的。因此,斥力计算是封装在一个 for 循环内走过 Obstacles 数组。

成群结队

让我们把群体行为,作为游戏软件 AI 使用势函数的另一个实例说明。这种行为和群聚很类似,但是它不需要满足群聚的规则。我们要做的仅仅是计算群体中各个单位之间的 Lenard-Jones 驱动力。这些力的引力分量会让这些单位靠在一起,而斥力分量会让它们彼此远离。

例5-4 是建立成群结队的群体所用的势函数。

//例5-4:成群结队的群体

void    DoUnitAI(int i)
{
int     j;
Vector  Fs;
Vector  Pfs;
Vector  r, u;
double  U, A, B, n, m, d;

// 群聚AI开始
Fs.x = Fs.y = Fs.z = 0;
Pfs.x = 0;
Pfs.y = Units[i].fLength / 2.0f;

if(Swarm)
{
for(j=1; j<_MAX_NUM_UNITS; j++)
{
if(i!=j)
{
r = Units[i].vPosition - Units[j].vPosition;
u = r;
u.Normalize();

A = 2000;
B = 20000;
n = 1;
m = 2;
d = r.Magnitude()/Units[i].fLength;
U = -A/pow(d, n) + B/pow(d, m);

Fs += VRotate2D( -Units[i].fOrientation, U * u);
}
}
}

Units[i].Fa = Fs;
Units[i].Pa = Pfs;
//群聚AI结束
}


图5-5 说明了成群结队行为的结果。



我们也可以在例5-4 的成群结队算法内结合先前讨论过的追逐和避开障碍物算法。这会让你的群体不仅仅成群结队,还会去追猎物,路上碰到了障碍物还会避开,如例5-5所示。

//例5-5:成群结队、追逐及避开障碍物

void    DoUnitAI(int i)
{

int     j;
Vector  Fs;
Vector  Pfs;
Vector  r, u;
double  U, A, B, n, m, d;

// 群聚AI开始
Fs.x = Fs.y = Fs.z = 0;
Pfs.x = 0;
Pfs.y = Units[i].fLength / 2.0f;

if(Swarm)
{
for(j=1; j<_MAX_NUM_UNITS; j++)
{
if(i!=j)
{
r = Units[i].vPosition - Units[j].vPosition;
u = r;
u.Normalize();

A = 2000;
B = 20000;
n = 1;
m = 2;
d = r.Magnitude()/Units[i].fLength;
U = -A/pow(d, n) + B/pow(d, m);

Fs += VRotate2D( -Units[i].fOrientation, U * u);
}
}
}

if(Chase)
{
r = Units[i].vPosition - Units[0].vPosition;
u = r;
u.Normalize();

A = 10000;
B = 10000;
n = 1;
m = 2;
d = r.Magnitude()/Units[i].fLength;
U = -A/pow(d, n) + B/pow(d, m);

Fs += VRotate2D( -Units[i].fOrientation, U * u);
}

if(Avoid)
{
for(j=0; j<_NUM_OBSTACLES; j++)
{
r = Units[i].vPosition - Obstacles[j];
u = r;
u.Normalize();

A = 0;
B = 13000;
n = 1;
m = 2.5;
d = r.Magnitude()/Units[i].fLength;
U = -A/pow(d, n) + B/pow(d, m);

Fs += VRotate2D( -Units[i].fOrientation, U * u);
}
}

Units[i].Fa = Fs;
Units[i].Pa = Pfs;
// 群聚AI结束

}


关于最佳化的建议

用势能的群聚算法的复杂度是N*N的,无法使用于大量单位的情况。下面尝试通过优化,减小复杂度。

对于障碍物避开算法,对于相距较远的障碍物,可以不去计算势能。因为距离较远的话,无论引力或者斥力都很小,影响也小,设定一个分隔距离的阈值就好了。这样仅需要对所有的障碍物判断是否在阈值内。可以节省很多除法和指数运算。

另一种做法是可以把游戏领域分成网络,每一个格子都配置一个数组,来存储落在这个格子里面的障碍物数据。当计算某个单位的势能时,我们只需要处理与该单位同在一个格子里面的单位以及这个格子周围的一圈格子中的单位。如果你的游戏领域很广,包含了许多障碍物,这种做法可以节省大量的计算资源。代价是需要更多内存来存储数据,还需要处理这些网格和障碍物之间的配置,会增加复杂度。

也可以用这种网格方法,对成群结队算法做最佳化工作。一旦设好网格后,每一格都配有一份清单。然后游戏循环能运行一轮时,可以走遍存储单位的数组,确认每个单位位于哪一格。接着,把每一格的每个单位的参考点,都存进该格配置的清单里。然后,不必进入多层循环让每个单位都去比较每个单位,只需要游走每格清单中单位,以及相邻网格的清单。同样的,处理这些配置关系会使得算法变复杂,但是可以节省CPU的耗用量。这种技巧,时常用在流体动力算法的计算里,可以使N*N的复杂度,降低到N的常数倍

示例源代码 下载

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

代码:AIDemo5-1(成群结队、追逐及避开障碍物),AIDemo5-2(在四方墙内避开障碍物)

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