309 lines
10 KiB
C#
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;
|
|
}
|
|
}
|