工作时,和比自己成功的人在一起。玩耍时,和比自己快乐的人在一起。
郑重说明:本文适合对游戏开发感兴趣的初级及中级开发和学习者,本人力图将技术用简单的语言表达清楚。鉴于水平有限,能力一般,文章如有错漏之处,还望批评指正,谢谢。
我们在之前有关游戏 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。
判定算法
随着 x 坐标每次增加 1,那如何判定 y 应该是保持不变还是增加 1 呢?
我们来看一下经典的 Bresenham 直线算法是怎么判定的。
如下图所示,x坐标一直在增长,y坐标可以选择增加或保持不变:
这里的判断标准有一个:误差值 ε。将 ε 定义为:
ε = 直线与当前像素右边缘的交点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
- 随着 x 每次递增 1,ε 需要每次自然地递增 m,变成 ε = ε + m。
- 如果 ε > 0,ε 还要再减去 1,以保证该误差绝对值小于 1。这样 ε = ε - 1,从正变成了负。
由于斜率 m 是浮点数字,每次我们重新计算差值 ε 的时候都会涉及浮点运算,因此不会太高效。
考虑到我们每次都是考察 ε 与 0 的大小关系, 因此不等式两边同时乘以 Δx(正数) 之后并不影响误差值的判断结果。
将误差值从浮点数 ε 计算转化为整数 Ε,计算时候的表述为:
Ε 初始值为:Ε = ε * Δx = Δy - Δx
- Ε 每次自然地递增 Δy,变成 Ε = Ε + Δy
- 如果 Ε > 0, Ε 还要再减去 Δx,这样 E = E - Δx,从正变成了负。
由于 Δx 和 Δy 都是提前算好的整数,整个算法就是加加减减整数来更新误差值 Ε,根据其正或者负来决定 y 是保持不变还是增加 1,因此算法执行效率提升很大,变得很高效。
一般化的情况
上面的特殊画线是直线处于第一象限且直线角度不大于 45 度角的情况。类似的,还有其他另外七种情况。
我们相应的调整 x、y 的位置及正负即可,有兴趣的可以去查下具体调整的关系,我们这里知道基本的原理就可以,细节就不再赘述了。
二、盲目搜索算法: BFS
前面的直线算法,如果在找到的坐标点中存在阻挡,我们就要尝试别的算法了。
想象这样一种场景,我们站在寻路的起点,除了能感知到终点坐标值之外一无所知,就像一个盲人,我称这种方式为盲人摸象式。
此时,最合适的方式是什么?
就是拿着盲人的拐杖一圈一圈的探索,逐圈往外扩展搜索,直到拐杖告诉我们坐标点就是终点,此时寻路结束。
这种方式在计算机领域有对应的算法实现,就是广度优先算法(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* 的搜索形式:
A* 变种:JPS跳点算法/Theta*算法
跳点算法,算法思想和A*类似,但是有更好的优化。了解不多,这里不讲。
小结
本文我们讲述了一些经典的寻路算法:
- 最简单也最先尝试的直线寻路算法
- 盲人搜索算法BFS、贪心算法
- 集大成者 A* 算法
其中 A* 算法作为最经典的算法是很多高级寻路方案的基础,如 navmesh。
希望本文对大家有所帮助!
作者:我是码财小子,会点编程代码,懂些投资理财,期待你的关注,不要错过我后续的文章更新。