我正在尝试制作一个AI伙伴,它会跟随玩家(由你控制)移动,目前它可以工作,但当我添加碰撞检测后,当伙伴碰到障碍物时效果就不太好了。我只是想知道有什么最好的方法(比如A*算法的实现)来让AI的移动更加平滑并避免障碍物?这是我当前的伙伴类中的更新方法:
public void update() { setBounds(getX(), getY(), getWidth(), getHeight()); float xDiff = Math.abs(player.getX() - getX()); float yDiff = Math.abs(player.getY() - getY()); if (player.getX() > getX() && xDiff > buddyDistance) { setX(getX()+speed); } else if (player.getX() < getX() && xDiff > buddyDistance) { setX(getX()-speed); } if (player.getY() > getY() && yDiff > buddyDistance) { setY(getY()+speed); } else if (player.getY() < getY() && yDiff > buddyDistance) { setY(getY()-speed); }}
回答:
一个易于实现且可能有效的解决方案是使用势场方法,这取决于你的障碍物类型。
这个想法很简单:玩家就像一个磁铁,吸引伙伴靠近自己。同时,障碍物会排斥伙伴,使其避开它们。
我将首先使用向量来解释,而不是Java,以便更易读。
假设b
是伙伴的位置,p
是玩家的位置,o_1, ... o_k
是你的障碍物位置。
b, p, o_1, ..., o_k
每个都是具有x
和y
坐标的二维向量。
那么向量(p-b)
是从伙伴指向玩家的向量。我们还需要向量(b-o_i)
,它是从障碍物i
指向伙伴的向量。此外,我们不直接使用向量(p-b)
和(b-o_i)
,而是先对它们进行归一化处理。
然后,normalized(p-b)
就是我们吸引伙伴到玩家所需的全部内容。
为了使伙伴远离障碍物,我们希望当伙伴靠近障碍物时排斥力强,当远离时排斥力小(甚至为零)。因此,一个明显的选择是用1/|b-o_i|
来缩放我们想要的方向,即normalized(b-o_i)
,其中|.|表示向量的范数。
现在,我们可以简单地混合所有这些“磁力”如下:
w = normalized(p-b) + normalized(b-o_1)/|b-o_1| + ... + normalized(b-o_l)/|b-o_k|
这个向量w
通常指向玩家,但每当伙伴靠近障碍物时,它就会被排斥,这正是你想要的效果。
但是,我们如何确保伙伴以正确的速度移动呢?这很简单。我们归一化w
,然后按速度缩放。也就是说,我们的最终速度向量是v = speed*w/|w|
这可以很容易地添加到你的代码中:
public void update() { setBounds(getX(), getY(), getWidth(), getHeight()); //我保留了你的代码中的这一行,但实际上我不知道它做什么 float dx = player.getX() - getX(); //注意:我删除了abs float dy = player.getY() - getY(); float norm = Math.sqrt(dx*dx + dy*dy); //归一化: float wx = dx/norm; float wy = dy/norm; for (obstacle o : obstacles) { //假设obstacles是一个包含障碍物类实例的可迭代数据结构 //注意,只需迭代附近的障碍物即可 dx = getX() - o.getX(); dy = getY() - o.getY(); norm = Math.sqrt(dx*dx + dy*dy); //归一化: float ox = dx/norm; float oy = dy/norm; //添加缩放以获得我们想要的排斥力 wx += ox/norm; wy += oy/norm; } float norm_of_w = Math.sqrt(wx*wx + wy*wy); float vx = speed * wx / norm_of_w; float vy = speed * wy / norm_of_w; setX(getX() + vx); setY(getY() + vy);}
不幸的是,有几点需要考虑:
- 不同类型的排斥可能比1/|b-o_i|更有效,例如1/|b-o_i|^2。
- 尝试调整力的大小可能会有所帮助,例如尝试
c*(b-o_i)/|b-o_i|
,使用不同值的c
(即ox = c*dx/norm;
等等)。如果c
太小,伙伴会一定程度上移动到障碍物中,如果c
非常大,伙伴在离障碍物较远时就会开始避开它们。对于不同大小的障碍物使用不同的c
值可能会得到更好的结果。 - 如果障碍物是圆形形状并且两个障碍物之间有足够的空间,避开障碍物效果最佳。否则,伙伴可能会陷入局部最优,玩家需要通过移动到一个可以将伙伴从局部最优中拉出来的位置来“救援”他。
- 如果障碍物不是圆形而是大型多边形,你可以尝试使用非常大的排斥力来覆盖多边形的大部分(即对于这种类型的障碍物使用非常大的
c
)。优点是伙伴可以避开障碍物,但不幸的是,如果你想让它靠近障碍物,它会因为强烈的排斥而拒绝靠近。 - 重要的是要记住,当伙伴和障碍物靠近时,
1/|b-o_i|
会很大。如果它们在同一位置,你的程序会尝试除以零。你可能需要检查这种情况并避免它。
这就是全部内容,但值得注意的是,通常在使用势场时,目标使用负电荷,障碍物使用正电荷,即
w = -|p-b| + 1/|b-o_1| + ... + 1/|b-o_k|
请注意,这里w
只是一个标量而不是向量。然后,应用梯度下降法向目标移动。这意味着计算w
相对于b.x
、b.y
的梯度。这个梯度然后指向到达玩家的方向,同时避开障碍物。这是一个比我提出的方法更好的方法,但需要更多的数学知识。你可以尝试或询问这是否是你想要的。
如果障碍物形状任意且局部最小值对你来说不可接受,最可能的最佳答案是使用Delaunay三角剖分结合漏斗算法。你可以在https://www.aaai.org/Papers/AAAI/2006/AAAI06-148.pdf中阅读更多相关信息
但我假设你更喜欢易于实现的东西。