一.应用背景
由于公司项目较大,为了妥善管理庞大繁杂的资源,我们采用了工作场景和运行时场景相分离的策略,将美术工作场景独立出来。美术人员只需要在自己的工作场景下操作,待场景搭建完善后,采用一套自动化的Build工作管线将美术工作场景资源导出为运行时所需的场景资源,以此来实现自动化的工作流程。该自动化Build管线也较为复杂,除了具体资源的Build逻辑外,还包括大量资源的依赖关系重建。在Build管线的开发过程中需要在我们的代码中大量调用并设定许多系统参数,而某些参数unity并未直接提供API来设定,因而在开发过程中需要频繁使用反射机制调用unity私有函数或者访问私有字段/属性,同时还会使用SerializedObject和SerializedProperty来修改无法直接通过API或反射来访问的字段/属性。本文将把导航网格设定相关的参数封装起来,以便在代码中直接调用并设定,而不是通过unity原有的参数设定面板来设定。
二.设计思想
1.unity原有的导航网格参数设定面板如下:
该面板主要由四部分组成,其中Agents和Areas栏中的参数独立于具体场景,即所有本工程的场景共用这些参数设定,其参数保存在ProjectSettings/NavMeshAreas.asset(如果打开是乱码,需要将工程的Serialization Mode设为Force Text,设置方法Edit->Project Settings...->Editor->Asset Serialization->Mode),如下图:
Bake栏的参数与具体场景相关,即每个场景的设定是独立的,其参数设定保存在具体的场景文件中(*.unity,如果打开为乱码,请将工程的Serialization Mode设为Force Text),如下图:
至于Object栏并未涉及到参数设定,因此这里不讨论它。
2.对Agents和Areas栏相关参数的设定,当然我们可以直接以文本形式打开并编辑NavMeshAreas.asset中的相应参数,但其前提是工程的Serialization Mode为Force Text。当条件不允许时(比如项目过大时),直接编辑NavMeshAreas.asset行不通,此时就需要使用SerializedObject和SerializedProperty的组合来来实现我们的需求。此时的关键是要获取NavMeshAreas.asset对应的Unity对象,可通过以下API获取: Unsupported.GetSerializedAssetInterfaceSingleton("NavMeshProjectSettings");
3.同理对Bake栏的参数的设定,同样需要获取存储参数的对象,可通过以下API获取:NavMeshBuilder.navMeshSettingsObject;
4.获取承载参数设定的对象后,我们需要将这些对象序列化,以便提取我们需要的参数,可构建其对应的SerializedObject对象;
5.在获取SerializedObject对象后,我们通过SerializedObject.FindProperty(string propertyPath)传入参数路径便可以获取到所需的参数;
6.对于生成的导航网格需要重新关联到指定场景时,最便捷的办法也是采用SerializedObject和SerializedProperty的组合。
三.核心源码
1.获取NavMeshAreas.asset对象并提取Agent和Area的源码:
public class NavMeshProjectSettingsParamSetter
{
private SerializedObject m_navMeshProjectSettingsSo;
internal NavMeshProjectSettingsParamSetter()
{
var serializedAssetInterfaceSingleton = Unsupported.GetSerializedAssetInterfaceSingleton("NavMeshProjectSettings");
m_navMeshProjectSettingsSo = new SerializedObject(serializedAssetInterfaceSingleton);
}
public NavigationAgent GetAgentByIndex(int index = 0)
{
return new NavigationAgent(m_navMeshProjectSettingsSo, index);
}
public int GetAgentsCount()
{
var agents = m_navMeshProjectSettingsSo.FindProperty("m_Settings");
return agents.arraySize;
}
public string[] GetAgentNames()
{
var agentnamesProp = m_navMeshProjectSettingsSo.FindProperty("m_SettingNames");
var names = new string[agentnamesProp.arraySize];
for (var index = 0; index < names.Length; index++)
{
names[index] = agentnamesProp.GetArrayElementAtIndex(index).stringValue;
}
return names;
}
public NavigationArea GetNavigationArea(int index = 0)
{
return new NavigationArea(m_navMeshProjectSettingsSo, index);
}
}
2.Agent参数设定源码:
public class NavigationAgent
{
private SerializedObject m_navMeshProjectSettingsObject;
private int agentIndex;
private SerializedProperty m_agentName;
private SerializedProperty m_agentRadius;
private SerializedProperty m_agentHeight;
private SerializedProperty m_agentStepHeight;
private SerializedProperty m_agentMaxSlope;
public NavigationAgent(SerializedObject navMeshProjectSettingsObject, int agentIndex)
{
this.m_navMeshProjectSettingsObject = navMeshProjectSettingsObject;
this.agentIndex = agentIndex;
var _agents = m_navMeshProjectSettingsObject.FindProperty("m_Settings");
var _settingNames = m_navMeshProjectSettingsObject.FindProperty("m_SettingNames");
var agent = _agents.GetArrayElementAtIndex(agentIndex);
m_agentName = _settingNames.GetArrayElementAtIndex(agentIndex);
m_agentRadius = agent.FindPropertyRelative("agentRadius");
m_agentHeight = agent.FindPropertyRelative("agentHeight");
m_agentStepHeight = agent.FindPropertyRelative("agentClimb");
m_agentMaxSlope = agent.FindPropertyRelative("agentSlope");
}
public int AgentIndex
{
get { return agentIndex; }
}
public string AgentName
{
get { return m_agentName.stringValue; }
set
{
m_agentName.stringValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
public float AgentRadius
{
get { return m_agentRadius.floatValue; }
set
{
m_agentRadius.floatValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
public float AgentHeight
{
get { return m_agentHeight.floatValue; }
set
{
m_agentHeight.floatValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
public float AgentStepHeight
{
get { return m_agentStepHeight.floatValue; }
set
{
m_agentStepHeight.floatValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
public float AgentMaxSlope
{
get { return m_agentMaxSlope.floatValue; }
set
{
m_agentMaxSlope.floatValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
}
3.Area参数设定源码
public class NavigationArea
{
private SerializedObject m_navMeshProjectSettingsObject;
private int areaIndex;
private SerializedProperty m_name;
private SerializedProperty m_cost;
internal NavigationArea(SerializedObject navMeshProjectSettingsObject, int areaIndex)
{
this.m_navMeshProjectSettingsObject = navMeshProjectSettingsObject;
this.areaIndex = areaIndex;
var areas = m_navMeshProjectSettingsObject.FindProperty("areas");
var size = areas.arraySize;
if (areaIndex < 0 || areaIndex > areas.arraySize - 1)
{
throw new System.ArgumentOutOfRangeException("越界");
}
var area = areas.GetArrayElementAtIndex(areaIndex);
m_name = area.FindPropertyRelative("name");
m_cost = area.FindPropertyRelative("cost");
}
public int AreaIndex
{
get { return areaIndex; }
}
public string Name
{
get { return m_name.stringValue; }
set
{
m_name.stringValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
public float Cost
{
get { return m_cost.floatValue; }
set
{
m_cost.floatValue = value;
m_navMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
}
4.Bake参数设定源码
public class NavMeshSettingsBakeParamSetter
{
private SerializedObject m_navigationSettings;
private SerializedProperty m_agentRadius;
private SerializedProperty m_agentHeight;
private SerializedProperty m_maxSlope;
private SerializedProperty m_stepHeight;
private SerializedProperty m_dropHeight;
private SerializedProperty m_jumpDistance;
private SerializedProperty m_manualVoxelSize;
private SerializedProperty m_voxelSize;
private SerializedProperty m_minRegionArea;
private SerializedProperty m_heightMesh;
internal NavMeshSettingsBakeParamSetter(Scene scene)
{
SceneManager.SetActiveScene(scene);
m_navigationSettings = new SerializedObject(NavMeshBuilder.navMeshSettingsObject);
m_agentRadius = m_navigationSettings.FindProperty("m_BuildSettings.agentRadius");
m_agentHeight = m_navigationSettings.FindProperty("m_BuildSettings.agentHeight");
m_maxSlope = m_navigationSettings.FindProperty("m_BuildSettings.agentSlope");
m_stepHeight = m_navigationSettings.FindProperty("m_BuildSettings.agentClimb");
m_dropHeight = m_navigationSettings.FindProperty("m_BuildSettings.ledgeDropHeight");
m_jumpDistance = m_navigationSettings.FindProperty("m_BuildSettings.maxJumpAcrossDistance");
m_manualVoxelSize = m_navigationSettings.FindProperty("m_BuildSettings.manualCellSize");
m_voxelSize = m_navigationSettings.FindProperty("m_BuildSettings.cellSize");
m_minRegionArea = m_navigationSettings.FindProperty("m_BuildSettings.minRegionArea");
m_heightMesh = m_navigationSettings.FindProperty("m_BuildSettings.accuratePlacement");
}
public float AgentRadius
{
get { return m_agentRadius.floatValue; }
set
{
m_agentRadius.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float AgentHeight
{
get { return m_agentHeight.floatValue; }
set
{
m_agentHeight.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float MaxSlope
{
get { return m_maxSlope.floatValue; }
set
{
m_maxSlope.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float StepHeight
{
get { return m_stepHeight.floatValue; }
set
{
m_stepHeight.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float DropHeight
{
get { return m_dropHeight.floatValue; }
set
{
m_dropHeight.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float JumpDistance
{
get { return m_jumpDistance.floatValue; }
set
{
m_jumpDistance.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public bool ManualVoxelSize
{
get { return m_manualVoxelSize.boolValue; }
set
{
m_manualVoxelSize.boolValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float VoxelSize
{
get { return m_voxelSize.floatValue; }
set
{
m_voxelSize.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public float MinRegionArea
{
get { return m_minRegionArea.floatValue; }
set
{
m_minRegionArea.floatValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
public bool HeightMesh
{
get { return m_heightMesh.boolValue; }
set
{
m_heightMesh.boolValue = value;
m_navigationSettings.ApplyModifiedProperties();
}
}
}
5.最后将上功能述封装起来,对外提供统一接口,同时这里也提供了获取与指定场景相关联导航网格的API,以及将指定导航网格关联到指定场景上去的API,这些都是无法直接通过调用Unity的API实现的。
public class NavigationHelper
{
public static NavMeshProjectSettingsParamSetter GetNavMeshProjectSettings()
{
return new NavMeshProjectSettingsParamSetter();
}
public static NavMeshSettingsBakeParamSetter GetNavMeshSettings(Scene scene)
{
return new NavMeshSettingsBakeParamSetter(scene);
}
public static Object GetNavMeshData(Scene scene)
{
SceneManager.SetActiveScene(scene);
var naviMeshSettings = new SerializedObject(NavMeshBuilder.navMeshSettingsObject);
var prop = naviMeshSettings.FindProperty("m_NavMeshData");
return prop != null ? prop.objectReferenceValue : null;
}
public static void SetNavMeshData(Scene scene, Object obj)
{
SceneManager.SetActiveScene(scene);
var naviMeshSettings = new SerializedObject(NavMeshBuilder.navMeshSettingsObject);
var prop = naviMeshSettings.FindProperty("m_NavMeshData");
if (prop != null)
{
prop.objectReferenceValue = obj;
naviMeshSettings.ApplyModifiedProperties();
}
EditorSceneManager.SaveScene(scene, scene.path);
}
}
四.测试结果
针对上述源码,这里进行相应的测试,测试源码如下:
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
public class NavigationHelperTest : MonoBehaviour
{
[MenuItem("Test/AgentTest")]
static void AgentTest()
{
var navMeshProjectSettings = NavigationHelper.GetNavMeshProjectSettings();
var agent0 = navMeshProjectSettings.GetAgentByIndex(0);
Debug.Log($"Index={agent0.AgentIndex},Name={agent0.AgentName}," +
$"Radius={agent0.AgentRadius},Height={ agent0.AgentHeight}," +
$"StepHeight={agent0.AgentStepHeight},MaxSlope={ agent0.AgentMaxSlope}");
agent0.AgentName = "AgentTest";
agent0.AgentRadius = 1;
agent0.AgentHeight = 1;
agent0.AgentStepHeight = 1;
agent0.AgentMaxSlope = 60;
Debug.Log($"Index={agent0.AgentIndex},Name={agent0.AgentName}," +
$"Radius={agent0.AgentRadius},Height={ agent0.AgentHeight}," +
$"StepHeight={agent0.AgentStepHeight},MaxSlope={ agent0.AgentMaxSlope}");
}
[MenuItem("Test/AreaTest")]
static void AreaTest()
{
var navMeshProjectSettings = NavigationHelper.GetNavMeshProjectSettings();
var area = navMeshProjectSettings.GetNavigationArea(3);
Debug.Log($"Index={area.AreaIndex},name={area.Name},cost={area.Cost}");
area.Name = "AreaTest";
area.Cost = 5;
Debug.Log($"Index={area.AreaIndex},name={area.Name},cost={area.Cost}");
}
[MenuItem("Test/BakeTest")]
static void BakeTest()
{
var bakeSetter = NavigationHelper.GetNavMeshSettings(SceneManager.GetActiveScene());
Debug.Log($"AgentRadius={bakeSetter.AgentRadius},AgentHeight={bakeSetter.AgentHeight}" +
$",MaxSlope={bakeSetter.MaxSlope},StepHeight={bakeSetter.StepHeight}" +
$",DropHeight={bakeSetter.DropHeight},JumpDistance={bakeSetter.JumpDistance}" +
$",ManualVoxelSize={bakeSetter.ManualVoxelSize},VoxelSize={bakeSetter.VoxelSize}" +
$",MinRegionArea={bakeSetter.MinRegionArea},HeightMesh={bakeSetter.HeightMesh}");
bakeSetter.AgentRadius = 1;
bakeSetter.AgentHeight = 1;
bakeSetter.MaxSlope = 1;
bakeSetter.StepHeight = 1;
bakeSetter.DropHeight = 1;
bakeSetter.JumpDistance = 1;
bakeSetter.ManualVoxelSize = true;
bakeSetter.VoxelSize = 1;
bakeSetter.MinRegionArea = 1;
bakeSetter.HeightMesh = true;
Debug.Log($"AgentRadius={bakeSetter.AgentRadius},AgentHeight={bakeSetter.AgentHeight}" +
$",MaxSlope={bakeSetter.MaxSlope},StepHeight={bakeSetter.StepHeight}" +
$",DropHeight={bakeSetter.DropHeight},JumpDistance={bakeSetter.JumpDistance}" +
$",ManualVoxelSize={bakeSetter.ManualVoxelSize},VoxelSize={bakeSetter.VoxelSize}" +
$",MinRegionArea={bakeSetter.MinRegionArea},HeightMesh={bakeSetter.HeightMesh}");
}
[MenuItem("Test/Redirect")]
static void Redirect()
{
var src = "Assets/Test1.unity";
var target = "Assets/Test2.unity";
var srcScene = EditorSceneManager.OpenScene(src, OpenSceneMode.Single);
var navidata = NavigationHelper.GetNavMeshData(srcScene);
var targetScene = EditorSceneManager.OpenScene(target, OpenSceneMode.Single);
NavigationHelper.SetNavMeshData(targetScene, navidata);
}
}
1.Agent参数设定测试效果:
2.Area参数设定效果:
3.针对Test1场景的Bake参数设定效果:
4.将Test1场景的导航网格数据重新关联到Test2场景,效果如下:
五.工程源码
详见本人Github。