using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; public class MissingScriptRemover : EditorWindow { private readonly List targetObjects = new List(); private readonly List prefabPaths = new List(); private bool includeSubFolders = true; private bool includeScene = false; private bool dryRun = true; private bool lastRunWasScan = true; private readonly Dictionary scanResults = new Dictionary(); private int totalMissing; private int fileCount; private Vector2 scrollPos; private bool hasScanned = false; [MenuItem("Tools/预制体丢失脚本清理工具")] public static void ShowWindow() { GetWindow("丢失脚本清理"); } 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 CollectPrefabPaths(Object[] objects) { var result = new List(); 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(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()) { 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; } }