服务端寻路实现总结

Posted leothzhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了服务端寻路实现总结相关的知识,希望对你有一定的参考价值。

 

整体思路:

  1. 通过客户端(Unity)导出地图信息
  2. 将地图信息放到服务端解析
  3. 服务端通过A*寻路算法计算路径点,然后服务端将这些路径点返回给客户端

说明:

  1. 此方案只在场景障碍物都是静态且不考虑高度的条件下有效
  2. 如果场景障碍物是动态且需要考虑高度的话,则服务端要非常强大的寻路库实现,这部分可以使用recastnavigation(https://github.com/recastnavigation/recastnavigation),同时这也是Unity内部源码使用的方案。

 

操作步骤:

Unity导出地图信息

  1. 设置游戏场景里物体障碍物属性为Navigation Static,并按照物体属性设置物体是否可行走(Walkable)和不可行走(Not Walkable),因为不考虑高度,Generate OffMeshLinks可不勾选。

    技术分享图片

    设置Object属性

  2. 当设置好所有障碍物信息后,执行地图网络烘焙(Bake)

    技术分享图片                        技术分享图片
    a)点击Bake烘焙出地图网络信息      b)烘焙后的网络信息图 

  3. 如果服务端是通过顶点和索引导入地图信息的,则通过UnityEngine.AI.NavMesh.CalculateTriangulation() 导出顶点和索引信息。
    如果服务端是通过二维坐标导入地图信息的,则通过UnityEngine.AI.NavMesh.SamplePosition()逐一顶点采样,如果hit中障碍物,则对应点设为1,否则设为0
    导出顶点和索引代码:
    技术分享图片
     1 using System.IO;
     2 using UnityEditor;
     3 using UnityEngine.SceneManagement;
     4 using UnityEngine;
     5 
     6 namespace AStar
     7 {
     8     //navmesh导出数据
     9     public class NavMeshExport : MonoBehaviour
    10     {
    11         [MenuItem("NavMesh/Export")]
    12         static void Export()
    13         {
    14             Debug.Log("NavMesh Export Start");
    15 
    16             UnityEngine.AI.NavMeshTriangulation navMeshTriangulation = UnityEngine.AI.NavMesh.CalculateTriangulation();
    17 
    18             //文件路径  
    19             string path = Application.dataPath /*+ "/AStar/obj/"*/ + SceneManager.GetActiveScene().name + ".obj";
    20 
    21             //新建文件
    22             StreamWriter streamWriter = new StreamWriter(path);
    23 
    24             //顶点  
    25             for (int i = 0; i < navMeshTriangulation.vertices.Length; i++)
    26             {
    27                 streamWriter.WriteLine("v " + navMeshTriangulation.vertices[i].x + " " + navMeshTriangulation.vertices[i].y + " " + navMeshTriangulation.vertices[i].z);
    28             }
    29 
    30             streamWriter.WriteLine("g pPlane1");
    31 
    32             //索引  
    33             for (int i = 0; i < navMeshTriangulation.indices.Length;)
    34             {
    35                 streamWriter.WriteLine("f " + (navMeshTriangulation.indices[i] + 1) + " " + (navMeshTriangulation.indices[i + 1] + 1) + " " + (navMeshTriangulation.indices[i + 2] + 1));
    36                 i = i + 3;
    37             }
    38 
    39             streamWriter.Flush();
    40             streamWriter.Close();
    41 
    42 
    43             AssetDatabase.Refresh();
    44 
    45             Debug.Log("NavMesh Export Success");
    46         }
    47     }
    48 }
    NavMeshExport
    技术分享图片

    导出后的obj图

    导出二维坐标代码:
    技术分享图片
      1 using UnityEngine;
      2 using System.Collections;
      3 using System.IO;
      4 using System.Text;
      5 using UnityEditor;
      6 using UnityEngine.SceneManagement;
      7 using UnityEngine;
      8 
      9 public class NavArrayExport: MonoBehaviour
     10 {
     11     #region Public Attributes
     12     public Vector3 leftUpStart = Vector3.zero;
     13     public float accuracy = 1;
     14     public int height = 30;
     15     public int wide = 30;
     16     public float maxdistance = 0.2f;
     17     public float kmin = -10;
     18     public float kmax = 20;
     19     #endregion
     20 
     21 
     22     #region Public Methods
     23 
     24     public void Exp()
     25     {
     26         exportPoint(leftUpStart, wide, height, accuracy);
     27     }
     28 
     29     public float ConvertToNearestHalfNum(float num)
     30     {
     31         float numFloor = Mathf.Floor(num);
     32         float half = 0.5f;
     33         float d1 = num - numFloor;
     34         float d2 = Mathf.Abs(num - (numFloor + half));
     35         float d3 = numFloor + 1 - num;
     36         float d = Mathf.Min(d1, d2, d3);
     37         if(d == d1)
     38         {
     39             return numFloor;
     40         }
     41         else if(d == d2)
     42         {
     43             return numFloor + 0.5f;
     44         }
     45         else
     46         {
     47             return numFloor + 1f;
     48         }
     49     }
     50 
     51     public void exportPoint(Vector3 startPos, int x, int y, float accuracy)
     52     {
     53         StringBuilder str = new StringBuilder();
     54         int[,] list = new int[x, y];
     55         str.Append("[MAP_DESC]
    ");
     56         str.Append("startpos=").Append(startPos).Append("
    ");
     57         str.Append("height=").Append(y).Append("
    wide=").Append(x).Append("
    accuracy=").Append(accuracy).Append("
    ");
     58         for (int j = 0; j < y; ++j)
     59         {
     60             for (int i = 0; i < x; ++i)
     61             {
     62                 UnityEngine.AI.NavMeshHit hit;
     63                 for (float k = kmin; k < kmax; k+=0.5f)
     64                 {
     65                     Vector3 p = startPos + new Vector3(i * accuracy, k, -j * accuracy);
     66                     if (UnityEngine.AI.NavMesh.SamplePosition(p, out hit, maxdistance, UnityEngine.AI.NavMesh.AllAreas))
     67                     {
     68                         //Debug.DrawRay(new Vector3(ConvertToNearestHalfNum(hit.position.x), hit.position.y, ConvertToNearestHalfNum(hit.position.z)), Vector3.up, Color.green);
     69                         list[(int)(ConvertToNearestHalfNum(hit.position.x) * 2), (int)(ConvertToNearestHalfNum(hit.position.z) * 2)] = 1;
     70                         break;
     71                     }
     72                 }
     73             }
     74         }
     75         str.Append("map=");
     76         for (int j = 0; j < y; ++j)
     77         {
     78             str.Append("[");
     79             for (int i = 0; i < x; ++i)
     80             {
     81                 str.Append(Mathf.Abs(1 - list[i, j])).Append(",");
     82             }
     83             str.Append("],");
     84         }
     85         //文件路径  
     86         string path = Application.dataPath + SceneManager.GetActiveScene().name + ".ini";
     87 
     88         //新建文件
     89         StreamWriter streamWriter = new StreamWriter(path);
     90         streamWriter.Write(str.ToString());
     91         streamWriter.Flush();
     92         streamWriter.Close();
     93 
     94 
     95         AssetDatabase.Refresh();
     96     }
     97     #endregion
     98 
     99 }
    100 
    101 [CustomEditor(typeof(NavArrayExport))]
    102 public class NavArrayExportHelper: Editor
    103 {
    104     public override void OnInspectorGUI()
    105     {
    106         base.OnInspectorGUI();
    107         if (GUILayout.Button("Export"))
    108         {
    109             var exp = target as NavExport;
    110             exp.Exp();
    111         }
    112     }
    113 }
    NavArrayExport

    技术分享图片导出的二维坐标表

服务端实现:

解决了客户端导出的地图信息后,服务端只需要通过获取客户端发送过来的start_pos和end_pos,并通过这两个点执行寻路逻辑,然后得到一串路径点,然后将这些路径点返回给客户端,客户端通过路径点执行对应人物的方向行走即可。不过要注意的是,这里客户端之所以是通过方向行走而不是直接走到那个路径点是因为我们是忽略了高度的因素)

  1. 服务端load地图信息,具体代码见navmesh的load()load_map_array()
  2. 服务端寻路算法实现:
        如果是通过顶点索引方案的方式寻路,则记录所有三角形的重心,将重心抽象成地图上的网络点,然后计算寻路时,以这些网络点当成作用对象来进行寻路,具体代码见navmesh的calulate_path()
       如果是通过数组的方式,那就更容易了,直接走传统的A*即可,具体代码见navmesh的calulate_path_array()
    navmesh代码如下:
    技术分享图片
      1 #!/bin/env python
      2 # -*- coding:utf8 -*-
      3 import os
      4 import ConfigParser
      5 import numpy
      6 from heapq import *
      7 from vector3 import Vector3
      8 from triangle import Triangle
      9 from math_tools import MathTools
     10 from common.singleton import Singleton
     11 from common.logger import Logger
     12 class Navmesh(Singleton):
     13     ‘‘‘
     14     服务端寻路组件
     15     ‘‘‘
     16     def __init__(self):    
     17         self.vertices = []
     18         self.indices = []
     19         self.all_triangles = [] # 所有三角形
     20         self.all_center_o = [] # 所有三角形的重心
     21         self.point_2_triangles = {} # 点包含的三角形列表集合
     22 
     23         # obj_file
     24         dir = os.path.dirname(os.path.realpath(__file__))
     25         #self.map_obj_file_name = ‘{}/{}‘.format(dir,‘test.obj‘)
     26         self.map_obj_file_name = {}/{}.format(dir,AssetsDeserScene.obj)
     27 
     28     
     29     def load(self):
     30         with open(self.map_obj_file_name, r+) as f:
     31             Logger().I(loading map data({})....format(self.map_obj_file_name))
     32             lines = f.readlines()
     33             for line in lines:
     34                 for kv in [line.strip().split( )]:
     35                     if kv[0] == v:
     36                         self.vertices.append(Vector3(float(kv[1]),float(kv[2]),float(kv[3])))
     37                     elif kv[0] == f:
     38                         a = int(kv[1])-1
     39                         b = int(kv[2])-1
     40                         c = int(kv[3])-1
     41                         t = Triangle(self.vertices[a], self.vertices[b], self.vertices[c])
     42                         self.indices.append(Vector3(a,b,c))
     43                         self.all_triangles.append(t)
     44                         self.all_center_o.append(t.o)
     45 
     46                         if t.a not in self.point_2_triangles:
     47                             self.point_2_triangles[t.a] = [t]
     48                         elif t not in self.point_2_triangles[t.a]:
     49                             self.point_2_triangles[t.a].append(t)
     50 
     51                         if t.b not in self.point_2_triangles:
     52                             self.point_2_triangles[t.b] = [t]
     53                         elif t not in self.point_2_triangles[t.b]:
     54                             self.point_2_triangles[t.b].append(t)
     55 
     56                         if t.c not in self.point_2_triangles:
     57                             self.point_2_triangles[t.c] = [t]
     58                         elif t not in self.point_2_triangles[t.c]:
     59                             self.point_2_triangles[t.c].append(t)
     60         
     61         return True
     62 
     63     def load_map_array(self):
     64         conf = ConfigParser.ConfigParser()
     65         conf.read(map/AssetsDeserScene.ini)
     66         map_data = conf.get(MAP_DESC,map)
     67         self.h = conf.getint(MAP_DESC,height)
     68         self.w = conf.getint(MAP_DESC,wide)
     69         self.accuracy = conf.getfloat(MAP_DESC,accuracy)
     70         self.nmap = numpy.array(eval(map_data))
     71 
     72     def convert_pos_to_arr_idx(self, v_pos):
     73         #(0,0,700)->(0,0)
     74         #(100,0,700)->(0,199)
     75         #(100,0,600)->(199,199)
     76         x = (self.h - MathTools.convert_to_half_num(v_pos.z)) * self.accuracy - 1
     77         z = MathTools.convert_to_half_num(v_pos.x) * self.accuracy - 1
     78         return (int(x), int(z))
     79     
     80     def convert_arr_idx_to_pos(self, idx):
     81         return Vector3((idx[1] + 1) / self.accuracy, 0, self.h - (idx[0] + 1)/self.accuracy)
     82 
     83     def calulate_path_array(self, start_pos, end_pos):
     84         ‘‘‘
     85         通过二维数组的地图数据寻路
     86         @paramIn: [Vector3] start_pos
     87         @paramIn: [Vector3] end_pos
     88         ‘‘‘
     89         start = self.convert_pos_to_arr_idx(start_pos)
     90         end = self.convert_pos_to_arr_idx(end_pos)
     91         res = MathTools.astar(self.nmap, start, end)
     92         
     93         paths = []        
     94 
     95         if res is False or len(res) == 0:
     96         # 找不到就直接走向end
     97             paths.append(end_pos)
     98             return paths
     99         
    100         # 将点路径转化为线段路径
    101         res.reverse()
    102         paths.append(self.convert_arr_idx_to_pos(res[0]))
    103         if len(res) > 1:
    104             for i in xrange(1,len(res)):
    105                 if MathTools.check_points_in_line(start,res[i-1],res[i]):
    106                     paths[-1] = self.convert_arr_idx_to_pos(res[i])
    107                 else:
    108                     # 斜率开始不一样,则添加最新的点
    109                     paths.append(self.convert_arr_idx_to_pos(res[i]))
    110                     start = res[i-1]
    111         return paths
    112         
    113         
    114         
    115 
    116     def calulate_path(self, start_pos, end_pos):
    117         open_list = []
    118         close_list = []
    119         start_pos_triangle = Triangle(start_pos, start_pos, start_pos)
    120         end_pos_triangle = Triangle(end_pos, end_pos, end_pos)
    121 
    122         start_triangle, start_pos = MathTools.fix_border_point_if_needed(start_pos, self.all_triangles)
    123         end_triangle, start_pos = MathTools.fix_border_point_if_needed(end_pos, self.all_triangles)
    124         rv_path = {}
    125         if start_triangle is None:
    126             #Logger().W(‘start_pos is not in the navmesh‘)
    127             return None
    128         if end_triangle is None:
    129             #Logger().W(‘end_pos is not in the navmesh‘)
    130             return None
    131         
    132         f = {}
    133         g = {}
    134         h = {}
    135 
    136         open_list.append(start_pos_triangle)
    137 
    138         while end_pos_triangle not in open_list and len(open_list) != 0:
    139             # step 1. get the min triangle in open_list
    140             t = open_list[0]
    141             for i in xrange(0, len(open_list)):
    142                 if open_list[i] not in f:
    143                     f[open_list[i]] = 0
    144                 if f[t] > f[open_list[i]]:
    145                     t = open_list[i]
    146             
    147             # step 2. remove the triangle from the open_list
    148             open_list.remove(t)
    149 
    150             # step 3. append the triangle to the close_list
    151             close_list.append(t)
    152 
    153             # step 4. explore
    154 
    155             # step 4.1. get round triangles
    156             current = t
    157             if current == start_pos_triangle:
    158                 current = start_triangle
    159             round_triangles = []
    160 
    161             if current.a in self.point_2_triangles:
    162                 round_triangles = round_triangles + self.point_2_triangles[current.a]
    163             if current.b in self.point_2_triangles:
    164                 round_triangles = round_triangles + self.point_2_triangles[current.b]
    165             if current.c in self.point_2_triangles:
    166                 round_triangles = round_triangles + self.point_2_triangles[current.c]
    167             
    168             # remove dulplicate triangles
    169             round_triangles = sorted(set(round_triangles),key=round_triangles.index) 
    170             if end_triangle in round_triangles:
    171                 round_triangles.append(end_pos_triangle)
    172 
    173             # step 4.2
    174             for i in xrange(0, len(round_triangles)):
    175                 round_triangle = round_triangles[i]
    176 
    177                 if round_triangle is t:
    178                     continue
    179                 
    180                 if round_triangle in close_list:
    181                     continue
    182                 
    183                 if round_triangle not in open_list:
    184                     rv_path[round_triangle] = t
    185                     if t not in g:
    186                         g[t] = 0
    187                     g[round_triangle] = g[t] + MathTools.distance(t.o,round_triangle.o)
    188                     h[round_triangle] = MathTools.distance(round_triangle.o, end_pos)
    189                     open_list.append(round_triangle)
    190                 else:
    191                     if round_triangle not in g:
    192                         g[round_triangle] = 0
    193                     if round_triangle not in h:
    194                         h[round_triangle] = 0
    195                     G = g[round_triangle]
    196                     H = h[round_triangle]
    197                     F = g[round_triangle] + MathTools.distance(round_triangle.o, t.o) + MathTools.distance(round_triangle.o, end_pos)
    198                     if G + H > F:
    199                         rv_path[round_triangle] = t
    200                         g[round_triangle] = g[t] + MathTools.distance(t.o,round_triangle.o)
    201                         h[round_triangle] = MathTools.distance(round_triangle.o, end_pos)
    202                         open_list.remove(round_triangle)
    203                     
    204         path_list = []
    205         t = end_pos_triangle
    206         while t in rv_path and rv_path[t] is not None:
    207             path_list.append(t.o)
    208             t = rv_path[t]
    209 
    210         path_list.reverse()
    211         
    212         return path_list
    213 
    214     
    215 # start_pos =Vector3(315.976135254,11.0661716461,360.217041016)
    216 # end_pos = Vector3(315.970001221,11.86622715,350.790008545)
    217 # nav = Navmesh()     
    218 # nav.load_map_array()
    219 # print nav.calulate_path_array(start_pos,end_pos)
    220 
    221 # start_pos = Vector3(-2.3, 0.3, -2.8)
    222 # end_pos = Vector3(3.2, 0.0, -2.8)
    223 # print nav.calulate_path(start_pos, end_pos)
    224 
    225 # (-0.3, 0.1, -3.9)
    226 # (1.7, 0.1, -4.2)
    227 # (3.2, 0.1, -2.8)
    navmesh
  3. 路径点返回给客户端

感受与体会:

  1. 当三角形非常大的时候,如下图,使用上述代码的calculate_path()方法会导致人物会出现“绕路”的情况,这是因为三角形太大导致重心已失去位置的“代表性”。如果想要使用三角网络寻路的同时并且解决这个问题,建议用recastnavigation库实现,因为这里的核心原理牵涉到很多复杂的图论问题

    技术分享图片

    三角网络很大的情况

  2. 使用二维数组方案最高效且简单
  3. 当地图比较大时,需要对地图进行分块划分,以节省服务端内存压力,以及省去不必要的寻路计算















以上是关于服务端寻路实现总结的主要内容,如果未能解决你的问题,请参考以下文章

数商云:日化用品行业采购智能管理平台整合供应闭环,实现企业端到端寻源采购

关于自动寻路(Navigation)的初级总结

大规模单位实时寻路和AI的构建

客户端地图内寻路总结与优化

在服务器端如何用JNI实现 NavMesh寻路

在服务器端如何用JNI实现 NavMesh寻路