1、环境
- Unity 2020.3.25f1
- 烘焙方式的NavMesh做法
2、问题描述
项目中一个副本章节有多个关卡,每个关卡有一个独立的场景,玩家可以在场景中用摇杆行走(NavMesh烘路面)。策划希望关卡切换能营造出一个完整大地图的感觉(无缝切换),所以需要对章节中所有关卡的场景做预加载。
我们采用Additive方式将多个Scene同时加载出来,然后将非当前关卡的RootGameObject隐藏,实现仅显示一个关卡的效果。这种方式多个Scene的NavMesh都被加载出来了,且同时生效,导致寻路错误,比如:看上去不能走的地方,走上去了;本该通行的路面,被卡住等。
网上一通搜得知,烘培方式的NavMesh是和场景绑定的,场景加载出来NavMesh就一起带出来了,并且Unity并未提供简单的方式将这种绑定关系解除。
3、解决方案
翻Manual文档找禁用NavMesh接口时,发现有动态添加、移除NavMeshData的API,然后看了眼烘培生成的Asset恰好是NavMeshData。此时萌生了想法:
- 将NavMesh跟Scene断开关联,保证LoadScene的时候NavMesh不会被强制加载生效
- 场景里挂个脚本,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);
}
}
最后把试验的结果整合起来,完整实现这个解决方案!
EditorSceneManager.sceneSaving
的时候,获取当前场景的NavMeshData,并在场景中创建NavMeshHolder节点,引用NavMeshData对象。(本来这步处理也想放到Closing时一起做,但关闭时创建对象Unity会报错)EditorSceneManager.sceneClosing
的时候,将NavMeshData从Scene中解绑。- 运行时的东西都在NavMeshHolder中,不需要其他处理了
这个方案有个弊端,美术同学下次打开场景是看不到NavMesh的,必须要手动烘焙一下。尝试过解决这个问题,没找到合适方案。最终跟美术同学确认,烘焙一次只需要几秒钟,这个就不管了。
Unity自己有个实时烘焙的Package:NavMesh Components,应该可以更好的解决问题。我们临近出版本,先用快速方案撑一波。
4、参考资料
最后,记录下没成功的,还原场景NavMesh的方案
-
NavMeshEditorHelpers.DrawBuildDebug
看API介绍像是用来绘制NavMesh的,但没成功,可能因为我没有用NavMeshBuilder逐步构建。 -
NavMeshHolder
标记为[ExecuteInEditMode]
这个方案,NavMesh确实可以显示出来,但是重新烘焙后,它引用的资源还是旧的,没有更新。
需要一个烘焙结束的事件来刷新NavMeshData,这个方案才真正可用。我翻了一遍相关的Editor代码,没找到合适的。 - 将2变种一下,加按钮让美术自己点显示、隐藏
这个方案没实际做,因为都要手动点按钮,直接烘焙更简单、安全,美术也不必关心额外的限制规则。
网友评论