using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using MFrame; using Object = UnityEngine.Object; /// /// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。 /// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。 /// 策划可在此窗口自助管理,无需改代码。 /// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。 /// public class FullScreenCoverTool : EditorWindow { private sealed class CoverInfo { public Object prefab; // 预制体资产引用(用于 Ping/打开) public string prefabPath; // 资源路径 public string nodeName; // 组件所在节点名 public bool enabled; // 组件是否启用 public bool keepShow; // 被遮挡时保持显示 } private readonly List results = new List(); private Vector2 scroll; private GameObject addTarget; private bool scanned; private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体"; [MenuItem("Tools/UI/全屏面板遮挡工具")] private static void ShowWindow() { var win = GetWindow("全屏面板遮挡"); win.minSize = new Vector2(540f, 320f); } private void OnEnable() { // 打开窗口时自动扫描一次 EditorApplication.delayCall += ScanAll; } private void OnGUI() { EditorGUILayout.HelpBox( "挂有 FullScreenCover 组件的面板 = 全屏面板:打开时自动隐藏下层 UI(省去被遮住的 DrawCall),关闭/销毁自动恢复。\n" + "注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。", MessageType.Info); // —— 添加区 —— EditorGUILayout.LabelField("添加组件(拖入面板预制体)", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { addTarget = (GameObject)EditorGUILayout.ObjectField(addTarget, typeof(GameObject), false); using (new EditorGUI.DisabledScope(addTarget == null)) { if (GUILayout.Button("添加组件", GUILayout.Width(80f))) { AddComponentToPrefab(AssetDatabase.GetAssetPath(addTarget)); addTarget = null; } } } EditorGUILayout.Space(4f); // —— 扫描区 —— using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("扫描全工程", GUILayout.Height(24f))) { ScanAll(); } GUILayout.Label(statusText, EditorStyles.miniLabel); } EditorGUILayout.Space(4f); // —— 结果列表 —— if (!scanned) { return; } if (results.Count == 0) { EditorGUILayout.HelpBox("没有找到任何挂载 FullScreenCover 的面板。可用上方“添加组件”为全屏面板配置。", MessageType.Warning); return; } scroll = EditorGUILayout.BeginScrollView(scroll); for (int i = 0; i < results.Count; i++) { DrawResultItem(results[i]); } EditorGUILayout.EndScrollView(); } private void DrawResultItem(CoverInfo info) { using (new EditorGUILayout.VerticalScope("box")) { using (new EditorGUILayout.HorizontalScope()) { // 点击名称定位资源 string title = info.prefab != null ? info.prefab.name : Path.GetFileName(info.prefabPath); if (GUILayout.Button(title, EditorStyles.boldLabel, GUILayout.Width(170f))) { EditorGUIUtility.PingObject(info.prefab); } GUILayout.Label(info.enabled ? "已启用" : "已禁用", info.enabled ? EditorStyles.miniLabel : EditorStyles.whiteMiniLabel, GUILayout.Width(46f)); GUILayout.FlexibleSpace(); // 被遮挡时保持显示(勾选即写回预制体) bool newKeep = GUILayout.Toggle(info.keepShow, "被遮挡时保持显示", GUILayout.Width(132f)); if (newKeep != info.keepShow) { info.keepShow = newKeep; SetKeepShow(info.prefabPath, newKeep); } if (GUILayout.Button("打开", GUILayout.Width(46f))) { AssetDatabase.OpenAsset(info.prefab); } if (GUILayout.Button("移除组件", GUILayout.Width(68f))) { if (EditorUtility.DisplayDialog("移除 FullScreenCover", $"确定从 {Path.GetFileName(info.prefabPath)} 移除 FullScreenCover 组件?\n该面板将不再自动隐藏下层。", "移除", "取消")) { RemoveComponent(info.prefabPath); ScanAll(); } } } EditorGUILayout.LabelField(info.prefabPath, EditorStyles.miniLabel); if (!info.enabled) { EditorGUILayout.LabelField("⚠ 组件处于禁用状态,不会生效(在预制体根节点 Inspector 勾选启用)。", EditorStyles.miniLabel); } } } // —— 扫描 —— private void ScanAll() { string scriptGuid = GetScriptGuid(); if (string.IsNullOrEmpty(scriptGuid)) { EditorUtility.DisplayDialog("错误", "无法获取 FullScreenCover 脚本 GUID,请确认脚本已编译无报错。", "确定"); return; } string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" }); results.Clear(); try { for (int i = 0; i < prefabGuids.Length; i++) { if (EditorUtility.DisplayCancelableProgressBar("全屏面板遮挡工具", $"扫描预制体中... ({i + 1}/{prefabGuids.Length})", (float)i / prefabGuids.Length)) { break; // 用户取消 } string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]); if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab")) { continue; } // 文本快筛:只有包含组件脚本 guid 的预制体才真正加载,避免逐个解析 if (!ContainsGuidFast(path, scriptGuid)) { continue; } GameObject prefab = AssetDatabase.LoadAssetAtPath(path); if (prefab == null) { continue; } FullScreenCover[] comps = prefab.GetComponentsInChildren(true); foreach (FullScreenCover comp in comps) { results.Add(new CoverInfo { prefab = prefab, prefabPath = path, nodeName = comp.gameObject.name, enabled = comp.enabled, keepShow = comp.isKeepShowWhenCovered }); } } } finally { EditorUtility.ClearProgressBar(); } scanned = true; statusText = $"扫描完成:{results.Count} 个面板预制体挂载 FullScreenCover"; Repaint(); } /// 动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。 private static string GetScriptGuid() { GameObject probe = new GameObject("FullScreenCoverGuidProbe"); try { MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent()); if (ms == null) { return null; } return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms)); } finally { Object.DestroyImmediate(probe); } } /// 逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。 private static bool ContainsGuidFast(string path, string guid) { try { foreach (string line in File.ReadLines(path)) { if (line.Contains(guid)) { return true; } } } catch (IOException) { // 文件被占用等情况按未命中处理 } return false; } // —— 修改预制体 —— /// 以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。 private static void EditPrefab(string path, System.Action edit) { GameObject root = PrefabUtility.LoadPrefabContents(path); try { edit(root); PrefabUtility.SaveAsPrefabAsset(root, path); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); } private void AddComponentToPrefab(string path) { if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab")) { EditorUtility.DisplayDialog("无法添加", "请拖入一个预制体资产(.prefab),而不是场景对象或模型。", "确定"); return; } bool already = false; EditPrefab(path, root => { if (root.GetComponent() != null) { already = true; } else { root.AddComponent(); } }); ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover")); ScanAll(); } private void RemoveComponent(string path) { EditPrefab(path, root => { FullScreenCover[] comps = root.GetComponentsInChildren(true); foreach (FullScreenCover comp in comps) { Object.DestroyImmediate(comp, true); } }); } private void SetKeepShow(string path, bool value) { EditPrefab(path, root => { FullScreenCover[] comps = root.GetComponentsInChildren(true); foreach (FullScreenCover comp in comps) { comp.isKeepShowWhenCovered = value; } }); } }