深刻理解游戏中寻路算法

若是你玩过MMOARPG游戏,好比魔兽,你会发现人物行走会颇有趣,为了模仿人物行走的真实体验,他们会选择最近路线达到目的地,期间会避开高山或者湖水,绕过箱子或者树林,直到走到你所选定的目的地。javascript

这种看似寻常的寻路在程序实现起来就须要必定的寻路算法来解决,如何在最短期内找到一条路径最短的路线,这是寻路算法首先要考虑的问题。php

在这篇文章中咱们会按部就班来说解寻路算法是如何演进的,你会看到一种算法从简单到高效所遇到的问题,以及精进的过程,带着问题来阅读,理解更快。html

本篇主要包含如下内容:java

一、图,算法

二、宽度最优搜索,数据结构

三、Dijkstra 算法,测试

四、贪心算法,google

五、A*搜索算法,spa

六、B*搜索算法,code

 

一、游戏中的人物是如何寻路的

你所看到的人物行走方式:

开发人员实际所看到的方式:

或者是这种:

对于一张地图,开发人员须要经过必定的方案将其转换为数据对象,常见的就是以上这种把地图切个成网格,固然了地图的划分方式不必定非要用网格这种方式,采用多边形方式也能够,这取决于你的游戏,通常状况下,同等面积的地图采用更少的顶点,寻路算法会更快。寻路中经常使用的数据结构就是图,如下咱们先来了解一下。

 二、图

在讲寻路算法以前咱们先了解一种数据结构—图,数据结构是咱们进行算法运算的基础,好的数据结构除了方便咱们理解算法,还会提高算法的效率。网格某种意义上也是图的演变,只是图形变了而已,理解了图的概念能够帮助咱们更好理解寻路算法。

图的基本定义:

图的正式表达式是G=(V,E),V是表明顶点的集合,E和V是一种二元关系,能够理解为边,好比有条边从顶点U到顶点V结束,那么E能够用(u,v)来表示这条边。具体的有向图和无向图,也是边是否有方向来区分。为了方便理解,咱们文中全部的数据演示都是基于网格地图来进行讲解,如下是几种关系梳理,以A为顶点,BCDE为子顶点,咱们能够把每一个格子也看是一个顶点。

三、搜索算法

对一个图进行搜索意味着按照某种特定的顺序依次访问其顶点。对于多图算法来讲,广度优先算法和深度优先搜索算法都十分重要,由于它们提供了一套系统地访问图数据结构的方法。咱们着重讲解广度优先搜索算法。

深度优先搜索

深度优先算法和最小路径关系不大,咱们只简单介绍。

深度优先搜索算法(简称DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽量深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的全部节点为止。

 

广度优先搜索

广度优先搜索算法(简称BFS)又称为宽度优先搜索,是一种图形搜索算法,很适合用来探讨最短路径的第一个模型,咱们会顺着这个思路往下讲。

BFS是一种盲目搜寻法,目的是系统地展开并检查中的全部节点,以找寻结果。换句话说,它并不考虑结果的可能位址,完全地搜索整张图,直到找到结果为止它的步骤以下:

- 首先将根节点放入队列中。

- 从队列中取出第一个节点,并检验它是否为目标。

- 若是找到目标,则结束搜寻并回传结果。

- 不然将它全部还没有检验过的直接子节点(邻节点)加入队列中。

- 若队列为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”。

网格:

咱们看下代码(js):

var frontier = new Array();

frontier.put(start);

var visited = new Array();

visited[start] = true;



while(frontier.length>0){

    current = frontier.get();

     //查找周围顶点

    for(next in graph.neighbors(current)){

        var notInVisited = visited.indexOf(next)==-1;

        //没有访问过

        if(notInVisited) {

            frontier.put(next);

            visited[next] = true;

          }

       }
}

从上能够发现,宽度搜索就是以开始顶点为起点,访问其子节点(在网格中是访问周围节点),而后不断的循环这个过程,直到找到目标,这种算法比较符合常规逻辑,把全部的的顶点所有枚举一遍。不过这种方式也有很明显的缺点。

缺陷:

一、效率底下, 时间复杂度是:T(n) = O(n^2)

二、每一个顶点之间没有权值,没法定义优先级,不能找到最优路线。好比遇到水域须要绕过行走,在宽度算法里面没法涉及。

如何解决这个问题?咱们来看Dijkstra 算法。

四、Dijkstra 算法

宽度优先搜索算法,解决了起始顶点到目标顶点路径规划问题,但不是最优以及合适的,由于它的边没有权值(好比距离),路径没法进行估算比较最优解。为什么权值这么重要,由于真实环境中,2个顶点之间的路线并不是一直都是直线,须要绕过障碍物才能达到目的地,好比森林,湖水,高山,都须要绕过而行,并不是直接穿过。

好比我采用宽度优先算法,遇到以下状况,他会直接穿过障碍物(绿色部分 ),明显这个不是咱们想要的结果:

 

解决痛点:

寻找图中一个顶点到另外一个顶点的最短以及最小带权路径是很是重要的提炼过程。为每一个顶点之间的边增长一个权值,用来跟踪所选路径的消耗成本,若是位置的新路径比先前的最佳路径更好,咱们将添加它,规划到新的路线中。

Dijkstra 算法基于宽度优先算法进行改进,把当前看起来最短的边加入最短路径树中 ,利用贪心算法计算并最终可以产生最优结果的算法。具体步骤以下:

一、每一个顶点都包含一个预估值cost(起点到当前顶点的距离),每条边都有权值v ,初始时,只有起始顶点的预估值cost为0,其余顶点的预估值d都为无穷大 ∞。

二、查找cost值最小的顶点A,放入path队列

三、循环A的直接子顶点,获取子顶点当前cost值命名为current_cost,并计算新路径new_cost,new_cost=父节点A的cost+v(父节点到当前节点的边权值),若是new_cost<current_cost,当前顶点的cost=new_cost

四、重复2,3直至没有顶点能够访问.

咱们看下图例:

咱们看下代码(js):

var frontier = new PriorityQueue();

frontier.put(start);

path = new Array();

//每一个顶点路径消耗

cost_so_far = new Array();



path[start] = 0;

cost_so_far[start] = 0



while(frontier.length>0){

   current = frontier.get();


   if current == goal:

      break

       //查找周围节点

    for(next in graph.neighbors(current)){

        var notInVisited = visited.indexOf(next)==-1;

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

           //没有访问过或者路径更近

        if(notInVisited ||  new_cost < cost_so_far[next]) {

            cost_so_far[next] = new_cost;

            priority = new_cost;

            frontier.put(next, priority);

            path[next] = current;

          }

       }

}

 

咱们看到虽然Dijkstra 算法 虽然相对于宽度优先搜索更加智能,基于cost_so_far ,能够规避路线比较长或者没法行走的区域,但依然会存在盲目搜索的倾向,咱们在地图中常见的状况是查找目标和起始点的路径,具备必定的方向性,而Dijkstra 算法从上述的图中能够看到,也是基于起点向子节点全方位扩散。

缺点:

一、运行时间复杂度是:T(n) = O(V^2),其中V为顶点个数。效率上并不高

二、目标查找不具备方向性

如何解决让搜索不是全盘盲目瞎找?咱们来看Greedy Best First Search算法(贪婪最佳优先搜索)。  

五、贪婪最佳优先搜索

在Dijkstra算法中,我已经发现了其最终要的缺陷,搜索存在盲目性。在这里,咱们只针对这个痛点,采用贪婪最佳优先搜索来解决。如何解决?咱们只需稍微改变下观念便可,在Dijkstra算法中,优先队列采用的是,每一个顶点到起始顶点的预估值来进行排序。在贪婪最佳优先搜索中 ,

解决痛点:

咱们采用每一个顶点到目标顶点的距离进行排序。一个采用离起始顶点的距离来排序,一个采用离目标顶点距离排序(离目标的远近排序)

哪一个更快?咱们看下图(左边宽度优先,右边贪婪优先):

从上图中咱们能够明显看到右边的算法(贪婪最佳优先搜索 )寻找速度要快于左侧,虽然它的路径不是最优和最短的,但障碍物最少的时候,他的速度却足够的快。这就是贪心算法的优点,基于目标去搜索,而不是彻底搜索。

 

咱们看下算法(js):

frontier = new PriorityQueue();

frontier.put(start, 0)

came_from = new Array();

came_from[start] = 0;



while(frontier.length>0){

    current = frontier.get()



   if current == goal:

      break



       for(next in graph.neighbors(current)){

           var notInVisited = visited.indexOf(next)==-1;

           //没有访问过

        if(notInVisited ) {

            //离目标的距离 ,距离越近优先级越高

             priority = heuristic(goal, next);

             frontier.put(next, priority);

             came_from[next] = current;

          }

       }

}



function heuristic(a, b){

    //离目标的距离

   return abs(a.x - b.x) + abs(a.y - b.y)

}

 

缺点:

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

如何在搜索尽可能少的顶点同时保证最短路径?咱们来看A*算法。

 六、A*算法

从上面算法的演进,咱们逐渐找到了最短路径和搜索顶点最少数量的两种方案,Dijkstra 算法和 贪婪最佳优先搜索。那么咱们有没有可能汲取两种算法的优点,令寻路搜索算法即使快速又高效?

答案是能够的,A*算法正是这么作了,它吸收了Dijkstra 算法中的cost_so_far,为每一个边长设置权值,不停的计算每一个顶点到起始顶点的距离,以得到最短路线,同时也汲取贪婪最佳优先搜索算法中不断向目标前进优点,并持续计算每一个顶点到目标顶点的距离,以引导搜索队列不断想目标逼近,从而搜索更少的顶点,保持寻路的高效。

解决痛点:

A*算法的优先队列排序方式基于F值:

F=cost(顶点到起始顶点的距离 )+heuristic(顶点到目标顶点的距离 )

 

咱们看下算法(js):

var frontier = new PriorityQueue();

frontier.put(start);

path = new Array();

cost_so_far = new Array();

path[start] = 0;

cost_so_far[start] = 0



while(frontier.length>0){

    current = frontier.get()



   if current == goal:

      break



       for(next in graph.neighbors(current)){

           var notInVisited = visited.indexOf(next)==-1;

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

           //没有访问过并且路径更近

        if(notInVisited ||  new_cost < cost_so_far[next]) {

              cost_so_far[next] = new_cost

              //队列优先级= new_cost(顶点到起始顶点的距离 )+heuristic(顶点到目标顶点的距离 )

            priority = new_cost + heuristic(goal, next)

            frontier.put(next, priority)

            path[next] = current

          }

       }

}



function heuristic(a, b){

    //离目标的距离

   return abs(a.x - b.x) + abs(a.y - b.y)

}

 

如下分别是Dijkstra算法,贪心算法,以及A*算法的寻路雷达图,其中格子有数字标识已经被搜索了,能够对比下三种效率:

 七、B*算法

B*算法是一种比A*算法更高效的算法, 适用于游戏中怪物的自动寻路,其效率远远超过A*算法,通过测试,效率是普通A*算法的几十上百倍。B*算法不想介绍了,本身去google下吧,

 

经过以上算法不断的演进,咱们能够看出每一种算法的局限,以及延伸出的新算法中出现的解决方式,但愿方便你的理解。

 

说明:

文章多出突破引用一下文章,代码也是。原文写的很棒,有兴趣,能够移步原文查看,或者google搜索:A*algorithm

引用文章:http://www.redblobgames.com/pathfinding/a-star/introduction.html

----------------------------------------end-----------------------------------

关注我的成长和游戏研发,致力推进国内游戏社区的成长与进步。

想了解更多有料的游戏技术,请关注个人公众号,原创以及独到。

相关文章
相关标签/搜索