diff --git a/unity/Assets/Editor/FullScreenCoverTool.cs b/unity/Assets/Editor/FullScreenCoverTool.cs new file mode 100644 index 00000000..ca2a1e59 --- /dev/null +++ b/unity/Assets/Editor/FullScreenCoverTool.cs @@ -0,0 +1,316 @@ +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; + } + }); + } +} diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs new file mode 100644 index 00000000..48f6aaf3 --- /dev/null +++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +namespace MFrame +{ + /// + /// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。 + /// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall), + /// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。 + /// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。 + /// + public class FullScreenCover : MonoBehaviour + { + [Tooltip("勾选后:本面板被上层全屏面板遮挡时仍保持显示(仅用于必须常驻可见的特殊面板)。")] + public bool isKeepShowWhenCovered = false; + } +} diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs index 98c6d79f..657a4024 100644 --- a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs +++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEngine; namespace MFrame @@ -56,6 +57,54 @@ namespace MFrame return _autoDestroy; } + // 全屏遮挡以组件形式配置:在面板预制体根节点挂 FullScreenCover 组件即生效(策划可自助添加/移除) + /// + /// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板) + /// + private FullScreenCover _fullScreenCover; + + /// + /// 是否正被上层全屏面板隐藏 + /// + private bool _hiddenByCover; + /// + /// 被本面板(全屏遮挡)隐藏的下层面板列表 + /// + private readonly List _panelsHiddenByMe = new List(); + + // —— 全屏遮挡:IPanelBase 接口实现 —— + UILayerIdx IPanelBase.panelLayerIdx => _layerIdx; + bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered; + bool IPanelBase.isHiddenByCover => _hiddenByCover; + + /// + /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall) + /// + public void SetCoverHidden() + { + _hiddenByCover = true; + if (_view != null) + { + _view.gameObject.SetActive(false); + } + } + + /// + /// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过) + /// + public void RestoreFromCover() + { + if (!_hiddenByCover) + { + return; + } + _hiddenByCover = false; + if (_view != null) + { + _view.gameObject.SetActive(true); + } + } + /// /// 尝试异步加载UI预制体 @@ -101,6 +150,8 @@ namespace MFrame transform = go.transform; // 获取视图组件并完成初始化 _view = transform.GetComponent(); + // 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null) + _fullScreenCover = transform.GetComponent(); _view.AddUpdateHandler(this.Update); _OnLoadComplete(); Show(data, completeHandler); @@ -150,8 +201,22 @@ namespace MFrame { _data = data; if (this._view != null) + { + // 若此前被上层全屏面板隐藏,主动 Show 视为恢复显示 + //(隐藏者列表中的遗留记录由 RestorePanelsHiddenByMe 的 isHiddenByCover 守卫失效,无需清理) + if (_hiddenByCover) + { + RestoreFromCover(); + } this._view.gameObject.SetActive(true); + } _OnShow(); + // 全屏遮挡面板(预制体根节点挂 FullScreenCover 组件且处于启用状态): + // 打开时隐藏下层正在显示的面板,省去被盖住的 DrawCall + if (_fullScreenCover != null && _fullScreenCover.enabled) + { + HidePanelsUnderThis(); + } _view.Show(() => { completeHandler?.Invoke(this); @@ -168,7 +233,8 @@ namespace MFrame { return false; } - return _view.gameObject.activeSelf; + // 被上层全屏面板隐藏时仍视为“打开中”,保持业务语义不变 + return _view.gameObject.activeSelf || _hiddenByCover; } public bool isLoading() { @@ -193,6 +259,8 @@ namespace MFrame { _view.Close(() => { + // 关闭完成后恢复被本面板隐藏的下层面板 + RestorePanelsHiddenByMe(); _OnClose(); complete?.Invoke(); }); @@ -204,6 +272,53 @@ namespace MFrame } + /// + /// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板, + /// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。 + /// + private void HidePanelsUnderThis() + { + Dictionary items = PanelMgr.Instance.items; + if (items == null) + { + return; + } + foreach (KeyValuePair kvp in items) + { + PanelMsgItem item = kvp.Value; + if (item.panel == null || ReferenceEquals(item.panel, this)) + { + continue; + } + IPanelBase panel = item.panel; + if (panel.panelLayerIdx < _layerIdx + && !panel.isKeepShowWhenCovered + && !panel.isHiddenByCover + && panel.isShowing()) + { + _panelsHiddenByMe.Add(panel); + panel.SetCoverHidden(); + } + } + } + + /// + /// 恢复被本面板隐藏的下层面板(幂等,可重复调用) + /// + private void RestorePanelsHiddenByMe() + { + int len = _panelsHiddenByMe.Count; + for (int i = 0; i < len; i++) + { + IPanelBase panel = _panelsHiddenByMe[i]; + if (panel != null && panel.isHiddenByCover) + { + panel.RestoreFromCover(); + } + } + _panelsHiddenByMe.Clear(); + } + /// @@ -211,8 +326,8 @@ namespace MFrame /// protected override void _Destroy() { - - + // 兼容未走 Close 直接销毁的路径,确保下层面板一定被恢复 + RestorePanelsHiddenByMe(); base._Destroy(); @@ -221,6 +336,7 @@ namespace MFrame // 清除面板数据 _data = null; + _fullScreenCover = null; if (_view != null) { _view.RemoveUpdateHandler(this.Update); diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs index c0a2283c..48cd4d14 100644 --- a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs +++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs @@ -31,7 +31,30 @@ namespace MFrame /// bool isLoading(); + /// + /// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板) + /// + UILayerIdx panelLayerIdx { get; } + /// + /// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true) + /// + bool isKeepShowWhenCovered { get; } + + /// + /// 是否正被上层全屏面板隐藏 + /// + bool isHiddenByCover { get; } + + /// + /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall + /// + void SetCoverHidden(); + + /// + /// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等) + /// + void RestoreFromCover(); } }