【技术·真相】谈一谈游戏AI - 真的搞懂寻路(一)

3,225 阅读8分钟

工作时,和比自己成功的人在一起。玩耍时,和比自己快乐的人在一起。


郑重说明:本文适合对游戏开发感兴趣的初级及中级开发和学习者,本人力图将技术用简单的语言表达清楚。鉴于水平有限,能力一般,文章如有错漏之处,还望批评指正,谢谢。


我们在之前有关游戏 AI 的文章中多次提到过,游戏 AI 基本都遵循 sense/think/act 的循环模式(人生也遵循这种模式,学习和认知世界/决策/行动)。

寻路作为一种 think 示例是非常典型的,在游戏中得到了广泛的使用。

所谓寻路,就是从起始点到终点之间,找出一系列连续可行走的坐标点,游戏中的 agent 就可以沿着这条路径,每隔一段时间移动一个点,最终到达。

寻路中涉及的概念有:

  • 具体寻路算法,也就是从起点开始,找到终点的方法
  • 地图表达,包括地图的具体分割,地图阻挡,2D/3D 地形

本文先来谈一谈寻路的基本算法。寻路算法的作用,就是通过规则找出所有这些路径坐标点。

一、直线寻路

首先介绍最简单也最常用的直线寻路。这是比较直观且容易想到的算法。在起始和终点之间拉一条直线,如果中间不存在阻挡,那寻路即告完成。

游戏中尤其是野外场景,阻挡物一般不会太多,直线寻路对于短距离寻路特别常用。其效率最高,因此寻路时,我们首先都会尝试直线寻路,当直线寻路遇到阻挡而不可行时,才会采取其他算法。


基本思路

程序算法的目的就是沿着一条起点和终点拉齐的直线,依次确定每一个点的坐标x、y值。

我们假定起点和终点都位于二维坐标系中。首先考虑一种简单的情况:x 依次递增,且 y 值也依次递增,且直线的角度小于 45 度。我们理解了这种情况之后再一般化到其他情况,会更容易。

如果 x 值沿着 x 轴每次移动 1 格,由于直线角度小于 45 度,因此 y 的增量不会大于 x 的增量 1。考虑到计算机中,坐标点都是整数,就需要用一系列离散的坐标点来拟合真实的直线。那么就需要确定沿着 y 轴什么时候 y 保持不变,什么时候 y 增加 1。

image.png


判定算法

随着 x 坐标每次增加 1,那如何判定 y 应该是保持不变还是增加 1 呢?

我们来看一下经典的 Bresenham 直线算法是怎么判定的。

如下图所示,x坐标一直在增长,y坐标可以选择增加或保持不变:

image.png

这里的判断标准有一个:误差值 ε。将 ε 定义为:

ε = 直线与当前像素右边缘的交点y值 - 当前像素上边缘的y值

  • 如果该 ε 小于0,如红线所示,则下一个像素的 y 坐标保持不变,也就是说下一个像素点的坐标应该是 (x + 1, y)
  • 如果该 ε 大于0,如蓝线所示,则下一个像素的y增加1,也就是说下一个像素点的坐标应该是 (x + 1, y + 1)

那么红线和蓝线代表的 ε 值具体是怎么计算的呢?

直线的斜率 m = Δy/Δx,其中 Δx、Δy 分别表示起点与终点 x 坐标与 y 坐标的差值。m 的值是浮点数,可以提前算好。

从上图中我们可以看到,误差值 ε 初始化值为:ε = m - 1 = Δy/Δx - 1

  1. 随着 x 每次递增 1,ε 需要每次自然地递增 m,变成 ε = ε + m。
  2. 如果 ε > 0,ε 还要再减去 1,以保证该误差绝对值小于 1。这样 ε = ε - 1,从正变成了负。

由于斜率 m 是浮点数字,每次我们重新计算差值 ε 的时候都会涉及浮点运算,因此不会太高效。

考虑到我们每次都是考察 ε 与 0 的大小关系, 因此不等式两边同时乘以 Δx(正数) 之后并不影响误差值的判断结果。

将误差值从浮点数 ε 计算转化为整数 Ε,计算时候的表述为:

Ε 初始值为:Ε = ε * Δx = Δy - Δx

  1. Ε 每次自然地递增 Δy,变成 Ε = Ε + Δy
  2. 如果 Ε > 0, Ε 还要再减去 Δx,这样 E = E - Δx,从正变成了负。

由于 Δx 和 Δy 都是提前算好的整数,整个算法就是加加减减整数来更新误差值 Ε,根据其正或者负来决定 y 是保持不变还是增加 1,因此算法执行效率提升很大,变得很高效。


一般化的情况

上面的特殊画线是直线处于第一象限且直线角度不大于 45 度角的情况。类似的,还有其他另外七种情况。

image.png

我们相应的调整 x、y 的位置及正负即可,有兴趣的可以去查下具体调整的关系,我们这里知道基本的原理就可以,细节就不再赘述了。


二、盲目搜索算法: BFS

前面的直线算法,如果在找到的坐标点中存在阻挡,我们就要尝试别的算法了。

想象这样一种场景,我们站在寻路的起点,除了能感知到终点坐标值之外一无所知,就像一个盲人,我称这种方式为盲人摸象式。

此时,最合适的方式是什么?

就是拿着盲人的拐杖一圈一圈的探索,逐圈往外扩展搜索,直到拐杖告诉我们坐标点就是终点,此时寻路结束。

image.png

这种方式在计算机领域有对应的算法实现,就是广度优先算法(BFS)。

很显然,这是一种全图搜索算法,从起点开始,一圈一圈往外,不会漏过任何一个坐标点。

由于是全图搜索,会搜索所有路径,因此可能存在多条路径到达终点。如果搜索所有路径,就一定能找出其中的最短路径。


三、盲人摸象搜索的变种:Dijkstra算法

广度搜索时,是按照固定的顺序依次搜索的。如果有加权值,或者游戏里不同的地形消耗不同。

针对不同方向的搜索代价不同,因而它的搜索类似于等高线:

同一条线上的代价一样,因而更倾向于代价小的。于是在广度搜索过程中,搜索的方向有优先级,待搜索的节点可以放置在一个优先级队列中。

由于仍然是广度搜索,即全图搜索,所以必定可以找出最优解。


四、目标搜索:贪心算法

上面的BFS算法的问题在于,全图扫描,完全没有辅助信息。想象一下,我们平时生活中的寻路,往往会环顾四周,首先确定一下方向,然后沿着特定的方向去搜索。

方向选择:待选择的点与目标点的距离越短越好,这往往称为启发函数

搜索策略:贪心,每次找离终点距离最短的点(例如曼哈顿距离)

但是我们知道贪心算法求出的不一定是最优解。看下下面的情况:

红线是贪心算法可能的寻路路径,但是绿线其实才是最优路径。

贪心算法的缺点:不一定能找出最短路径。下面图搜索中,贪心算法的路径是 S->A->E->T,实际的最优解是 S->B->D->T

因此我们得出结论,如果场景中阻挡不多,贪心算法可以很快找到路径,但是这个路径不一定是最优解。


五、综合搜索时间和最优解:A*算法

从上面的分析我们可以得知,BFS(或其变种 Dijkstra 算法)可以找到最优解但是搜索时间较长,贪心算法执行较快但是不一定能找到最优解, A* 算法就是在权衡二者优缺点的基础上提出来的。

A* 算法通过下面这个函数来计算每个节点的优先级:

f(n) = g(n) + h(n)

其中:

  • f(n)是节点 n 的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
  • g(n) 是节点 n 距离起点的消耗。
  • h(n) 是节点 n 距离终点的估计代价,这也就是 A* 算法的启发函数(类似于贪心算法)。

以下为上面所提各种搜索算法的搜索情况(地图为格子的情况),可以看到 A* 算法搜索的次数和 Greedy 贪心算法接近,同时也和 Dijkstra 算法一样找到了最优解。

简单总结一下: BFS 的搜索形式 vs Dijkstra 的搜索形式 vs A* 的搜索形式:

image.png image.png image.png


A* 变种:JPS跳点算法/Theta*算法

跳点算法,算法思想和A*类似,但是有更好的优化。了解不多,这里不讲。


小结

本文我们讲述了一些经典的寻路算法:

  • 最简单也最先尝试的直线寻路算法
  • 盲人搜索算法BFS、贪心算法
  • 集大成者 A* 算法

其中 A* 算法作为最经典的算法是很多高级寻路方案的基础,如 navmesh。


希望本文对大家有所帮助!

作者:我是码财小子,会点编程代码,懂些投资理财,期待你的关注,不要错过我后续的文章更新。