Files
5.24BuYu/Assets/Framework/Tool/MissingScript/Editor/MissingScriptRemover.cs
T
2026-09-01 21:19:15 +08:00

309 lines
10 KiB
C#

using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public class MissingScriptRemover : EditorWindow
{
private readonly List<Object> targetObjects = new List<Object>();
private readonly List<string> prefabPaths = new List<string>();
private bool includeSubFolders = true;
private bool includeScene = false;
private bool dryRun = true;
private bool lastRunWasScan = true;
private readonly Dictionary<string, int> scanResults = new Dictionary<string, int>();
private int totalMissing;
private int fileCount;
private Vector2 scrollPos;
private bool hasScanned = false;
[MenuItem("Tools/预制体丢失脚本清理工具")]
public static void ShowWindow()
{
GetWindow<MissingScriptRemover>("丢失脚本清理");
}
private void OnGUI()
{
EditorGUILayout.HelpBox(
"拖入 Prefab 或文件夹 → 扫描 → 清理\n" +
"扫描模式只统计不修改;确认无误后取消勾选「仅扫描」再点清理", MessageType.Info);
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("目标(Prefab / 文件夹,可多选拖入)", EditorStyles.boldLabel);
Rect dropRect = GUILayoutUtility.GetRect(0, 40, GUILayout.ExpandWidth(true));
GUI.Box(dropRect, targetObjects.Count == 0
? "拖入 Prefab 或文件夹到此处"
: string.Join(", ", targetObjects.ConvertAll(o => o.name).ToArray()),
EditorStyles.helpBox);
Event evt = Event.current;
if ((evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) && dropRect.Contains(evt.mousePosition))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
if (DragAndDrop.objectReferences.Length > 0)
{
targetObjects.Clear();
targetObjects.AddRange(DragAndDrop.objectReferences);
hasScanned = false;
}
}
evt.Use();
}
if (GUILayout.Button("使用 Project 当前选中", GUILayout.Height(22)))
{
Object[] sel = Selection.objects;
if (sel != null && sel.Length > 0)
{
targetObjects.Clear();
targetObjects.AddRange(sel);
hasScanned = false;
}
else
{
EditorUtility.DisplayDialog("提示", "请先在 Project 窗口选中 Prefab 或文件夹", "确定");
}
}
EditorGUILayout.Space(6);
includeSubFolders = EditorGUILayout.ToggleLeft("包含子文件夹", includeSubFolders);
includeScene = EditorGUILayout.ToggleLeft("同时处理当前打开的场景", includeScene);
dryRun = EditorGUILayout.ToggleLeft("仅扫描(不修改任何文件)", dryRun);
EditorGUILayout.Space(6);
EditorGUILayout.BeginHorizontal();
GUI.enabled = targetObjects.Count > 0 || includeScene;
if (GUILayout.Button("扫描", GUILayout.Height(28)))
{
ScanOrClean(true);
}
GUI.enabled = true;
GUI.enabled = (targetObjects.Count > 0 || includeScene) && !dryRun;
string cleanLabel = dryRun ? "清理(已禁用:勾选了仅扫描)" : "清理";
if (GUILayout.Button(cleanLabel, GUILayout.Height(28)))
{
if (EditorUtility.DisplayDialog("确认清理",
$"将永久删除丢失的脚本组件(不可撤销,建议先提交或备份)。\n目标: {(targetObjects.Count > 0 ? string.Join(", ", targetObjects.ConvertAll(o => o.name).ToArray()) : "")}{(includeScene ? " + 当前场景" : "")}\n\n确认继续?",
"确认清理", "取消"))
{
ScanOrClean(false);
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
if (hasScanned)
{
string modeText = lastRunWasScan ? "[扫描结果]" : "[清理结果]";
EditorGUILayout.LabelField($"{modeText} 共 {fileCount} 个文件,丢失脚本组件 {totalMissing} 处", EditorStyles.boldLabel);
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
foreach (var kv in scanResults)
{
if (kv.Value > 0)
{
Color old = GUI.color;
GUI.color = new Color(1f, 0.75f, 0.45f);
EditorGUILayout.LabelField($"{kv.Key} ({kv.Value})");
GUI.color = old;
}
}
EditorGUILayout.EndScrollView();
if (scanResults.Count > 0 && GUILayout.Button("定位选中第一个文件"))
{
foreach (var kv in scanResults)
{
if (kv.Value > 0 && kv.Key.EndsWith(".prefab"))
{
var obj = AssetDatabase.LoadMainAssetAtPath(kv.Key);
if (obj != null)
{
Selection.activeObject = obj;
EditorGUIUtility.PingObject(obj);
}
break;
}
}
}
}
}
private void ScanOrClean(bool scanOnly)
{
lastRunWasScan = scanOnly;
prefabPaths.Clear();
prefabPaths.AddRange(CollectPrefabPaths(targetObjects.ToArray()));
scanResults.Clear();
totalMissing = 0;
fileCount = 0;
int sceneMissing = 0;
bool cancelled = false;
try
{
for (int i = 0; i < prefabPaths.Count; i++)
{
string path = prefabPaths[i];
if (EditorUtility.DisplayCancelableProgressBar(
scanOnly ? "扫描丢失脚本" : "清理丢失脚本",
path, (float)i / prefabPaths.Count))
{
cancelled = true;
break;
}
int removed = ProcessPrefab(path, !scanOnly);
scanResults[path] = removed;
totalMissing += removed;
fileCount++;
}
if (includeScene)
{
sceneMissing = ProcessOpenScene(!scanOnly);
if (sceneMissing > 0)
{
scanResults["[当前场景]"] = sceneMissing;
totalMissing += sceneMissing;
}
fileCount++;
}
}
finally
{
EditorUtility.ClearProgressBar();
}
if (!scanOnly && totalMissing > 0)
{
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
hasScanned = true;
string summary = cancelled
? $"已取消。处理到第 {fileCount} 个,共发现 {totalMissing} 处丢失脚本"
: (scanOnly
? $"扫描完成: {fileCount} 个文件,共 {totalMissing} 处丢失脚本"
: $"清理完成: 处理 {fileCount} 个文件,删除 {totalMissing} 个丢失脚本组件");
if (scanOnly && totalMissing > 0)
summary += "\n取消勾选「仅扫描」后点击「清理」即可删除";
Debug.Log($"[MissingScriptRemover] {summary}");
EditorUtility.DisplayDialog(scanOnly ? "扫描完成" : "清理完成", summary, "确定");
}
private IEnumerable<string> CollectPrefabPaths(Object[] objects)
{
var result = new List<string>();
foreach (var obj in objects)
{
if (obj == null) continue;
string path = AssetDatabase.GetAssetPath(obj);
if (string.IsNullOrEmpty(path)) continue;
if (Directory.Exists(path))
{
if (includeSubFolders)
{
foreach (string guid in AssetDatabase.FindAssets("t:Prefab", new[] { path }))
{
string p = AssetDatabase.GUIDToAssetPath(guid);
if (!result.Contains(p)) result.Add(p);
}
}
else
{
foreach (string file in Directory.GetFiles(path, "*.prefab"))
{
string p = file.Replace('\\', '/');
if (!result.Contains(p)) result.Add(p);
}
}
}
else if (path.EndsWith(".prefab", System.StringComparison.OrdinalIgnoreCase))
{
if (!result.Contains(path)) result.Add(path);
}
}
result.Sort();
return result;
}
private int ProcessPrefab(string path, bool clean)
{
GameObject root = PrefabUtility.LoadPrefabContents(path);
int total = 0;
try
{
total = ProcessGameObjectTree(root, clean);
if (clean && total > 0)
{
PrefabUtility.SaveAsPrefabAsset(root, path);
}
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
return total;
}
private int ProcessGameObjectTree(GameObject root, bool clean)
{
int total = 0;
total += HandleGameObject(root, clean);
Transform[] children = root.GetComponentsInChildren<Transform>(true);
foreach (Transform t in children)
{
if (t.gameObject == root) continue;
total += HandleGameObject(t.gameObject, clean);
}
return total;
}
private int HandleGameObject(GameObject go, bool clean)
{
if (clean)
{
return GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go);
}
int count = 0;
foreach (Component c in go.GetComponents<Component>())
{
if (c == null) count++;
}
return count;
}
private int ProcessOpenScene(bool clean)
{
int total = 0;
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
foreach (GameObject rootObj in scene.GetRootGameObjects())
{
total += ProcessGameObjectTree(rootObj, clean);
}
if (clean && total > 0)
{
EditorSceneManager.MarkSceneDirty(scene);
}
return total;
}
}