环境:Unity 2018.4.12
需求:
项目中经常需要在Unity Editor模式下批量删除Prefab中的某个脚本,但是直接用`GameObject.DestroyImmediate(obj);`会报错:Destroying assets is not permitted to avoid data loss.解决方法是通过`SerializedProperty.DeleteArrayElementAtIndex(i)`。
代码:
private bool RemoveComponents(Component[] components)
{
if (components.Length == 0)
{
return false;
}
foreach (var component in components)
{
SerializedObject so = new SerializedObject(component.gameObject);
SerializedProperty sp = so.FindProperty("m_Component");
var allComponents = component.gameObject.GetComponents<Component>();
for (int i = 0, len = allComponents.Length; i < len; i++)
{
if (allComponents[i] == component)
{
sp.DeleteArrayElementAtIndex(i);
so.ApplyModifiedProperties();
break;
}
}
}
return true;
}
public bool RemoveComponents<T>(GameObject root) where T : Component
{
if (root == null)
{
return false;
}
var components = root.GetComponentsInChildren<T>(true);
return RemoveComponents(components);
}
工具
为了方便使用,实现一个EditorWindow来操作,代码如下:
using UnityEditor;
using UnityEngine;
namespace Common.Editor.Common
{
public class RemoveComponentEditor : EditorWindow
{
private static RemoveComponentEditor _instance;
private static string[] _selectAssets;
private static string _key;
private static MonoScript _monoScript;
[MenuItem("PrefabTool/RemoveComponentEditor")]
public static void ShowWindow()
{
_instance = GetWindow(typeof(RemoveComponentEditor)) as RemoveComponentEditor;
_instance.Show();
}
void OnGUI()
{
_selectAssets = Selection.assetGUIDs;
if (_selectAssets == null || _selectAssets.Length == 0)
{
EditorGUILayout.LabelField("No Assets Selected");
}
else
{
EditorGUILayout.LabelField($"Selected Assets:\n");
foreach (var asset in _selectAssets)
{
var path = AssetDatabase.GUIDToAssetPath(asset);
EditorGUILayout.LabelField($"{path}\n");
}
}
_key = EditorGUILayout.TextField("Prefab Name Key:", _key);
_monoScript = EditorGUILayout.ObjectField("Script", _monoScript, typeof(MonoScript), false) as MonoScript;
if (GUILayout.Button("Remove"))
{
Remove();
}
}
private void Remove()
{
if (_selectAssets == null || _selectAssets.Length == 0)
{
EditorUtility.DisplayDialog("Error", "Please Select A Folder Or Asset", "OK");
return;
}
if (_monoScript == null)
{
EditorUtility.DisplayDialog("Error", "Please Select A Mono Script", "OK");
return;
}
var prefabs = CommonToolUtil.GetSelectPrefabs(_key);
CommonToolUtil.ExecutePrefabs(prefabs, Execute, "Execute", true);
}
private bool Execute(GameObject prefab)
{
return CommonToolUtil.RemoveComponents(prefab, _monoScript);
}
}
}
GetSelectPrefabs()函数用来获得选中目录下的所有Prefabs,参数key用来过滤Prefab的名字,因为一个文件夹下可能有多个采用不同前后缀命名的Variant:
public static List<GameObject> GetSelectPrefabs(string key)
{
List<GameObject> list = new List<GameObject>();
Object[] selectedAssets = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
foreach (Object asset in selectedAssets)
{
string filePath = AssetDatabase.GetAssetPath(asset);
if (!filePath.EndsWith(".prefab"))
{
continue;
}
if (!string.IsNullOrEmpty(key))
{
var fileName = Path.GetFileNameWithoutExtension(filePath);
if (!fileName.Contains(key))
{
continue;
}
}
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(filePath);
if (prefab == null)
{
continue;
}
list.Add(prefab);
}
return list;
}
ExecutePrefabs函数遍历Prefabs,并显示进度条,有修改则保存
public static void ExecutePrefabs(List<GameObject> prefabs, Func<GameObject, bool> action, string content, bool save = true)
{
var count = prefabs.Count;
var index = 0;
foreach (var prefab in prefabs)
{
EditorUtility.DisplayProgressBar(content,
$"{content}...{++index}/{count}", (float) index / count);
bool result = action.Invoke(prefab);
if (save && result)
{
PrefabUtility.SavePrefabAsset(prefab);
}
}
if (save)
{
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
EditorUtility.ClearProgressBar();
}
RemoveComponents函数通过MonoScript获得Component数组,调用上面的方法移除组件
public static bool RemoveComponents(GameObject root, MonoScript monoScript)
{
if (root == null || monoScript == null)
{
return false;
}
var type = monoScript.GetClass();
if (type == null)
{
return false;
}
var components = root.GetComponentsInChildren(type);
return RemoveComponents(components);
}
运行效果如图:
可以选择目录或预制,设置关键词过滤,选择要删除的组件