Unity NavMesh 动态烘焙绘制与随机取点

最初的Unity导航系统很不完善,只能静态烘焙场景图的可行走区域,而且必须在本地保存场景的NavMesh数据,难以运行时动态计算;这使得鲜有开发者愿意再尝试Unity内置的导航功能,转向了AStar寻路算法的研究。

但实际上AStar算法真的适合大多数开发情况且性能较优么?

了解过AStar算法的都知道,它是基于格子来遍历计算行走权重的,算法复杂度其实是相对较高的,受到格子密度,地图大小和路线长度的的影响较大。

AStar更适合的是策略性寻路,该算法更有利于找出最短路径的最优解,能够达到足够的精确性。

而Unity的NavMesh是用的拐角点算法,随便找一个场景烘焙一下便可得知,例如:

 

烘焙出来的NavMesh区域只在障碍物边缘与平面边缘存在顶点,而不会像AStar一样均匀的布满整个平面;如果是一个无任何障碍物的平面,那就只会有平面边缘的几个顶点,算法效率是相对较高的,并不会因为地图变大而有明显算法复杂度上的变化。

相反,NavMesh的缺点也正是AStar的优点,那就是难以保证寻路的最优解,更多的时候是用于AI能够更快计算出绕过障碍物朝向目标前进的路径。

 

对于场景不变的静态地图来说,Unity最初的NavMesh已经能够满足需求,但如果地图随机生成或障碍物的位置随时变化,此时静态NavMesh一下子就捉襟见肘了。

好在随着Unity版本的更新,关于动态烘焙的方法也已经能有效实现,这样无论是以怎样千变万化的方式生成的随机地图,随机地图在游戏中如何构建重组,都能动态刷新出NavMesh的可行走区域。

 1 using UnityEngine;
 2 using UnityEngine.AI;
 3 using System.Collections.Generic;
 4 
 5 // Tagging component for use with the LocalNavMeshBuilder
 6 // Supports mesh-filter and terrain - can be extended to physics and/or primitives
 7 [DefaultExecutionOrder(-200)]
 8 public class NavMeshSourceTag : MonoBehaviour
 9 {
10     // Global containers for all active mesh/terrain tags
11     public static List<MeshFilter> m_Meshes = new List<MeshFilter>();
12     public static List<Terrain> m_Terrains = new List<Terrain>();
13 
14     void OnEnable()
15     {
16         var m = GetComponent<MeshFilter>();
17         if (m != null)
18         {
19             m_Meshes.Add(m);
20         }
21 
22         var t = GetComponent<Terrain>();
23         if (t != null)
24         {
25             m_Terrains.Add(t);
26         }
27     }
28 
29     void OnDisable()
30     {
31         var m = GetComponent<MeshFilter>();
32         if (m != null)
33         {
34             m_Meshes.Remove(m);
35         }
36 
37         var t = GetComponent<Terrain>();
38         if (t != null)
39         {
40             m_Terrains.Remove(t);
41         }
42     }
43 
44     // Collect all the navmesh build sources for enabled objects tagged by this component
45     public static void Collect(ref List<NavMeshBuildSource> sources)
46     {
47         sources.Clear();
48 
49         for (var i = 0; i < m_Meshes.Count; ++i)
50         {
51             var mf = m_Meshes[i];
52             if (mf == null) continue;
53 
54             var m = mf.sharedMesh;
55             if (m == null) continue;
56 
57             var s = new NavMeshBuildSource();
58             s.shape = NavMeshBuildSourceShape.Mesh;
59             s.sourceObject = m;
60             s.transform = mf.transform.localToWorldMatrix;
61             s.area = 0;
62             sources.Add(s);
63         }
64 
65         for (var i = 0; i < m_Terrains.Count; ++i)
66         {
67             var t = m_Terrains[i];
68             if (t == null) continue;
69 
70             var s = new NavMeshBuildSource();
71             s.shape = NavMeshBuildSourceShape.Terrain;
72             s.sourceObject = t.terrainData;
73             // Terrain system only supports translation - so we pass translation only to back-end
74             s.transform = Matrix4x4.TRS(t.transform.position, Quaternion.identity, Vector3.one);
75             s.area = 0;
76             sources.Add(s);
77         }
78     }
79 }
NavMeshSourceTag类是为了收集需要录入烘焙列表的模型网格数据和地形数据,用的是一个全局的静态数据列表来存储,需要挂载在场景的网格物件上,标记哪些物件的网格在生成数据时需要考虑在内。
当然了,如果一个物体是由多个网格拼接而成,读者只需要将OnEnable和OnDisable中的代码稍作修改,改为读取子物体中的所以MeshFilter和Terrain组件即可:
1         foreach (var m in GetComponentsInChildren<MeshFilter>())
2         {
3             if (m != null)
4             {
5                 m_Meshes.Add(m);
6             }
7         }

将之前收集到的网格物件的源数据动态烘焙刷新生成NavMesh:

  1 using UnityEngine;
  2 using UnityEngine.AI;
  3 using System.Collections;
  4 using System.Collections.Generic;
  5 using NavMeshBuilder = UnityEngine.AI.NavMeshBuilder;
  6 
  7 // Build and update a localized navmesh from the sources marked by NavMeshSourceTag
  8 [DefaultExecutionOrder(-102)]
  9 public class LocalNavMeshBuilder : MonoBehaviour
 10 {
 11     // The center of the build
 12     public Transform m_Tracked;
 13 
 14     // The size of the build bounds
 15     public Vector3 m_Size = new Vector3(80.0f, 20.0f, 80.0f);
 16 
 17     NavMeshData m_NavMesh;
 18     AsyncOperation m_Operation;
 19     NavMeshDataInstance m_Instance;
 20     List<NavMeshBuildSource> m_Sources = new List<NavMeshBuildSource>();
 21 
 22     IEnumerator Start()
 23     {
 24         while (true)
 25         {
 26             UpdateNavMesh(true);
 27             yield return m_Operation;
 28         }
 29     }
 30 
 31     void OnEnable()
 32     {
 33         Bake();
 34     }
 35 
 36     void OnDisable()
 37     {
 38         //Unload navmesh and clear handle
 39         m_Instance.Remove();
 40     }
 41 
 42     /// <summary>
 43     /// 按范围动态更新NavMesh
 44     /// </summary>
 45     /// <param name="asyncUpdate">是否异步加载</param>
 46     void UpdateNavMesh(bool asyncUpdate = false)
 47     {
 48         NavMeshSourceTag.Collect(ref m_Sources);
 49         var defaultBuildSettings = NavMesh.GetSettingsByID(0);
 50         var bounds = QuantizedBounds();
 51 
 52         if (asyncUpdate)
 53             m_Operation = NavMeshBuilder.UpdateNavMeshDataAsync(m_NavMesh, defaultBuildSettings, m_Sources, bounds);
 54         else
 55             NavMeshBuilder.UpdateNavMeshData(m_NavMesh, defaultBuildSettings, m_Sources, bounds);
 56     }
 57 
 58     static Vector3 Quantize(Vector3 v, Vector3 quant)
 59     {
 60         float x = quant.x * Mathf.Floor(v.x / quant.x);
 61         float y = quant.y * Mathf.Floor(v.y / quant.y);
 62         float z = quant.z * Mathf.Floor(v.z / quant.z);
 63         return new Vector3(x, y, z);
 64     }
 65 
 66     Bounds QuantizedBounds()
 67     {
 68         // Quantize the bounds to update only when theres a 0.1% change in size
 69         var center = m_Tracked ? m_Tracked.position : transform.position;
 70         return new Bounds(Quantize(center, .001f * m_Size), m_Size);
 71     }
 72 
 73     //选择物体时在Scene中绘制Bound区域
 74     void OnDrawGizmosSelected()
 75     {
 76         if (m_NavMesh)
 77         {
 78             Gizmos.color = Color.green;
 79             Gizmos.DrawWireCube(m_NavMesh.sourceBounds.center, m_NavMesh.sourceBounds.size);
 80         }
 81 
 82         Gizmos.color = Color.yellow;
 83         var bounds = QuantizedBounds();
 84         Gizmos.DrawWireCube(bounds.center, bounds.size);
 85 
 86         Gizmos.color = Color.green;
 87         var center = m_Tracked ? m_Tracked.position : transform.position;
 88         Gizmos.DrawWireCube(center, m_Size);
 89     }
 90 
 91     //动态烘焙NavMesh
 92     public void Bake()
 93     {
 94         // Construct and add navmesh
 95         m_NavMesh = new NavMeshData();
 96         m_Instance = NavMesh.AddNavMeshData(m_NavMesh);
 97         if (m_Tracked == null)
 98             m_Tracked = transform;
 99         UpdateNavMesh(false);
100     }
101 }

有一个地方需要注意,因为NavMeshBuilder.UpdateNavMeshData(m_NavMesh, defaultBuildSettings, m_Sources, bounds); 刷新NavMeshData时需要读取模型的网格信息,此时需要将导入的模型读写打开,设置位置如下:

用法示例:

 1 using UnityEngine;
 2 
 3 public class LocalNavMeshCtrl : MonoBehaviour
 4 {
 5     public LocalNavMeshBuilder Bulider;
 6     public float Offse;
 7     void Awake()
 8     {
 9         EventManager.AddListener<EnterRoomEvent>(EnterRoomHanlder);
10     }
11 
12     private void EnterRoomHanlder(EnterRoomEvent e)
13     {
14         if (Bulider != null)
15         {
16             var rooms = BattleUtils.MapMgr.Rooms;
17             if (rooms.ContainsKey(e.RoomIndex) && rooms[e.RoomIndex].RoomType == RoomType.Battle)
18             {
19                 Bulider.m_Tracked = rooms[e.RoomIndex].transform;
20                 var size = PTBattleMgr.CurRoomCtrl.Size;
21                 Bulider.m_Size = new Vector3(size.x * 4 + Offse, 10, size.y * 4 + Offse);
22             }
23         }
24     }
25 
26     private void OnDestroy()
27     {
28         EventManager.RemoveListener<EnterRoomEvent>(EnterRoomHanlder);
29     }
30 }

例如进入某一房间或区域就按照该房间区域的大小进行NavMesh的动态烘焙,可以非常方便的改变烘焙的范围和中心点等,也可以考虑让该烘焙范围一直跟随玩家的Transform运动。

 

一个区域内的NavMesh动态烘焙完成后,很多AI可能需要在NavMesh中取随机点进行导航的目标点的设置或巡逻等,可以写一个扩展方法得到NavMesh的顶点数据,取任何一个三角内的点即可:

 1     public static Vector3 GetNavMeshRandomPos(this GameObject obj)
 2     {
 3         NavMeshTriangulation navMeshData = NavMesh.CalculateTriangulation();
 4 
 5         int t = Random.Range(0, navMeshData.indices.Length - 3);
 6 
 7         Vector3 point = Vector3.Lerp(navMeshData.vertices[navMeshData.indices[t]], navMeshData.vertices[navMeshData.indices[t + 1]], Random.value);
 8         point = Vector3.Lerp(point, navMeshData.vertices[navMeshData.indices[t + 2]], Random.value);
 9 
10         return point;
11     }
posted @ 2020-01-15 15:04  汐夜  阅读(6467)  评论(0编辑  收藏  举报