寻路算法浅析

很多游戏特别是rts,rpg类游戏,都需要用到寻路。
在游戏中,我们经常想找到从一个位置到另一位置的路径。我们不仅在寻找最短的距离;我们还希望考虑旅行时间。
要找到此路径,我们可以使用图搜索算法,该算法在将地图表示为图时起作用。
A *是图形搜索的流行选择。广度优先搜索是最简单的图形搜索算法,
这篇文章会介绍广度游戏搜索,然后介绍Dijkstra算法,最后逐步提高到A *

目录
BFS(广度优先搜索)
Dijkstra算法
贪婪优先搜索
A星算法浅析与实践
A星算法优化
B星todo

一、广度优先搜索(Breadth-first search (BFS))
图展示
1.0 广度优先搜索算法(简称BFS)又称为宽度优先搜索,是一种图形搜索算法

BFS是一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。
它并不考虑结果的可能位址,彻底地搜索整张图,直到找到结果为止
1.1 伪代码

首先将根节点放入队列中。
从队列中取出第一个节点current,并检验它是否为目标。
如果找到目标,则结束搜寻并回传结果。
否则将它所有尚未检验过的所有相邻节点加入队列中。
并设父节点为current
若队列为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”。
1.2 缺陷:

1、 时间复杂度是:T(n) = O(n^2),效率底下
2、每个顶点之间没有权值,无法定义优先级,不能找到最优路线。比如遇到水域需要绕过行走,在宽度算法里面无法涉及。
1.3 如何解决这个问题?

宽度优先搜索算法,解决了起始顶点到目标顶点路径规划问题,但不是最优以及合适的,因为它的边没有权值(比如距离),路径无法进行估算比较最优解。

在某些寻路方案中,不同类型的移动会有不同的成本。例如,在《文明》中,穿越平原或沙漠可能需要1个移动点,但穿越森林或丘陵可能需要5个移动点。

当我们希望探路者将这些费用考虑在内的时候,就引入了Dijkstra的算法

二、Dijkstra的算法【迪杰斯特拉算法】(or Uniform Cost Search)

图片展示

2.0 与广度优先搜索有何不同?

我们可以为每个顶点添加一个移动代价,因此让我们添加一个新变量cost_so_far,以从起始位置跟踪总移动代价。如果位置的新路径比先前的最佳路径更好,我们将添加它,规划到新的路线中。
2.1 伪代码:

初始时,将所有顶点的cost都设为最大值int.MaxValue。
将起始点cost设为0。
首先将根节点放入队列中。
开始循环
从队列中找cost值【代价】最小的节点current
如果找到目标,则结束搜寻并回传结果。
否则将它所有尚未检验过的所有相邻节点加入队列中。
循环current的所有相邻节点neighbors,并计算从起点到新节点的成本
new_cost = current.cost + neighbor.weight(每个格子默认权重为1,障碍50)
并设父节点为current
直至没有节点可以访问.
2.2 缺陷

1、时间复杂度是:T(n) = O(V^2),其中V为顶点个数。效率上并不高
2、目标查找不具有方向性
2.3 至此,我们都还是在BFS,只是加了个权重。那么如何解决让搜索不是全盘盲目瞎找?

我们来看Greedy Best First Search算法(贪婪最佳优先搜索)。

三、贪婪最佳优先搜索

图片展示

3.0 与Dijkstra的算法有何不同?

从上图中我们可以明显看到右边的算法(贪婪最佳优先搜索 )寻找速度要快于左侧,虽然它的路径不是最优和最短的,但障碍物最少的时候,他的速度却足够的快。
这就是贪心算法的优势,基于目标去搜索,而不是完全搜索。
在Dijkstra算法中,优先队列采用的是,每个顶点到起始顶点的预估值来进行排序 在贪婪最佳优先搜索中,我们采用每个顶点到目标顶点的距离进行排序。
3.1 伪代码

首先将根节点放入队列中。
开始循环
从队列中找cost值【代价】最小的节点current
如果找到目标,则结束搜寻并回传结果。
否则将它所有尚未检验过的所有相邻节点加入队列中。
循环current的所有相邻节点neighbors,并计算新节点到终点的代价
new_cost = GetDistance(neighbor, end);
并设父节点为current
直至没有节点可以访问.
3.1.2 GetDistance补充

每个顶点到目标顶点的距离这个距离有多种方式可以计算:

一、对角线距离【允许对角运动】 有下面这3种表示方法

①return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)
②return D * max(dx, dy) +(D2 - D) * min(dx, dy)
③Patrick Lester if(dx > dy) (D * (dx - dy) + D2 * dy) else (D * (dy - dx) + D2 * dx)
二、曼哈顿距离【不允许对角】

return abs(a.x - b.x) + abs(a.y - b.y)
三、欧几里得距离

return D * sqrt(dx * dx + dy dy)
不要用平方根,这样做会靠近g(n)的极端情况而不再计算任何东西,A
退化成BFS:
3.2 缺陷

路径不是最短路径,只能是较优

四、A星算法

图片展示
4.0 与Dijkstra 算法和 贪婪最佳优先搜索的不同?

它吸取了Dijkstra 算法中的cost_so_far,为每个边长设置权值,不停的计算每个顶点到起始顶点的距离(G),以获得最短路线,
同时也汲取贪婪最佳优先搜索算法中不断向目标前进优势,并持续计算每个顶点到目标顶点的距离(Heuristic distance),以引导搜索队列不断想目标逼近,从而搜索更少的顶点,保持寻路的高效。
4.1 思路

①在明确地算出了顶点到起始顶点的距离(G)和顶点到目标顶点的距离(H),
②我们只要在选择走哪个格子的时候,选择G+H的值最小的格子,这个值暂且命名为F,F = G + H,该值代表了在当前路线下选择走该方块的代价。
4.2.0 F = G + H详解

下图中A是起点,B是终点,方格中的数值,左上方表示G值,右上方表示H值,下方表示F值。

图片展示
举例:鼠标指向的方格,G值为14(斜向),H值为28(当前节点距离终点B最近的距离为两个对角线的距离也就是28),所以F=G+H = 42

4.2.1 A星算法核心公式就是F值的计算:

F = G + H
F - 方块的总移动代价
G - 开始点到当前方块的移动代价
H - 当前方块到结束点的预估移动代价

4.2.2 G值是怎么计算的?

假设现在我们在某一格子,邻近有8个格子可走,
当我们往上、下、左、右这4个格子走时,移动代价为10;
当往左上、左下、右上、右下这4个格子走时,移动代价为14;即走斜线的移动代价为走直线的1.4倍。
这就是G值最基本的计算方式,适用于大多数2.5Drpg页游。
拓展公式:

G = 移动代价 * 代价因子
根据游戏需要,G值的计算可以进行拓展。如加上地形因素对寻路的影响。
如给平地地形设置代价因子为1,丘陵地形为2,在移动代价相同情况下,平地地形的G值更低,算法就会倾向选择G值更小的平地地形。

4.2.3 H值是如何预估出来的?

很显然,在只知道当前点,结束点,不知道这两者的路径情况下,我们无法精确地确定H值大小,所以只能进行预估。
有多种方式可以预估H值,如曼哈顿距离、欧式距离、对角线估价,最常用最简单的方法就是使用曼哈顿距离进行预估:
H = 当前方块到结束点的水平距离 + 当前方块到结束点的垂直距离

最后,A星算法还需要用到两个列表:

开放列表- 用于记录所有可考虑选择的格子
封闭列表- 用于记录所有不再考虑的格子

4.3 A星算法伪代码:

首先将根节点放入队列中。
将根节点放入封闭列表
开始循环
从队列中找f值【总代价】最小的节点current
如果找到目标,则结束搜寻并回传结果。
否则将它所有尚未检验过的所有相邻节点加入队列中。
假如某邻近点既没有在开放列表或封闭列表里面,则计算出该邻近点的g值和h值,并设父节点为P,然后将其放入开放列表
gCost = current.gCost + GetDistance(current, neighbor);
hCost = GetDistance(end, neighbor);
直至没有节点可以访问.

图片展示
4.4 核心算法实现:

/// <summary>
    /// A*计算
    /// </summary>
    IEnumerator Count ()
    {
        //等待前面操作完成
        yield return new WaitForSeconds (0.1f);
        //添加起始点
        openList.Add (grids [startX, startY]);
        //声明当前格子变量,并赋初值
        Grid currentGrid = openList [0] as Grid;
        //循环遍历路径最小F的点
       while (openList.Count > 0 && currentGrid.type != GridType.End) {
           //获取此时最小F点
           currentGrid = openList [0] as Grid;
           //如果当前点就是目标
           if (currentGrid.type == GridType.End) {
               Debug.Log ("Find");
               //生成结果
               GenerateResult (currentGrid);
           }
           //上下左右,左上左下,右上右下,遍历
           for (int i = -1; i <= 1; i++) {
               for (int j = -1; j <= 1; j++) {
                   if (i != 0 || j != 0) {
                       //计算坐标
                       int x = currentGrid.x + i;
                       int y = currentGrid.y + j;
                       //如果未超出所有格子范围,不是障碍物,不是重复点
                       if (x >= 0 && y >= 0 && x < row && y < colomn
                           && grids [x, y].type != GridType.Obstacle
                           && !closeList.Contains (grids [x, y])) {
                           //计算G值
                           int g = currentGrid.g + (int)(Mathf.Sqrt ((Mathf.Abs (i) + Mathf.Abs (j))) * 10);
                           //与原G值对照
                           if (grids [x, y].g == 0 || grids [x, y].g > g) {
                               //更新G值
                               grids [x, y].g = g;
                               //更新父格子
                               grids [x, y].parent = currentGrid;
                           }
                           //计算H值
                           grids [x, y].h = Manhattan(x, y);
                           //计算F值
                           grids [x, y].f = grids [x, y].g + grids [x, y].h;
                           //如果未添加到开启列表
                           if (!openList.Contains (grids [x, y])) {
                               //添加
                               openList.Add (grids [x, y]);
                           }
                       }
                   }
               }
           }

           //重新排序
           openList.Sort();

4.5 A*算法优化

1、选择排序更快的二叉树来作为开放列表,帮助我们更快地从开放列表中取出F值最小的点;
2、对何种情况下可以走斜线路径加以判断;
3、采用布兰森汉姆算法预先判断两点是否可以直接通行,可通行就直接返回两点的直线路径,不可直接通行再采用A星算法寻路,提高寻路效率;
4、A星算法得出寻路路径后,可采用弗洛伊德算法对路径进行平滑处理,使人物走动更为自然
5、设计两个或者更多寻路系统以便使用在不同场合,取决于路径的长度。这也正是专业人士的做法,用大的区域计算长的路径,然后在接近目标的时候切换到使用小格子/区域的精细寻路。
6. 从起点和终点同时开始找,双向A*

总结
BFS是盲目搜索,一直找邻居,直到找到目标;
迪杰斯特拉算法是基于代价的,算起点到当前点代价最小的;
贪婪优先算法是目标导向搜索,算当前点到目标代价最小的;
A星是结合迪杰斯特拉算法和贪婪优先算法的寻路,维护2个列表,开放和封闭列表,每次寻路的时 候,从开放列表找出一个总代价最小的节点,如果不是终点,就算出起点到当前位置的移动代价G和当前位置到终点的移动代价H,计算出总代价F,然后加入封闭列表,并把这个节点所有的邻居都遍历一遍,计算总代价,加入开放列表,然后循环遍历,直到找到终点

图片展示

  • 5
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值