最初的Unity导航系统很不完善,只能静态烘焙场景图的可行走区域,而且必须在本地保存场景的NavMesh数据,难以运行时动态计算;这使得鲜有开发者愿意再尝试Unity内置的导航功能,转向了AStar寻路算法的研究。
但实际上AStar算法真的适合大多数开发情况且性能较优么?
了解过AStar算法的都知道,它是基于格子来遍历计算行走权重的,算法复杂度其实是相对较高的,受到格子密度,地图大小和路线长度的的影响较大。
AStar更适合的是策略性寻路,该算法更有利于找出最短路径的最优解,能够达到足够的精确性。
而Unity的NavMesh是用的拐角点算法,随便找一个场景烘焙一下便可得知,例如:
烘焙出来的NavMesh区域只在障碍物边缘与平面边缘存在顶点,而不会像AStar一样均匀的布满整个平面;如果是一个无任何障碍物的平面,那就只会有平面边缘的几个顶点,算法效率是相对较高的,并不会因为地图变大而有明显算法复杂度上的变化。
相反,NavMesh的缺点也正是AStar的优点,那就是难以保证寻路的最优解,更多的时候是用于AI能够更快计算出绕过障碍物朝向目标前进的路径。
对于场景不变的静态地图来说,Unity最初的NavMesh已经能够满足需求,但如果地图随机生成或障碍物的位置随时变化,此时静态NavMesh一下子就捉襟见肘了。
好在随着Unity版本的更新,关于动态烘焙的方法也已经能有效实现,这样无论是以怎样千变万化的方式生成的随机地图,随机地图在游戏中如何构建重组,都能动态刷新出NavMesh的可行走区域。
代码语言:javascript复制 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 }
代码语言:javascript复制NavMeshSourceTag类是为了收集需要录入烘焙列表的模型网格数据和地形数据,用的是一个全局的静态数据列表来存储,需要挂载在场景的网格物件上,标记哪些物件的网格在生成数据时需要考虑在内。
代码语言:javascript复制 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 10% change in size
69 var center = m_Tracked ? m_Tracked.position : transform.position;
70 return new Bounds(Quantize(center, 0.1f * 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 }
将之前收集到的网格物件的源数据动态刷新生成NavMesh,用法示例:
代码语言:javascript复制 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的顶点数据,取任何一个三角内的点即可:
代码语言:javascript复制 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 }