Unity NavMesh 动态烘焙

最初的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;
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容