[RS] Unity多场景加载,NavMesh重叠问题解决

2022-03-01  本文已影响0人  _Walker__

1、环境

2、问题描述

  项目中一个副本章节有多个关卡,每个关卡有一个独立的场景,玩家可以在场景中用摇杆行走(NavMesh烘路面)。策划希望关卡切换能营造出一个完整大地图的感觉(无缝切换),所以需要对章节中所有关卡的场景做预加载。
  我们采用Additive方式将多个Scene同时加载出来,然后将非当前关卡的RootGameObject隐藏,实现仅显示一个关卡的效果。这种方式多个Scene的NavMesh都被加载出来了,且同时生效,导致寻路错误,比如:看上去不能走的地方,走上去了;本该通行的路面,被卡住等。

NavMesh重叠

  网上一通搜得知,烘培方式的NavMesh是和场景绑定的,场景加载出来NavMesh就一起带出来了,并且Unity并未提供简单的方式将这种绑定关系解除。

3、解决方案

  翻Manual文档找禁用NavMesh接口时,发现有动态添加、移除NavMeshData的API,然后看了眼烘培生成的Asset恰好是NavMeshData。此时萌生了想法:

  1. 将NavMesh跟Scene断开关联,保证LoadScene的时候NavMesh不会被强制加载生效
  2. 场景里挂个脚本,Enable时加载NavMesh,Disable时移除NavMesh

首先做个快速的验证测试,我手动将Scene序列化文件中对NavMeshData的引用删除

// Scene引用的NavMeshData
m_NavMeshData: {fileID: 23800000, guid: d82730c1770cac545a77d844d83bc62b, type: 2}
// 改成
m_NavMeshData: {fileID: 0}

然后把第2项做了

public class NavMeshHolder : MonoBehaviour
{
    public NavMeshData NavData;
    private NavMeshDataInstance _navDataInst;

    public void OnEnable()
    {
        if (null != NavData)
        {
            _navDataInst = NavMesh.AddNavMeshData(NavData);
        }
    }

    public void OnDisable()
    {
        if (null != NavData)
        {
            NavMesh.RemoveNavMeshData(_navDataInst);
        }
    }
}

  进游戏测试,问题解决!接下来是怎么优美的解决第1项,最粗暴的方案是用正则匹配替换。自己不喜欢用这种,不管文件结构的处理方式,所以还是先尝试找Unity的接口。两个方向:

1)用SerializedObject修改场景序列化信息
2)看Unity是否直接提供了相应的API。

  开始两个方向都没找到切入点,后来搜到一篇帖子(参考资料)给出了1)的做法,实现的过程中又发现了2)的接口。两种做法都记录下

// 1) 用SerializedObject获取、修改NavMeshData
public static NavMeshData CurSceneNavMeshData
{
    get
    {
        SerializedObject so = new SerializedObject(NavMeshBuilder.navMeshSettingsObject);
        SerializedProperty sp = so.FindProperty("m_NavMeshData");
        NavMeshData data = (NavMeshData) sp.objectReferenceValue;
        return data;
    }
    set
    {
        SerializedObject so = new SerializedObject(NavMeshBuilder.navMeshSettingsObject);
        SerializedProperty sp = so.FindProperty("m_NavMeshData");
        sp.objectReferenceValue = value;
        so.ApplyModifiedPropertiesWithoutUndo();
    }
}
// 2) 直接用UnityAPI,这里需要反射,Unity有接口但没有public出来
private static DynamicType _NavMeshBuilder = new DynamicType(typeof(NavMeshBuilder));

public static NavMeshData CurSceneNavMeshData
{
    get
    {
        NavMeshData data = (NavMeshData) _NavMeshBuilder.PrivateStaticProperty<Object>("sceneNavMeshData");
        return data;
    }
    set
    {
        BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic;
        _NavMeshBuilder.SetProperty("sceneNavMeshData", flags, value);
    }
}

  最后把试验的结果整合起来,完整实现这个解决方案!

  1. EditorSceneManager.sceneSaving的时候,获取当前场景的NavMeshData,并在场景中创建NavMeshHolder节点,引用NavMeshData对象。(本来这步处理也想放到Closing时一起做,但关闭时创建对象Unity会报错)
  2. EditorSceneManager.sceneClosing的时候,将NavMeshData从Scene中解绑。
  3. 运行时的东西都在NavMeshHolder中,不需要其他处理了

  这个方案有个弊端,美术同学下次打开场景是看不到NavMesh的,必须要手动烘焙一下。尝试过解决这个问题,没找到合适方案。最终跟美术同学确认,烘焙一次只需要几秒钟,这个就不管了。

  Unity自己有个实时烘焙的Package:NavMesh Components,应该可以更好的解决问题。我们临近出版本,先用快速方案撑一波。

4、参考资料

【unity】自动化流程之代码修改导航网格参数


  最后,记录下没成功的,还原场景NavMesh的方案

  1. NavMeshEditorHelpers.DrawBuildDebug
    看API介绍像是用来绘制NavMesh的,但没成功,可能因为我没有用NavMeshBuilder逐步构建。
  2. NavMeshHolder标记为[ExecuteInEditMode]
    这个方案,NavMesh确实可以显示出来,但是重新烘焙后,它引用的资源还是旧的,没有更新。
    需要一个烘焙结束的事件来刷新NavMeshData,这个方案才真正可用。我翻了一遍相关的Editor代码,没找到合适的。
  3. 将2变种一下,加按钮让美术自己点显示、隐藏
    这个方案没实际做,因为都要手动点按钮,直接烘焙更简单、安全,美术也不必关心额外的限制规则。
上一篇下一篇

猜你喜欢

热点阅读