当前位置:   article > 正文

unity 游戏中的寻路与导航系统(5种寻路算法详解)_unity 寻路算法

unity 寻路算法

@了解游戏中常见的寻路方式

通常来讲一般是根据建模方式来决定寻路方式

常见的寻路方式——建模方式

这里提供一下三种建模方式的示意图,如下 ,分别对应着,原图、Grid、WayPoint、NavMesh
在这里插入图片描述

寻路建模

从三个角度来分别比较一下三种建模
1.实现复杂度:NavMesh > Gird > WayPoint;
2.内催和计算机开销: Gird >NavMesh > WayPoint;
3.表达精确性: NavMesh > Gird > WayPoint;

Grid(格子):实际上是通过把原始地图分割称为格子,在计算机中标识唯一个二维数组,用0和1分别表示障碍和可行走区域格子的密度在一定程度上代表了寻路的精度。

1. 优点 :
	1. 容易实现
	2. 容易动态修改
		1. 如果需要动态加入障碍,只需要确定障碍物会张勇那些格子,然后重新寻路即可
3. 缺点: 
	1. 内存和计算机开销大
		1. 越大的地图,所分割出来的格子就越多,那么响应的内存占用和计算机开销就会变得很大。
		2. 分割格子大小的度量也不容易确定,分的大了,会缩小可以行走的区域,如果定的小了,又会增加内存与计算机的开销)
	2. 可能需要做额外的平滑
		1. 由于地图都是通过格子分割出来的,那么在寻路的时候,角色所能行走的路线中,同样也会出现90度的拐角,甚至可能有连续的多个拐角,这样的话会导致这种游戏寻路变得很僵硬,所以需要做额外的平滑。
	3. 并不是很适合3D地图
		1.  格子寻路最早是针对与2D地图寻路提出来的,并没有涉及到高度的问题,可以设想一下,在3D地图中,地表面是起起伏伏的,比如说可能回有城墙,如果使用格子寻路的话,他可能会忽略这个高度,而直接将城墙所在的位置标识为可以到达的区域,因此,格子寻路并不适合3D地图   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

WayPoint (这种方法会在地图中标注一些路点,寻路只能发生在路点和路点之间,在计算机中表示为一张连通图。)点的密度可以代表寻路精度

1. 优点
	1. 实现简单
	2. 内存和计算机开销低
		1. 路点寻路只是使用到了真是可行走区域的一小部分,与格子相比,牺牲了路线的灵活性,换取交第的内存和计算机开销,可以观察一下上图(c),只要两个黑点之间是连通的,那么角色就可以按照黑点所在的位置到达目标黑点的位置。
2. 缺点
	1. 需要人工参与 (因为需要人工标注路店位置和可行走的路线)
	2. 局限性比较大
		1. 通过以上的解释,可以看出来,路点寻路实际上是非常僵硬的,他必须按照游戏设定初期,人为规定好的路线去行走,这样的话,有可能会给玩家这样的体验,明明可以直接到达的地方,通过寻路只能绕来绕去,花费了很多不必要的时间,当然,如果对于那些通过上线时间来获取利润或者其他奖励的游戏,无可厚非(但只是特例)
		2. 对于起始点不在路点图中的情况,还必须要找到最近的路点,然后再移动到角色想要到达的目的路点
		3. 路点并不考虑实际的底层地图,比如说,我在游戏中有一个坐标是,角色只能通过条约才能过去的路障,那么就有可能出现这样的情况,角色在移动过程中,可能会被某些阻碍物阻挡而卡住的情况;     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

NavMesh ( 这种方法使用一系列算法将原始地图转换成三角形网格的集合,网格和网格之间构成连通关系用于寻路。在计算机中表示为顶点的集合,以及三个顶点索引一组(边)的集合。)

1.  优点
	1. 精确表达世界 
		1. 虽然导航网格无法做到还原全部地图,但是他实际上比上述说的两种导航更为精确。
		2. 这种导航建模引入了高度星系,这样的话就可以很方便的表示3D地图
	2. 内存和计算机开销小 
		1. 导航网格在形成的时候,会将地图抽象成三角网格,也就是顶点和边的模型,消耗的性能要比较小。
2. 缺点
	1. 复杂、难以实现和修改,自己从头开始创建一个导航网格系统的确是特别复杂的,需要很多集合算法的知识,**unity可以自动烘培一个网格系统,所以对unity开发人员来说,,这点不算什么(当然如果人工制作这个网格系统的话,就很麻烦了)**
	在很大的地图中的寻路,一般可以用Waypoint 与其他的寻路方式结合使用
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

总结

1.gird 一般适合2D地图
2.Waypoint一般是与其他寻路方式结合,可以用来做2D地图,也可以3D地图,比如说,在游戏中寻路从我到北京的路径,我当前所在位置距离苏州很近,那么就可以先用Navmesh寻路我到苏州的最短路径,然后再使用waypoint 提前设定一条苏州到北京的路径,也是比较好的方案
3.Navmesh 一般用作3D地图
总结,三种寻路方式的优缺点在上文已经讲述了,其实寻路方案没有最好,只要适合你就行了,读者可以根据自己的情况进行选择

常见的寻路方式——算法*(以下的算法一般都使用在gird上)

1.数据结构
1.树
2.图
2.算法
1.深度优先
2.广度优先
3.dijkstra
4.A* 2D格子 结合深度优先或者广度优先
5.B*

接下来具体的解释一下这几种算法,先来看两张图,分别是正常的游戏地图与开发人员眼中的地图
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201105153020970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xmYW55aXpl,size_16,color_FFFFFF,t_70#pic_center)
  • 1
  • 2

在这里插入图片描述
对于一张地图,开发人员需要通过一定的方案将其转换称为数据对象,最常见的就是将地图切割成为网格,或者切割称为多边形,这取决于你的游戏地图,一般情况下,同等面积的地图中采用更少的顶点,寻路算法会更快,而寻路算法中最常见的数据结构就是图,我们先来了解一下图

什么是图?

图的正则表达式是G=(V,E)其代表了顶点的集合,EheV是一种二元关系,可以简单的理解为边,比如说有条边是从顶点A到顶点D结束,那么就可以用(A,D)来表示,图的大致分类有两种,有向图和无向图,如下
无向图
在这里插入图片描述
V(G)= {V1,V2,V3,V4,V5,V6}

E(G)= {(V1,V2),(V1,V3),(V2,V6),(V2,V5),(V2,V4),(V4,V3),(V3,V5),(V5,V6)}
有向图
在这里插入图片描述
V(G)= {V1,V2,V3,V4,V5,V6}

E(G)= {<V2,V1>,<V3,V1>,<V4,V3>,<V4,V2>,<V3,V5>,<V5,V3>,<V2,V5>,<V6,V5>,<V2,V6>,<V6,V2>}
简单的了解了一下数据结构之后,我们来看一下算法
在进行寻路系统的制作的时候,选择一个好的算法尤其重要,他不单单要考虑复杂度,还要考虑内存与计算机的开销等等因素。

寻路算法

1.深度优先搜索(邻接矩阵实现)
深度优先(DFS)是一种遍历树或者图的算法,假设给定图G的初态是所有顶点均未曾访问过。在G中任选一顶点v为初始出发点(源点),则深度优先遍历可定义如下:首先访问出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止。若此时图中仍有未访问的顶点(未连通),则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。
递归实现的伪代码

1)访问顶点v;visited[v]=1//算法执行前visited[n]=02)w=顶点v的第一个邻接点;
(3while(w存在)  
           if(w未被访问)
                   从顶点w出发递归执行该算法; 
           w=顶点v的下一个邻接点;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

非递归实现的伪代码

1)栈S初始化;visited[n]=0;
(2)访问顶点v;visited[v]=1;顶点v入栈S
(3while(栈S非空)
            x=栈S的顶元素(不出栈)if(存在并找到未被访问的x的邻接点w)
                    访问w;visited[w]=1;
                    w进栈;
            else
                     x出栈;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但是由于这个算法计算的并不是最短路径,所以在寻路算法中基本不用

广度优先遍历

广度优先遍历类似于树的层序遍历
从图中某顶点触出发进行广度优先遍历的基本思想是
1.先访问顶点v
2.依次访问v的各个未被访问的邻接点
3.分别从上一步中的邻接点中依次出发访问他们未被访问的邻接点,具体如下图
在这里插入图片描述
首先访问V0,然后将V0入队,然后V0取出,一次访问他的邻接点V1,V2,V5,并将这三个邻接点依次入队,将V1取出,依次访问V1的所有未被访问的邻接点V4,然后V4入队,重复上述过程,访问完所有图中的顶点;
从上述的解释中可以看出,BFS(广度优先遍历)是一种盲目搜寻的办法,她并不考虑结果的可能位置,而是彻底的搜索整张地图,一直到他找到结果为止

伪代码
1.初始化队列Q
2.访问顶点vvisited[v] = 1;顶点v入队列
3.while(队列非空)
	3.1 v=队列q的对头元素出队;
	3.2 W = 顶点V的第一个邻接点;
	3.3 While(存在)
		3.3.1 如果W未被访问到,则访问顶点W visited[W] = 1;(表示W点已经被访问过) 顶点W入队
		3.3.2 W = 顶点V的下一个邻接点; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从上述的解释中可以看出,他的效率很低,时间复杂度是T(n) = O(n2),顶点与顶点之间没有权重,没有办法定义优先级,所以不能找到最优路线,比如说,如果在地图中遇到高山,那么算法应该是绕过去而不是遍历。
在这里插入图片描述

Dijkstra算法

从上述的广度优先遍历来看,他并不会避免那些角色不能到达的位置,所以我们解决的痛点就是怎样才能在遍历的时候避免这样的情况,这也就是Dijkstra算法所解决的事情——为每一条边添加一个权重,重复的更新最短路径
Dijkstra 算法用来求出单元点最短路径问题,他是由迪杰斯特拉踢出的一种按照路径长度递增的次序产生最颠路径的算法,其基本思想是,设置一个集合S存放已经找到的最短路径的顶点,S的初始状态只包含远点V对Vi属于V-S,假设从远点V到Vi的有向边为最短路径,以后没球德一条最短路径V。。。Vk,就将Vk加入集合S中,并将路径V。。。Vi与原来的假设相比较,取路径长度较小者为当前最短路径,重复上述过程,一直到集合V中所有顶点加入到集合S中。
在这里插入图片描述
下面来说一下DIjkstra算法基于的储存结构
1.图的储存结构,因为在算法执行过程中,需要快速的球的任意两个顶点之间边上的权值,所以采取邻接矩阵储存
2.辅助数组dist[n];元素dist[i]表示当前所找到的从原点v到终点vi的最短路已经长度,初态:若从v到vi由弧,则dist[i]表示为弧上的权值;否则则置dist[i]为无穷大,若当前求得的终点为vk,则根据下式进行迭代:dist[i] = min {dist[i],dist[k]+arc[k][i] 0<=i<=n-1
3.辅助数组path[n],元素path[n]是一个串,表示当前所找到的从原点到终点的最丢按路径
4.数组s[n] ;存放远点和已经生成的终点

伪代码
	1.初始化数组dist,path,s
	2.while(s中的元素个数<n)
		2.1 在dist[n]中求最小值,其编号为k;
		2.2 输出dist[k]和path[k];
		2.3 修改数组dist 和path
		2.4将顶点k添加到数组s中
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述
在这里插入图片描述
介绍完Dijkstra算法,有没有感觉其实他还是有缺陷的?如果图比较大的话,我们要找到图中两个顶点的最短距离的话,同样要遍历全部地图,这也是Dijkstra最重要的痛点,那么现在问题是,怎样才能让不使其遍历整张地图而找到路径呢?
那我们不妨来更改一下思路,Dijkstra算法笼统的来说是从顶点出发,分别检测一下顶点的邻接点到出发点的权值,保存最小的,这样的话会遍历地图中所有的顶点,如果换一种思路的话,从顶点触发,分别检测顶点的邻接点到目标点的距离这也就是贪心算法
算法如下

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

效果
在这里插入图片描述
缺点:虽然说寻找的顶点少了,但是路径只能说是较短,而不是最短,那么怎样才能在搜索经量少的顶点的同时保证最短路径?

A*算法

通过上面算法的进步,我们现在已经有了两种解决方案,1.找出最短路径(Dijkstra)2.搜索尽量少的顶点(贪婪最佳优先),是不是只要将这两种优势结合起来就可以了呢?
A算法做到了
A
算法的优先队列排序方式基于F值:
F=cost(顶点到起始顶点的距离 )+heuristic(顶点到目标顶点的距离 )
看一下算法

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)
 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

实现效果比较
在这里插入图片描述

B*算法

B算法要比A算法的效率搞很多很多,下面来说一下B*是怎样实现的
首先来定义几个节点
1.探索节点:在寻路过程中向前探索的节点(地图格子)称为探索节点,起始探索节点即为原点。
2.自由探索节点:探索节点朝着目标前进,如果前方不是阻挡,探索节点可以继续向前进入下一个地图格子,这种探索节点我们称为自由探索节点;
3.绕爬的探索节点:探索节点朝着目标前进,如果前方是阻挡,探索节点将试图绕过阻挡,绕行中的探索节点我们成为绕爬的探索节点;

算法过程

1.起始,探索节点为自由节点,从原点出发,向目标前进;
2.自由节点前进过程中判断前面是否为障碍,
1.不是障碍,向目标前进一步,仍为自由节点;
2.是障碍,以前方障碍为界,分出左右两个分支,分别试图绕过障碍,这两个分支节点即成为两个绕爬的探索节点;
3.绕爬的探索节点绕过障碍后,又成为自由节点,回到2);
4.探索节点前进后,判断当前地图格子是否为目标格子,如果是则寻路成功,根据寻路过程构造完整路径;
5.寻路过程中,如果探索节点没有了,则寻路结束,表明没有目标格子不可达;

演示

起始,此时为自由节点,从原点触发,向目标前进
遇到障碍,分出上下两条路,试图绕过去
衍生出来的两个节点绕过障碍后,变为自由节点,继续朝着目标前进。此时又遇到障碍。
在这里插入图片描述
到达目标位置

寻路方案选择

1.客户端寻路

什么时候使用客户端寻路呢?
	比如说,你对寻路方案的精度要求非常非常的高,但是,又对数据校验需求并不是很高,这种情况下,单纯的客户端校验就够了。单纯的客户端寻路比较容易遇到外挂,如果在服务端并没有做校验,那么你在客户端寻出一条路,交给服务器,服务器直接会广播给其他人,这样如果遇到外挂(穿墙外挂等等)的话,影响玩家游戏体验,
  • 1
  • 2

2,服务端寻路

如果说有大量的AI跑在服务端,他需要有非常复杂的路径去寻找,比如要穿越丛林,沙漠等,可能就要做服务端寻路,其有自己独特的好处就是,他在实现同步方面比较有优势,服务端寻出一条路,然后直接广播给所有人就好了,这样的话,所有人所能看到的寻路结果是一致的。 
  • 1

3.客户端寻路、服务器校验

那么怎样在服务器上面做校验呢?其实方法有很多,
	比如说格子,那直接把你寻路出来的结果发送给服务器,服务器重新跑一遍看看符不符合就可以了,但是这样其实就没有必要了,我直接服务端做不就行了吗。
	如果说,你的地图像是魔兽世界这样的,又有天空又有沙漠,山洞等等,这样的地图,你可能要考虑使用navmesh寻路,那么问题又来了 ,那服务端要做navmesh寻路的话,的确可以做,如果你要是使用unity做的navmesh寻路的话,unitty生成的数据,在服务端是很难直接去使用的,即便能去使用,你的服务端有没有客户端这个导航的算法,所以你要在服务端重新自己写一套navmesh 寻路算法,这样做,复杂度很高,成本也很高,优点得不偿失的感觉,所以如果客户端不得不用navmesh的话,那么就可以将navmesh做一个转变,只要服务端能够判断出来,当前的角色是否在作弊就够了,比如你可以判断一下移动速度,正常的寻路大致需要多久,或者一定会经过那个障碍,然后根据角色当前位置,障碍位置,目的位置的距离,计算出一个大致的时间,如果角色所用时间大大小于我计算出来的时间,那么他一定不正常;
	另外一层可以验证位移,因为在客户端方面,每隔一个很小的位移,都会同步一下位置,如果说使用了外挂,那么他在行走的过程中,一定有一瞬间是在某个障碍上的,如果在服务端,我能够知道,这个障碍是否可达,也可以做校验,那么服务器只要是能有这样一组数据,能够判断出来,某些位置是否是可站立的位置就可以了,也就是说,在客户端生成一张障碍图,发送给服务器就可以了,服务器只需要判断你当前是否在障碍区就可以做出响应的反应了。
	实际上,如果这个校验是否产生灾难性的后果,不坐校验实际上也是可以的
  • 1
  • 2
  • 3
  • 4
  • 5

NavMesh的创建

window->AI-Navmesh ,然后对场景进行烘焙即可,当然还有很多知识点,比如需要设置一下是否可走,跳跃等标签,需要设置一下半径等,网上很多,这里就不再说了。

NavAgent

为赋予寻路功能的角色动态添加navAgent组件,然后通过设置路径、设置目的地、判断是否停下等等接口来控制角色型为

lineRender组件

当寻路路径确定以后,可以将寻路的各个坐标传进去,然后这个组件可以根据传进来的坐标(NavMeshPath有一个属性是corners,这个是寻路出来的路径坐标信息,同时还有有坐标个数多少等信息),与你提供的路径照片,显示出一种指向目标的可视化路径

跨地图寻路

举例,野外地图有传送门,只要查找一下,传送门的传送点的配置表,找到两张地图之间是由那个传送点连接起来的,那样就可以获取到传送点的位置,走到传送点的是偶自动转送,等到了另一张地图的时候,重新设置一下寻路的目的地即可。

服务器如何校验

在客户端通过一定的技巧,将角色能过够到达的位置转化成为一个个的三维数据传给服务器,同时当服务器收到这样的一组数据之后,可以通过键值关联,如果当前这个坐标能够到达,索引为1,否则为0,在角色完成寻路的时候,可以将寻路所生成的这样一组数据发送给服务器,服务器进行判断,如果角色传过来的这样一组寻路坐标中的索引并不是1,那么代表这条寻路是有微体的(可能是外挂),这样服务器就可以做响应的反应(令角色掉线或者纠正角色位置 )

参考文章:
寻路建模的三种方式

深入理解游戏中寻路算法

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/95258
推荐阅读
相关标签
  

闽ICP备14008679号