New file |
| | |
| | | using UnityEngine; |
| | | using UnityEditor; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Reflection; |
| | | using System; |
| | | |
| | | /// <summary> |
| | | /// 扫描 GameObject 中的无效脚本的编辑器工具 |
| | | /// </summary> |
| | | public class InvalidScriptScanner : EditorWindow |
| | | { |
| | | // 扫描模式枚举 |
| | | private enum ScanMode |
| | | { |
| | | Basic, // 基本扫描 |
| | | Advanced, // 高级扫描 |
| | | Deep // 深度扫描 |
| | | } |
| | | |
| | | // 当前扫描模式 |
| | | private static ScanMode currentScanMode = ScanMode.Advanced; |
| | | |
| | | /// <summary> |
| | | /// 添加右键菜单项 |
| | | /// </summary> |
| | | [MenuItem("GameObject/扫描无效脚本", false, 20)] |
| | | private static void ScanInvalidScripts() |
| | | { |
| | | GameObject selectedObject = Selection.activeGameObject; |
| | | if (selectedObject == null) |
| | | { |
| | | EditorUtility.DisplayDialog("错误", "请先选择一个游戏对象", "确定"); |
| | | return; |
| | | } |
| | | |
| | | // 使用改进的方法检测无效脚本 |
| | | List<int> invalidScriptIndices = FindInvalidScripts(selectedObject); |
| | | |
| | | if (invalidScriptIndices.Count > 0) |
| | | { |
| | | // 找到了无效脚本 |
| | | int invalidScriptIndex = invalidScriptIndices[0]; // 获取第一个无效脚本 |
| | | Debug.LogWarning($"在游戏对象 '{selectedObject.name}' 上发现无效脚本,位于组件索引 {invalidScriptIndex}"); |
| | | |
| | | // 高亮显示该对象 |
| | | Selection.activeGameObject = selectedObject; |
| | | |
| | | // 打开检查器并滚动到该组件 |
| | | EditorGUIUtility.PingObject(selectedObject); |
| | | |
| | | // 显示对话框 |
| | | if (EditorUtility.DisplayDialog("发现无效脚本", |
| | | $"在游戏对象 '{selectedObject.name}' 上发现无效脚本,位于组件索引 {invalidScriptIndex}。\n\n是否要移除该无效脚本?", |
| | | "移除", "取消")) |
| | | { |
| | | // 移除无效脚本 |
| | | RemoveInvalidScript(selectedObject, invalidScriptIndex); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 未找到无效脚本,尝试更深层次的扫描 |
| | | if (currentScanMode != ScanMode.Deep) |
| | | { |
| | | if (EditorUtility.DisplayDialog("未找到无效脚本", |
| | | $"在游戏对象 '{selectedObject.name}' 上未发现无效脚本。\n\n是否要尝试深度扫描?这可能需要更长时间。", |
| | | "深度扫描", "取消")) |
| | | { |
| | | // 临时切换到深度扫描模式 |
| | | ScanMode previousMode = currentScanMode; |
| | | currentScanMode = ScanMode.Deep; |
| | | |
| | | // 重新扫描 |
| | | invalidScriptIndices = FindInvalidScripts(selectedObject); |
| | | |
| | | // 恢复之前的扫描模式 |
| | | currentScanMode = previousMode; |
| | | |
| | | if (invalidScriptIndices.Count > 0) |
| | | { |
| | | // 找到了无效脚本 |
| | | int invalidScriptIndex = invalidScriptIndices[0]; |
| | | Debug.LogWarning($"深度扫描:在游戏对象 '{selectedObject.name}' 上发现无效脚本,位于组件索引 {invalidScriptIndex}"); |
| | | |
| | | // 显示对话框 |
| | | if (EditorUtility.DisplayDialog("深度扫描发现无效脚本", |
| | | $"在游戏对象 '{selectedObject.name}' 上发现无效脚本,位于组件索引 {invalidScriptIndex}。\n\n是否要移除该无效脚本?", |
| | | "移除", "取消")) |
| | | { |
| | | // 移除无效脚本 |
| | | RemoveInvalidScript(selectedObject, invalidScriptIndex); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 深度扫描也未找到无效脚本 |
| | | EditorUtility.DisplayDialog("深度扫描完成", $"在游戏对象 '{selectedObject.name}' 上未发现无效脚本。", "确定"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 用户取消深度扫描 |
| | | EditorUtility.DisplayDialog("扫描完成", $"在游戏对象 '{selectedObject.name}' 上未发现无效脚本。", "确定"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 已经是深度扫描模式 |
| | | EditorUtility.DisplayDialog("扫描完成", $"在游戏对象 '{selectedObject.name}' 上未发现无效脚本。", "确定"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 查找无效脚本 |
| | | /// </summary> |
| | | /// <param name="gameObject">要检查的游戏对象</param> |
| | | /// <returns>无效脚本的索引列表</returns> |
| | | private static List<int> FindInvalidScripts(GameObject gameObject) |
| | | { |
| | | List<int> invalidIndices = new List<int>(); |
| | | |
| | | // 检查游戏对象是否有效 |
| | | if (gameObject == null) |
| | | return invalidIndices; |
| | | |
| | | // 方法1:使用 SerializedObject 检查组件 |
| | | SerializedObject serializedObject = new SerializedObject(gameObject); |
| | | SerializedProperty componentsProperty = serializedObject.FindProperty("m_Component"); |
| | | |
| | | for (int i = 0; i < componentsProperty.arraySize; i++) |
| | | { |
| | | SerializedProperty componentProperty = componentsProperty.GetArrayElementAtIndex(i); |
| | | SerializedProperty scriptProperty = componentProperty.FindPropertyRelative("component"); |
| | | |
| | | // 检查脚本引用是否有效 |
| | | if (scriptProperty.objectReferenceValue == null || scriptProperty.objectReferenceInstanceIDValue == 0) |
| | | { |
| | | invalidIndices.Add(i); |
| | | continue; |
| | | } |
| | | |
| | | // 额外检查:尝试获取实际组件并检查其类型 |
| | | UnityEngine.Object scriptObject = scriptProperty.objectReferenceValue; |
| | | if (scriptObject != null) |
| | | { |
| | | // 检查脚本是否为缺失或损坏的 MonoScript |
| | | MonoScript monoScript = scriptObject as MonoScript; |
| | | if (monoScript != null && monoScript.GetClass() == null) |
| | | { |
| | | invalidIndices.Add(i); |
| | | continue; |
| | | } |
| | | |
| | | // 高级扫描:检查组件是否可以正确序列化 |
| | | if (currentScanMode >= ScanMode.Advanced) |
| | | { |
| | | try |
| | | { |
| | | // 尝试获取组件的类型 |
| | | Type componentType = scriptObject.GetType(); |
| | | if (componentType == null || componentType.ToString().Contains("Missing") || componentType.ToString().Contains("missing")) |
| | | { |
| | | invalidIndices.Add(i); |
| | | continue; |
| | | } |
| | | } |
| | | catch (Exception) |
| | | { |
| | | // 如果获取类型时出现异常,可能是无效脚本 |
| | | invalidIndices.Add(i); |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 方法2:使用 GetComponents 检查组件(可能会检测到一些方法1漏掉的情况) |
| | | Component[] components = gameObject.GetComponents<Component>(); |
| | | for (int i = 0; i < components.Length; i++) |
| | | { |
| | | if (components[i] == null) |
| | | { |
| | | // 检查这个索引是否已经在列表中 |
| | | if (!invalidIndices.Contains(i)) |
| | | { |
| | | invalidIndices.Add(i); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | // 深度扫描:尝试访问组件的一些属性,看是否会抛出异常 |
| | | if (currentScanMode == ScanMode.Deep) |
| | | { |
| | | try |
| | | { |
| | | // 尝试获取组件的名称和启用状态 |
| | | string name = components[i].name; |
| | | bool enabled = false; |
| | | |
| | | // 尝试获取启用状态(只对 Behaviour 类型有效) |
| | | if (components[i] is Behaviour behaviour) |
| | | { |
| | | enabled = behaviour.enabled; |
| | | } |
| | | |
| | | // 尝试序列化组件 |
| | | SerializedObject testSerializedObject = new SerializedObject(components[i]); |
| | | SerializedProperty iterator = testSerializedObject.GetIterator(); |
| | | bool hasProperties = iterator.NextVisible(true); |
| | | |
| | | // 如果无法获取任何属性,可能是无效脚本 |
| | | if (!hasProperties && !invalidIndices.Contains(i)) |
| | | { |
| | | invalidIndices.Add(i); |
| | | } |
| | | } |
| | | catch (Exception) |
| | | { |
| | | // 如果访问组件属性时出现异常,可能是无效脚本 |
| | | if (!invalidIndices.Contains(i)) |
| | | { |
| | | invalidIndices.Add(i); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 检查隐藏的游戏对象(即使是隐藏的,也可能包含无效脚本) |
| | | if (currentScanMode >= ScanMode.Advanced) |
| | | { |
| | | // 获取所有隐藏的子对象 |
| | | Transform[] allChildren = gameObject.GetComponentsInChildren<Transform>(true); |
| | | foreach (Transform child in allChildren) |
| | | { |
| | | // 跳过当前对象(因为已经检查过了) |
| | | if (child.gameObject == gameObject) |
| | | continue; |
| | | |
| | | // 如果子对象是隐藏的或者父对象是隐藏的,单独检查它 |
| | | if (!child.gameObject.activeInHierarchy) |
| | | { |
| | | // 递归检查隐藏的子对象 |
| | | List<int> childInvalidIndices = FindInvalidScriptsInHiddenObject(child.gameObject); |
| | | if (childInvalidIndices.Count > 0) |
| | | { |
| | | // 如果在隐藏的子对象中找到了无效脚本,添加到结果中 |
| | | // 注意:这里我们只是记录了索引,实际上需要在UI中显示完整路径 |
| | | foreach (int index in childInvalidIndices) |
| | | { |
| | | if (!invalidIndices.Contains(index)) |
| | | { |
| | | invalidIndices.Add(index); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | return invalidIndices; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 在隐藏的游戏对象中查找无效脚本 |
| | | /// </summary> |
| | | private static List<int> FindInvalidScriptsInHiddenObject(GameObject gameObject) |
| | | { |
| | | List<int> invalidIndices = new List<int>(); |
| | | |
| | | // 检查游戏对象是否有效 |
| | | if (gameObject == null) |
| | | return invalidIndices; |
| | | |
| | | // 使用 SerializedObject 检查组件 |
| | | SerializedObject serializedObject = new SerializedObject(gameObject); |
| | | SerializedProperty componentsProperty = serializedObject.FindProperty("m_Component"); |
| | | |
| | | for (int i = 0; i < componentsProperty.arraySize; i++) |
| | | { |
| | | SerializedProperty componentProperty = componentsProperty.GetArrayElementAtIndex(i); |
| | | SerializedProperty scriptProperty = componentProperty.FindPropertyRelative("component"); |
| | | |
| | | // 检查脚本引用是否有效 |
| | | if (scriptProperty.objectReferenceValue == null || scriptProperty.objectReferenceInstanceIDValue == 0) |
| | | { |
| | | invalidIndices.Add(i); |
| | | } |
| | | } |
| | | |
| | | return invalidIndices; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 移除无效脚本 |
| | | /// </summary> |
| | | private static void RemoveInvalidScript(GameObject gameObject, int componentIndex) |
| | | { |
| | | SerializedObject serializedObject = new SerializedObject(gameObject); |
| | | SerializedProperty componentsProperty = serializedObject.FindProperty("m_Component"); |
| | | |
| | | if (componentIndex >= 0 && componentIndex < componentsProperty.arraySize) |
| | | { |
| | | componentsProperty.DeleteArrayElementAtIndex(componentIndex); |
| | | serializedObject.ApplyModifiedProperties(); |
| | | Debug.Log($"已从游戏对象 '{gameObject.name}' 移除无效脚本"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 递归扫描所有子对象中的无效脚本 |
| | | /// </summary> |
| | | [MenuItem("GameObject/递归扫描所有无效脚本", false, 21)] |
| | | private static void ScanAllInvalidScriptsRecursively() |
| | | { |
| | | GameObject selectedObject = Selection.activeGameObject; |
| | | if (selectedObject == null) |
| | | { |
| | | EditorUtility.DisplayDialog("错误", "请先选择一个游戏对象", "确定"); |
| | | return; |
| | | } |
| | | |
| | | List<KeyValuePair<GameObject, int>> invalidScripts = new List<KeyValuePair<GameObject, int>>(); |
| | | ScanInvalidScriptsInChildren(selectedObject, invalidScripts); |
| | | |
| | | if (invalidScripts.Count > 0) |
| | | { |
| | | // 找到了无效脚本 |
| | | Debug.LogWarning($"在游戏对象 '{selectedObject.name}' 及其子对象中发现 {invalidScripts.Count} 个无效脚本"); |
| | | |
| | | // 显示结果窗口 |
| | | InvalidScriptResultWindow.ShowWindow(invalidScripts); |
| | | } |
| | | else |
| | | { |
| | | // 未找到无效脚本,尝试更深层次的扫描 |
| | | if (currentScanMode != ScanMode.Deep) |
| | | { |
| | | if (EditorUtility.DisplayDialog("未找到无效脚本", |
| | | $"在游戏对象 '{selectedObject.name}' 及其子对象中未发现无效脚本。\n\n是否要尝试深度扫描?这可能需要更长时间。", |
| | | "深度扫描", "取消")) |
| | | { |
| | | // 临时切换到深度扫描模式 |
| | | ScanMode previousMode = currentScanMode; |
| | | currentScanMode = ScanMode.Deep; |
| | | |
| | | // 重新扫描 |
| | | invalidScripts.Clear(); |
| | | ScanInvalidScriptsInChildren(selectedObject, invalidScripts); |
| | | |
| | | // 恢复之前的扫描模式 |
| | | currentScanMode = previousMode; |
| | | |
| | | if (invalidScripts.Count > 0) |
| | | { |
| | | // 找到了无效脚本 |
| | | Debug.LogWarning($"深度扫描:在游戏对象 '{selectedObject.name}' 及其子对象中发现 {invalidScripts.Count} 个无效脚本"); |
| | | |
| | | // 显示结果窗口 |
| | | InvalidScriptResultWindow.ShowWindow(invalidScripts); |
| | | } |
| | | else |
| | | { |
| | | // 深度扫描也未找到无效脚本 |
| | | EditorUtility.DisplayDialog("深度扫描完成", $"在游戏对象 '{selectedObject.name}' 及其子对象中未发现无效脚本。", "确定"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 用户取消深度扫描 |
| | | EditorUtility.DisplayDialog("扫描完成", $"在游戏对象 '{selectedObject.name}' 及其子对象中未发现无效脚本。", "确定"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 已经是深度扫描模式 |
| | | EditorUtility.DisplayDialog("扫描完成", $"在游戏对象 '{selectedObject.name}' 及其子对象中未发现无效脚本。", "确定"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 递归扫描子对象 |
| | | /// </summary> |
| | | private static void ScanInvalidScriptsInChildren(GameObject gameObject, List<KeyValuePair<GameObject, int>> results) |
| | | { |
| | | // 使用改进的方法检测无效脚本 |
| | | List<int> invalidIndices = FindInvalidScripts(gameObject); |
| | | |
| | | // 添加到结果列表 |
| | | foreach (int index in invalidIndices) |
| | | { |
| | | results.Add(new KeyValuePair<GameObject, int>(gameObject, index)); |
| | | } |
| | | |
| | | // 递归检查所有子对象(包括隐藏的) |
| | | foreach (Transform child in gameObject.transform) |
| | | { |
| | | ScanInvalidScriptsInChildren(child.gameObject, results); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 添加设置菜单项 |
| | | /// </summary> |
| | | [MenuItem("GameObject/无效脚本扫描设置", false, 22)] |
| | | private static void ShowSettings() |
| | | { |
| | | // 创建设置窗口 |
| | | InvalidScriptScannerSettings.ShowWindow(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 无效脚本扫描设置窗口 |
| | | /// </summary> |
| | | public class InvalidScriptScannerSettings : EditorWindow |
| | | { |
| | | private static string[] scanModeNames = { "基本扫描", "高级扫描", "深度扫描" }; |
| | | |
| | | public static void ShowWindow() |
| | | { |
| | | InvalidScriptScannerSettings window = GetWindow<InvalidScriptScannerSettings>("无效脚本扫描设置"); |
| | | window.minSize = new Vector2(300, 150); |
| | | window.Show(); |
| | | } |
| | | |
| | | private void OnGUI() |
| | | { |
| | | EditorGUILayout.LabelField("扫描设置", EditorStyles.boldLabel); |
| | | EditorGUILayout.Space(); |
| | | |
| | | // 获取当前扫描模式 |
| | | int currentMode = (int)typeof(InvalidScriptScanner).GetField("currentScanMode", |
| | | BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); |
| | | |
| | | // 显示扫描模式选择 |
| | | int newMode = EditorGUILayout.Popup("扫描模式", currentMode, scanModeNames); |
| | | |
| | | // 如果模式发生变化,更新设置 |
| | | if (newMode != currentMode) |
| | | { |
| | | typeof(InvalidScriptScanner).GetField("currentScanMode", |
| | | BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, newMode); |
| | | } |
| | | |
| | | EditorGUILayout.Space(); |
| | | EditorGUILayout.HelpBox("扫描模式说明:\n" + |
| | | "基本扫描:只检查明显的无效脚本\n" + |
| | | "高级扫描:检查更多类型的无效脚本,包括隐藏对象\n" + |
| | | "深度扫描:进行最彻底的检查,但可能较慢", MessageType.Info); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 无效脚本结果窗口 |
| | | /// </summary> |
| | | public class InvalidScriptResultWindow : EditorWindow |
| | | { |
| | | private List<KeyValuePair<GameObject, int>> invalidScripts; |
| | | private Vector2 scrollPosition; |
| | | |
| | | public static void ShowWindow(List<KeyValuePair<GameObject, int>> scripts) |
| | | { |
| | | InvalidScriptResultWindow window = GetWindow<InvalidScriptResultWindow>("无效脚本扫描结果"); |
| | | window.invalidScripts = scripts; |
| | | window.minSize = new Vector2(400, 300); |
| | | window.Show(); |
| | | } |
| | | |
| | | private void OnGUI() |
| | | { |
| | | if (invalidScripts == null || invalidScripts.Count == 0) |
| | | { |
| | | EditorGUILayout.LabelField("没有发现无效脚本"); |
| | | return; |
| | | } |
| | | |
| | | EditorGUILayout.LabelField($"发现 {invalidScripts.Count} 个无效脚本", EditorStyles.boldLabel); |
| | | EditorGUILayout.Space(); |
| | | |
| | | if (GUILayout.Button("移除所有无效脚本")) |
| | | { |
| | | if (EditorUtility.DisplayDialog("确认", "确定要移除所有无效脚本吗?", "确定", "取消")) |
| | | { |
| | | RemoveAllInvalidScripts(); |
| | | } |
| | | } |
| | | |
| | | EditorGUILayout.Space(); |
| | | scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); |
| | | |
| | | for (int i = 0; i < invalidScripts.Count; i++) |
| | | { |
| | | EditorGUILayout.BeginHorizontal(); |
| | | |
| | | if (GUILayout.Button("选择", GUILayout.Width(60))) |
| | | { |
| | | Selection.activeGameObject = invalidScripts[i].Key; |
| | | EditorGUIUtility.PingObject(invalidScripts[i].Key); |
| | | } |
| | | |
| | | EditorGUILayout.ObjectField(invalidScripts[i].Key, typeof(GameObject), true); |
| | | |
| | | EditorGUILayout.LabelField($"组件索引: {invalidScripts[i].Value}", GUILayout.Width(100)); |
| | | |
| | | if (GUILayout.Button("移除", GUILayout.Width(60))) |
| | | { |
| | | RemoveInvalidScript(invalidScripts[i].Key, invalidScripts[i].Value); |
| | | invalidScripts.RemoveAt(i); |
| | | i--; |
| | | GUIUtility.ExitGUI(); |
| | | } |
| | | |
| | | EditorGUILayout.EndHorizontal(); |
| | | } |
| | | |
| | | EditorGUILayout.EndScrollView(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 移除无效脚本 |
| | | /// </summary> |
| | | private void RemoveInvalidScript(GameObject gameObject, int componentIndex) |
| | | { |
| | | SerializedObject serializedObject = new SerializedObject(gameObject); |
| | | SerializedProperty componentsProperty = serializedObject.FindProperty("m_Component"); |
| | | |
| | | if (componentIndex >= 0 && componentIndex < componentsProperty.arraySize) |
| | | { |
| | | componentsProperty.DeleteArrayElementAtIndex(componentIndex); |
| | | serializedObject.ApplyModifiedProperties(); |
| | | Debug.Log($"已从游戏对象 '{gameObject.name}' 移除无效脚本"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 移除所有无效脚本 |
| | | /// </summary> |
| | | private void RemoveAllInvalidScripts() |
| | | { |
| | | // 创建一个副本,因为我们会在循环中修改原始列表 |
| | | var scripts = new List<KeyValuePair<GameObject, int>>(invalidScripts); |
| | | |
| | | // 按照组件索引从大到小排序,这样删除时不会影响其他组件的索引 |
| | | scripts = scripts.OrderByDescending(pair => pair.Value) |
| | | .GroupBy(pair => pair.Key) |
| | | .SelectMany(group => group.OrderByDescending(pair => pair.Value)) |
| | | .ToList(); |
| | | |
| | | foreach (var pair in scripts) |
| | | { |
| | | RemoveInvalidScript(pair.Key, pair.Value); |
| | | } |
| | | |
| | | invalidScripts.Clear(); |
| | | Repaint(); |
| | | } |
| | | } |