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();
|
}
|
}
|