[笔记]A*寻路算法初探
写在开始之前
最近突然对各路游戏的寻路算法很感兴趣,于是去学习了下游戏里的AI们是如何寻路的。网上相关内容很多,但同时有些说法也不一,制作自己的A* 算法时也有因不同的说法而困惑。整理多方资料并自己实践之后,以下是我对寻路算法,尤其是A* 算法的一些自己的总结。以下为自己的思考与想法,可能不准确之处,请指正。
我本次的模拟比较简单,下面简述一下模拟环境:
- 地图是棋盘式的格子地图;
- 各个点没有权值,或者说权值为1;
- 只能上下左右走,不支持斜着走;
- 未考虑终点被包住而到达不了的情况,发生此类情况时,算法会遍历所有可到达点无果后才证明目标无法到达。
虽然模拟的比较简单,但是足以学习、说明问题。为了使算法更形象可视化,我给算法的过程做了小动画。成品存在了我的Github里:https://github.com/XieNaoban/Pathfinding 这里代码就不贴了。使用的java编写。由于是初学java,所以代码写的狗屎一样不要介意(讲道理我现在自己也看不下去了。不过做出来的GUI效果还挺满意。)另外一开始对这个寻路理解的比较混乱,体系没有建全,所以不同算法所在的函数也很不一样,函数分类也不合理(然而不高兴改了)。
寻路的基本思路
首先总结一下寻路的最常见的两个思路,遍历与贪心算法。
遍历:只考虑起点
这里说的遍历其实用的是Dijkstra算法或BFS,也就从起点开始一扩散出去寻找最短路径。也就是遍历距离起点最短距离为1、2、3……n-1、n的点。对于地形复杂比如有山有沼泽(即有权值)的地图(图),使用Dijkstra算法,用优先队列存储遍历到地图的每一个点时的权值和。对于一张平滑的也就是无权值(权值相同)的地图,Dijkstra的表现与BFS相同,即只需退化使用队列进行BFS遍历即可,无需使用Dijkstra算法。这里我的模拟的地图没有权值,直接使用了BFS。
黄色代表被搜索过,蓝色代表搜索完成后找到的路径(在这里显然也是最短路径)。格子中的值为从起点到本格子的最短距离。由图可知算法从起点向周围扩散,直到扩散到终点。
结论是,BFS(或带权图中用的Dijkstra)肯定找到最短路径,但是问题在于,它太耗时了,访问的点(黄色格子)太多了。想想就知道很多点完全没有必要遍历。
造成这一现象的原因是,这个算法只考虑了起点,一直找距离起点最短的路直到遇到终点。
贪心:只想着终点
试图不遍历,直接找到终点去,那基本就是使用贪心算法了吧。
以上的启发式函数仅仅考虑了起点,因而导致算法无目的地向四周扩散寻找。那既然是使用贪心算法,每次寻找目前为之的最优解,那么这次只要考虑终点在哪里就行了,一路向终点走,起点不用考虑。这使得算法疯狂试图向终点靠近。
结果发现无障碍情况下效果杠杠的,没有一块多余的白色。但是如果有障碍呢?
虽然一格都没有多搜索,但是游戏里AI要是这样走路那玩家肯定吃不消。而且还有一个问题,万一走到了死胡同里,算法就会判定无法到达终点,而事实却是是算法自己钻了牛角尖。所以必须允许算法倒退,从死胡同里后退一步或多步,换条路子走。可以使用栈即可实现。有时会发现换了路之后反而更快地到达了之前到过的一个点,于是有些点重复搜索了,重复搜索的话很有可能比无脑遍历都不划算。于是我关闭了重复搜索,效果好多了。
可以看到算法产生了很多失败的搜索(黄格子)(都是被自己走过的路堵死的。。。创战记既视感)但是搜索的路比遍历少多了,但是说起来,允许倒退重新寻路,说到底这也还算是是进行遍历了。。。有时候还不及遍历。(其实我搜索完后画的路径不是完全根据算法来的,算法找的路还要更绕,我很多地方已经根据黄格子的梯度抄近路了。)
同时贪心算法只考虑了终点,永远只向着终点方向走,不考虑目前离起点已经走了多少格了。
A*算法
A* 用的也是Dijkstra。每次找出当前期望值最高的点(即最可能最快速度通往终点的点),一步步搜索过去。Wiki上也说了,这是对Dijkstra’s Algorithm 的扩展,因为它使用了性能更好的启发式引导其搜索。
疑问:“最短”路径还是“最合理”路径
这是个令我困惑的问题。翻了些知乎或博客等,竟然众说纷纭。有人说“不是。因为‘启发’是不精确的。”。也有人说“尽管A*基于无法保证最佳解的启发式方法,A*却能保证找到一条最短路径。”这就很尴尬了。
看了这么多资料以及加上我以往的玩游戏经验,我认为A*只是个比较宽的概念,它可以找到最佳路径,也可以找的只是合理路径。既然用了启发,那么它很可能找的不是“最短”而只是说根据这个启发找到的最“合理”路径。但是也可能你的启发式函数做的很好,找到的最合理路径就是最佳路径(就比如文章开头的说的遍历,下面说)。关键就在于你给他的“启发”是怎么样的。 小时候玩红警还是什么游戏时就遇到过单位走的路不是最短路径。
核心:寻找启发式函数h(n)
A*既然用了Dijkstra,那它的基本过程就是:从优先队列里拉出期望最高的n点(在我的算法里表示为h(n)值最低),标记,并把它周围的未知点放进优先队列。所以关键就是这个h(n)。
h(n)怎么找?其实最简单的,上面讲的遍历说起来也是h(n)的一种。从起点进行遍历的
h(n) = n.t (t为距离起点的距离),也就是说而对距离近的优先搜索。而这个算法其实就是最短路径的算法,这里“最合理路径”就是“最短路径”。
而对于上面讲的贪心算法,他的引导函数可以看作是
h(n) = max(abs(n.x - end.x), abs(n.y - end.y)) ,哪边离终点近就走哪里。之前这是用贪心实现的,但如果用做A*的h(n)实现呢?
效果很好,不过这只能用在仅支持上下左右行走的场景,找的路也比较“合理”(虽然明显不是最短路径)。现在只考虑还有什么可以试试呢?
想到起点终点都要考虑进去,首先想到了这个:
h(n) = sqrt((n.x - end.x) ^ 2 + (n.y - end.y) ^ 2) + sqrt((n.x - start.x) ^ 2 + (n.y - end.y) ^ 2) 。即点n到起始点的距离加上n到终点的距离。也就是说,距离起点终点所在的直线越近,h(n)就越低。
效果还不错。但是问题在于sqrt计算成本太高。即时策略类游戏的话这不好。而且对于这次只有直着走的模拟有些大材小用了。而且还是有些部分是不需要搜索的。
还有几个可用的函数,一个和之前的h(n) = max(abs(n.x - end.x), abs(n.y - end.y)) 比较像:
h(n) = abs(x - end.x) + abs(y-end.y) 找出x、y轴方向哪边离终点远,哪边远倾向于走哪边。还有一个和sqrt那个比较像:
h = abs((x - end.x) * (start.y-end.y) - (start.x-end.x) * (y-end.y)) ,倾向与走与起点终点所在直线方向的平行线,这个个人觉得不错,比sqrt那个计算少,而且在棋盘式地图里效果也好。但是它也有一个问题:
他会搜索反向于终点的方向。所以可以考虑用多个函数组合:
h = abs((x - end.x) * (start.y-end.y) - (start.x-end.x) * (y-end.y)) + (abs(x - end.x) + abs(y-end.y))*500;
效果比单一使用好很多。虽然实际效果其实不比h(n) = max(abs(n.x - end.x), abs(n.y - end.y)) 强,但也给我们提供一个寻找h(n)的思路,将来可以运用到任意方向的地图上。其中里面不同函数还有权重,比如第二个函数乘以了500。权重可以按照实战效果来定。
不过你可能也根据我放的截图发现了,这些算法都倾向于走起点与终点所在直线之上。即每当越过一个障碍物,就试图重新靠近起点终点所在连线上。尽管连线上或许还有障碍物。这也是我应该改进的地方。
总结:“精度”还是“速度”
显然,复杂些的启发式函数或是会造成更多搜索的方法往往能取得更好的效果,但是同时速度也更慢。因此如何选择将是本算法的一道难题。对于本次模拟的图,甚至BFS的表现都非常良好,但对于很大的地图、有很多NPC的游戏,就得权衡运行速度与寻路精度了。只要运行结果看似合理不会引起玩家破口大骂,A*算法的目的也算是达到了。
[笔记]A*寻路算法初探的更多相关文章
- A*寻路算法的探寻与改良(一)
A*寻路算法的探寻与改良(一) by:田宇轩 第一部分:这里我们主 ...
- 深度学习课程笔记(十一)初探 Capsule Network
深度学习课程笔记(十一)初探 Capsule Network 2018-02-01 15:58:52 一.先列出几个不错的 reference: 1. https://medium.com/ai% ...
- A星寻路算法-Mind&Hand(C++)
//注1:Mind & Hand,MIT校训,这里指的理解与实现(动脑也动手) //注2:博文分为两部分:(1)理解部分,为参考其他优秀博文的摘要梳理:(2)代码部分,是C++代码实现的,源码 ...
- A星寻路算法介绍
你是否在做一款游戏的时候想创造一些怪兽或者游戏主角,让它们移动到特定的位置,避开墙壁和障碍物呢? 如果是的话,请看这篇教程,我们会展示如何使用A星寻路算法来实现它! 在网上已经有很多篇关于A星寻路算法 ...
- A*寻路算法探究
A*寻路算法探究 A*算法常用在游戏的寻路,是一种静态网路中求解最短路径的搜索方法,也是解决很多搜索问题的算法.相对于Dijkstra,BFS这些算法在复杂的搜索更有效率.本文在U3D中进行代码的测试 ...
- A*寻路算法
对于初学者而言,A*寻路已经是个比较复杂的算法了,为了便于理解,本文降低了A*算法的难度,规定只能横竖(四方向)寻路,而无法直接走对角线,使得整个算法更好理解. 简而言之,A*寻路就是计算从起点经过该 ...
- 算法:Astar寻路算法改进,双向A*寻路算法
早前写了一篇关于A*算法的文章:<算法:Astar寻路算法改进> 最近在写个js的UI框架,顺便实现了一个js版本的A*算法,与之前不同的是,该A*算法是个双向A*. 双向A*有什么好处呢 ...
- 算法:Astar寻路算法改进
早前写了一篇<RCP:gef智能寻路算法(A star)> 出现了一点问题. 在AStar算法中,默认寻路起点和终点都是N x N的方格,但如果用在路由上,就会出现问题. 如果,需要连线的 ...
- js实现A*寻路算法
这两天在做百度前端技术学院的题目,其中有涉及到寻路相关的,于是就找来相关博客进行阅读. 看了Create Chen写的理解A*寻路算法具体过程之后,我很快就理解A*算法的原理.不得不说作者写的很好,通 ...
随机推荐
- waypoints
http://imakewebthings.com/waypoints waypoints 滑冰122分钟 Cygwin http:/nxutils.sourceforge.net http://ba ...
- 【2017-03-31】JS-DOM操作:操作属性、彩虹导航栏、定时器、操作内容、创建元素并添加、操作相关元素
一.操作属性 1.什么是属性: <div class="div" id="div1" style="" ></div> ...
- MongoDB基础教程系列--第一篇 进入MongoDB世界
1.什么是MongoDB MongoDB是跨平台的.一个基于分布式文件存储的数据库.由C++语言编写.用它创建的数据库具备性能高.可用性强.易于扩展等特点.MongoDB将数据存储为一个文档,数据结构 ...
- 老李推荐:第5章5节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 获取系统服务引用
老李推荐:第5章5节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 获取系统服务引用 上一节我们描述了monkey的命令处理入口函数run是如何调用optionP ...
- python_21_线程+进程+协程
python_线程_进程_协程 什么是线程? -- os能够进行运算调度的最小单位,被包含在进程之中,是一串指令的集合 -- 每个线程都是独立的,可以访问同一进程下所有的资源 什么是进程? -- 每个 ...
- scss的初级学习随笔小计
$white: #fff;$three: #333;$six: #666;$nine: #999;$red: #fff;$orange: #f63;$yellow: #fc0;$opcity: rgb ...
- ARM中断处理过程
以s3c2440 ARM9核为例: 一:s3c2440 ARM处理器特性: 1.S3C2440支持个中断源,含子中断源: 2.ARM9采用五级流水线方式: 3.支持外部中断和内部中断: 二.s3c2 ...
- HashMap源码详解(JDK7版本)
一.内部属性 内部属性源码: //内部数组的默认初始容量,作为hashmap的初始容量,是2的4次方,2的n次方的作用是减少hash冲突 static final int DEFAULT_INITIA ...
- 关于阿里图标库Iconfont生成图标的三种使用方式(fontclass/unicode/symbol)
1.附阿里图标库链接:http://www.iconfont.cn/ 2.登录阿里图标库以后,搜索我们需要的图标,将其加入购物车,如图3.将我们需要的图标全部挑选完毕以后,点击购物车图标4.这时候右侧 ...
- 为已有表快速创建自动分区和Long类型like 的方法-Oracle 11G
对上一篇文章进行实际的运用.在工作中遇到有一张大表(五千万条数据),在开始的时候忘记了创建自动分区,导致现在使用非常不方便,查询的速度非常的满,所以就准备重新的分区表,最原始方法是先创建新的分区表,然 ...