美文网首页
Unity NavMesh 动态烘焙

Unity NavMesh 动态烘焙

作者: 忆中异 | 来源:发表于2022-03-30 18:53 被阅读0次

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

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

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

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

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

    image.png

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

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

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

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

    using UnityEngine;
    using UnityEngine.AI;
    using System.Collections.Generic;
    
    // Tagging component for use with the LocalNavMeshBuilder
    // Supports mesh-filter and terrain - can be extended to physics and/or primitives
    [DefaultExecutionOrder(-200)]
    public class NavMeshSourceTag : MonoBehaviour
    {
        // Global containers for all active mesh/terrain tags
        public static List<MeshFilter> m_Meshes = new List<MeshFilter>();
        public static List<Terrain> m_Terrains = new List<Terrain>();
    
        void OnEnable()
        {
            var m = GetComponent<MeshFilter>();
            if (m != null)
            {
                m_Meshes.Add(m);
            }
    
            var t = GetComponent<Terrain>();
            if (t != null)
            {
                m_Terrains.Add(t);
            }
        }
    
        void OnDisable()
        {
            var m = GetComponent<MeshFilter>();
            if (m != null)
            {
                m_Meshes.Remove(m);
            }
    
            var t = GetComponent<Terrain>();
            if (t != null)
            {
                m_Terrains.Remove(t);
            }
        }
    
        // Collect all the navmesh build sources for enabled objects tagged by this component
        public static void Collect(ref List<NavMeshBuildSource> sources)
        {
            sources.Clear();
    
            for (var i = 0; i < m_Meshes.Count; ++i)
            {
                var mf = m_Meshes[i];
                if (mf == null) continue;
    
                var m = mf.sharedMesh;
                if (m == null) continue;
    
                var s = new NavMeshBuildSource();
                s.shape = NavMeshBuildSourceShape.Mesh;
                s.sourceObject = m;
                s.transform = mf.transform.localToWorldMatrix;
                s.area = 0;
                sources.Add(s);
            }
    
            for (var i = 0; i < m_Terrains.Count; ++i)
            {
                var t = m_Terrains[i];
                if (t == null) continue;
    
                var s = new NavMeshBuildSource();
                s.shape = NavMeshBuildSourceShape.Terrain;
                s.sourceObject = t.terrainData;
                // Terrain system only supports translation - so we pass translation only to back-end
                s.transform = Matrix4x4.TRS(t.transform.position, Quaternion.identity, Vector3.one);
                s.area = 0;
                sources.Add(s);
            }
        }
    }
    

    NavMeshSourceTag类是为了收集需要录入烘焙列表的模型网格数据和地形数据,用的是一个全局的静态数据列表来存储,需要挂载在场景的网格物件上,标记哪些物件的网格在生成数据时需要考虑在内。
    当然了,如果一个物体是由多个网格拼接而成,读者只需要将OnEnable和OnDisable中的代码稍作修改,改为读取子物体中的所以MeshFilter和Terrain组件即可:

    foreach (var m in GetComponentsInChildren<MeshFilter>())
            {
                if (m != null)
                {
                    m_Meshes.Add(m);
                }
            }
    

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

    using UnityEngine;
    using UnityEngine.AI;
    using System.Collections;
    using System.Collections.Generic;
    using NavMeshBuilder = UnityEngine.AI.NavMeshBuilder;
    
    // Build and update a localized navmesh from the sources marked by NavMeshSourceTag
    [DefaultExecutionOrder(-102)]
    public class LocalNavMeshBuilder : MonoBehaviour
    {
        // The center of the build
        public Transform m_Tracked;
    
        // The size of the build bounds
        public Vector3 m_Size = new Vector3(80.0f, 20.0f, 80.0f);
    
        NavMeshData m_NavMesh;
        AsyncOperation m_Operation;
        NavMeshDataInstance m_Instance;
        List<NavMeshBuildSource> m_Sources = new List<NavMeshBuildSource>();
    
        IEnumerator Start()
        {
            while (true)
            {
                UpdateNavMesh(true);
                yield return m_Operation;
            }
        }
    
        void OnEnable()
        {
            Bake();
        }
    
        void OnDisable()
        {
            //Unload navmesh and clear handle
            m_Instance.Remove();
        }
    
        /// <summary>
        /// 按范围动态更新NavMesh
        /// </summary>
        /// <param name="asyncUpdate">是否异步加载</param>
        void UpdateNavMesh(bool asyncUpdate = false)
        {
            NavMeshSourceTag.Collect(ref m_Sources);
            var defaultBuildSettings = NavMesh.GetSettingsByID(0);
            var bounds = QuantizedBounds();
    
            if (asyncUpdate)
                m_Operation = NavMeshBuilder.UpdateNavMeshDataAsync(m_NavMesh, defaultBuildSettings, m_Sources, bounds);
            else
                NavMeshBuilder.UpdateNavMeshData(m_NavMesh, defaultBuildSettings, m_Sources, bounds);
        }
    
        static Vector3 Quantize(Vector3 v, Vector3 quant)
        {
            float x = quant.x * Mathf.Floor(v.x / quant.x);
            float y = quant.y * Mathf.Floor(v.y / quant.y);
            float z = quant.z * Mathf.Floor(v.z / quant.z);
            return new Vector3(x, y, z);
        }
    
        Bounds QuantizedBounds()
        {
            // Quantize the bounds to update only when theres a 0.1% change in size
            var center = m_Tracked ? m_Tracked.position : transform.position;
            return new Bounds(Quantize(center, .001f * m_Size), m_Size);
        }
    
        //选择物体时在Scene中绘制Bound区域
        void OnDrawGizmosSelected()
        {
            if (m_NavMesh)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawWireCube(m_NavMesh.sourceBounds.center, m_NavMesh.sourceBounds.size);
            }
    
            Gizmos.color = Color.yellow;
            var bounds = QuantizedBounds();
            Gizmos.DrawWireCube(bounds.center, bounds.size);
    
            Gizmos.color = Color.green;
            var center = m_Tracked ? m_Tracked.position : transform.position;
            Gizmos.DrawWireCube(center, m_Size);
        }
    
        //动态烘焙NavMesh
        public void Bake()
        {
            // Construct and add navmesh
            m_NavMesh = new NavMeshData();
            m_Instance = NavMesh.AddNavMeshData(m_NavMesh);
            if (m_Tracked == null)
                m_Tracked = transform;
            UpdateNavMesh(false);
        }
    }
    

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

    image.png

    用法示例:

    using UnityEngine;
    
    public class LocalNavMeshCtrl : MonoBehaviour
    {
        public LocalNavMeshBuilder Bulider;
        public float Offse;
        void Awake()
        {
            EventManager.AddListener<EnterRoomEvent>(EnterRoomHanlder);
        }
    
        private void EnterRoomHanlder(EnterRoomEvent e)
        {
            if (Bulider != null)
            {
                var rooms = BattleUtils.MapMgr.Rooms;
                if (rooms.ContainsKey(e.RoomIndex) && rooms[e.RoomIndex].RoomType == RoomType.Battle)
                {
                    Bulider.m_Tracked = rooms[e.RoomIndex].transform;
                    var size = PTBattleMgr.CurRoomCtrl.Size;
                    Bulider.m_Size = new Vector3(size.x * 4 + Offse, 10, size.y * 4 + Offse);
                }
            }
        }
    
        private void OnDestroy()
        {
            EventManager.RemoveListener<EnterRoomEvent>(EnterRoomHanlder);
        }
    }
    

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

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

    public static Vector3 GetNavMeshRandomPos(this GameObject obj)
        {
            NavMeshTriangulation navMeshData = NavMesh.CalculateTriangulation();
    
            int t = Random.Range(0, navMeshData.indices.Length - 3);
    
            Vector3 point = Vector3.Lerp(navMeshData.vertices[navMeshData.indices[t]], navMeshData.vertices[navMeshData.indices[t + 1]], Random.value);
            point = Vector3.Lerp(point, navMeshData.vertices[navMeshData.indices[t + 2]], Random.value);
    
            return point;
        }
    

    相关文章

      网友评论

          本文标题:Unity NavMesh 动态烘焙

          本文链接:https://www.haomeiwen.com/subject/euswjrtx.html