揭秘在召唤师峡谷中移动路径选择逻辑?

摘要:在游戏中,只须要鼠标轻轻的一点,系统会当即寻找离角色最近的一条路线。这背后的行为逻辑又有什么奥秘呢?

做者:JohnserfSeedhtml

在游戏中,当咱们须要让角色移动到指定位置时,只须要鼠标轻轻的一点就能够完成这简单的步骤,系统会当即寻找离角色最近的一条路线。算法

但是,这背后的行为逻辑又有什么奥秘呢? 你会怎么写这个寻路算法呢?windows

通常咱们遇到这种路径搜索问题,你们首先能够想到的是广度优先搜索算法(Breadth First Search)、还有深度优先(Depth First Search)、弗洛伊德(Floyd)、迪杰斯特拉(Dij)等等这些很是著名的路径搜索算法,可是在绝大多数状况下这些算法面临的缺点就暴露了出来:时间复杂度比较高。工具

因此,大部分环境里咱们用到的是一个名叫A* (A star)的搜索算法优化

说到最短路径呢,咱们就不得不提到广度优先遍历(BFS),它是一个万能算法,它不仅仅能够用在 寻路或者搜索的问题上。windows的系统工具:画板 中的油漆桶就是其比较典型一个的例子。spa

这里对路径搜索作一个比较简洁的示例3d

假设咱们是在一个网格上面进行最短路径的搜索htm

咱们只能上下左右移动,不能够穿越障碍物。算法的目的是为了能让你寻找到一条从起点到站点的最短路径blog

假设每次均可以上下左右朝4个方向进行移动排序

算法在每一轮遍历后会标记这一轮探索过的方块称为边界(Frontier),就是这些绿色的方块。

而后算法呢会循环往复的从这些边界方块开始,朝他们上下左右四个方向进行探索,直到算法遍历到了终点方块才会中止。而最短路径呢就是算法以前一次探索过的路径。为了获得算法探索过的整条路径呢,咱们能够在搜索的过程当中顺势记录下路径的来向。

好比这里方块上的白色箭头就表明了以前方块的位置

在每一次探索路径的时候,咱们要作的也只是额外的记录下这个信息

要注意,全部探索过的路径咱们须要将它们标记成灰色,表明它们“已经被访问过“,这样子算法就不会重复探索已经走过的路径了。

广度优先算法显然能够帮助咱们找到最短路径,不过呢它有点傻,由于它对路径的寻找是没有方向性的,它会向各个方向探测过去。

最坏的状况多是找到终点须要遍历整个地图,所以很不智能,咱们须要一个更加高效的算法。

就是本次咱们要介绍的A * (A star)搜索算法

A* Search Algorithm

”A*搜索算法“也被叫作“启发式搜索”

与广度优先不一样的是,咱们在每一轮循环的时候不会去探索全部的边界方块(Frontier),而会去选择当前“代价(cost)”最低的方块进行探索。

这里的“代价”就颇有意思了,也是A*算法智能的地方。

咱们能够把这里的代价分红两部分,一部分是“当前路程代价(可表示成f-cost)”:好比你从起点出发一共走过多少个格子,f-cost就是几。

另外一部分是“预估代价(可表示成g-cost)”:用来表示从当前方块到再终点方块大概须要多少步,预估预估因此它不是一个精确的数值,也不表明从当前位置出发就必定会走那么远的距离,不过咱们会用这个估计值来指导算法去优先搜索更有但愿的路径。

最经常使用到的“预估代价”有欧拉距离(Euler Distance)“,就是两点之间的直线距离(x1 - x2)^2 + (y1 - y2)^2

固然还有更容易计算的“曼哈顿距离(Manhattan Distance)”,就是两点在竖直方向和水平方向上的距离总和|x1 - x2|+|y1 - y 2|

曼哈顿距离不用开方,速度快,因此在A* 算法中咱们能够用它来充当g-cost。

接下来,咱们只要把以前讲到的这两个代价相加就得出了总代价:f-cost + g-cost。

而后在探索方块中,优先挑选总代价最低的方块进行探索,这样子就会少走不少弯路

并且搜索到的路径也必定是最短路径。

在第一轮循环中,算法对起点周围的四个方块进行探索,并计算出“当前代价”和“预估代价”。

好比这里的1表明从起步到当前方块走了1步

这里的4表明着方块到终点的曼哈顿距离,在这四个边界方块中,右边方块代价最低,所以在下一轮循环中会优先对它进行搜寻

在下一轮循环中,咱们已一样的方式计算出方块的代价,发现最右边的方块价值依然最低,所以在下一轮的循环中,咱们对它进行搜寻

算法就这样子循环往复下去,直到搜寻到终点为止

增长一下方块的数量级,A*算法一样能够找到正确的最短路径

最为关键的是,它搜寻的方块个数明显比广度优先遍历少不少,所以也就更高效。

理解了算法的基本原理后,接下来就是上代码了,这里我直接引用redblobgames的Python代码实现,由于人家实在写的太好了!

def heuristic(a, b): #Manhattan Distance
    (x1, y1) = a
    (x2, y2) = b
    return abs(x1 - x2) + abs(y1 - y2)

def a_star_search(graph, start, goal):
 frontier = PriorityQueue()
    frontier.put(start, 0)
    came_from = {}
    cost_so_far = {}
    came_from[start] = None
    cost_so_far[start] = 0
    
    while not frontier.empty():
        current = frontier.get()
        
        if current = goal:
            break
            
        for next in graph.neighbors(current):
            new_cost = cost_so_far[current] + graph.cost(current, next)
            if next not in cost_so_far or new_cost < cost_so_far[next]:
                cost_so_far[next] = new_cost
                priority = new_cost + heuristic(goal, next)
                frontier.put(next, priority)
                came_from[next] = current
                
    return came_from, cost_so_far

先来看看最上面几行,frontier中存放了咱们这一轮探测过的全部边界方块(以前图中那些绿色的方块)

frontier = PriorityQueue()

PriorityQueue表明它是一个优先队列,就是说它可以用“代价”自动排序,并每次取出”代价“最低的方块

frontier.put(start, 0)

队列里面呢咱们先存放一个元素,就是咱们的起点

came_from = {}

接下来的的 came_from 是一个从当前方块到以前的映射,表明路径的来向

cost_so_far = {}

这里的 cost_so_far 表明了方块的“当前代价”

came_from[start] = None
cost_so_far[start] = 0

这两行将起点的 came_from 置空,并将起点的当前代价设置成0,这样子就能够保证算法数据的有效性

while not frontier.empty():
 current = frontier.get()

接下来,只要 frontier 这个队列不为空,循环就会一直执行下去,每一次循环,算法从优先队列里抽出代价最低的方块

if current = goal:
 break

而后检测这个方块是否是终点块,若是是算法结束,不然继续执行下去

for next in graph.neighbors(current):

接下来,算法会对这个方块上下左右的相邻块,也就是循环中 next 表示的方块进行以下操做

new_cost = cost_so_far[current] + graph.cost(current, next)

算法会先去计算这个 next 方块的“新代价”,它等于以前代价 加上从 current 到 next 块的代价

因为咱们用的是网格,因此后半部分是 1

if next not in cost_so_far or new_cost < cost_so_far[next]:

而后只要 next 块没有被检测过,或者 next 当前代价比以前的要低

frontier.put(next, priority)

咱们就直接把他加入到优先队列,而且这里的总代价priority等于“当前代价”加上”预估代价“

priority = new_cost + heuristic(goal, next)

预估代价就是以前讲到的“曼哈顿距离”

def heuristic(a, b):     (x1, y1) = a     (x2, y2) = b     return abs(x1 - x2) + abs(y1 - y2)

以后程序就会进入下一次循环,重复执行以前的全部步骤

这段程序真的是写的特别巧妙,可能比较难以理解但是多看几遍说不定你就忽然灵光乍现了呢

拓展

若是把地图拓展成网格形式(Grid),由于图的节点太多,遍历起来会很是的低效

因而咱们能够吧网格地图简化成 节点更少的路标形式(WayPoints)

而后须要注意的是:这里任意两个节点之间的距离就再也不是1了,而是节点之间的实际距离

咱们还能够用自上而向下分层的方式来存储地图

好比这个四叉树(Quad Tree)

又或者像unity中使用的导航三角网(Navigation Mesh),这样子算法的速度就会获得进一步优化

另外,我还推荐redblobgames的教程

各类算法的可视化,以及清楚的看见各类算法的遍历过程、中间结果

以及各类方法之间的比较,很是的直观形象,对于算法的理解也颇有帮助。

参考:

[1]周小镜. 基于改进A算法的游戏地图寻径的研究[D].西南大学,2010.

[2]https://www.redblobgames.com/pathfinding/a-star/introduction.html 

[3]https://en.wikipedia.org/wiki/A_search_algorithm

 

点击关注,第一时间了解华为云新鲜技术~