This commit is contained in:
ShallowT1Dream
2026-09-10 17:29:24 +08:00
parent 3071ec4b8f
commit 80aa4f0436
20 changed files with 32618 additions and 0 deletions
@@ -0,0 +1,16 @@
using UnityEngine;
namespace MFrame
{
/// <summary>
/// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。
/// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall),
/// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。
/// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。
/// </summary>
public class FullScreenCover : MonoBehaviour
{
[Tooltip("勾选后:本面板被上层全屏面板遮挡时仍保持显示(仅用于必须常驻可见的特殊面板)。")]
public bool isKeepShowWhenCovered = false;
}
}
@@ -0,0 +1,316 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using MFrame;
using Object = UnityEngine.Object;
/// <summary>
/// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。
/// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。
/// 策划可在此窗口自助管理,无需改代码。
/// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。
/// </summary>
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<CoverInfo> results = new List<CoverInfo>();
private Vector2 scroll;
private GameObject addTarget;
private bool scanned;
private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体";
[MenuItem("Tools/UI/全屏面板遮挡工具")]
private static void ShowWindow()
{
var win = GetWindow<FullScreenCoverTool>("全屏面板遮挡");
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<GameObject>(path);
if (prefab == null)
{
continue;
}
FullScreenCover[] comps = prefab.GetComponentsInChildren<FullScreenCover>(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();
}
/// <summary>动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。</summary>
private static string GetScriptGuid()
{
GameObject probe = new GameObject("FullScreenCoverGuidProbe");
try
{
MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent<FullScreenCover>());
if (ms == null)
{
return null;
}
return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms));
}
finally
{
Object.DestroyImmediate(probe);
}
}
/// <summary>逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。</summary>
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;
}
// —— 修改预制体 ——
/// <summary>以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。</summary>
private static void EditPrefab(string path, System.Action<GameObject> 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<FullScreenCover>() != null)
{
already = true;
}
else
{
root.AddComponent<FullScreenCover>();
}
});
ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover"));
ScanAll();
}
private void RemoveComponent(string path)
{
EditPrefab(path, root =>
{
FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
foreach (FullScreenCover comp in comps)
{
Object.DestroyImmediate(comp, true);
}
});
}
private void SetKeepShow(string path, bool value)
{
EditPrefab(path, root =>
{
FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
foreach (FullScreenCover comp in comps)
{
comp.isKeepShowWhenCovered = value;
}
});
}
}
@@ -0,0 +1,398 @@
# FullScreenCover 全屏遮挡省 DC — 使用方法与避坑
> 源文件:`my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs`(组件,17 行)
> 配套工具:`Assets/Editor/FullScreenCoverTool.cs`(菜单 `Tools/UI/全屏面板遮挡工具`)
> 验证环境:Unity 团结引擎(Tuanjie)+ MFrame 框架 + URP
> **一句话:全屏面板打开时,把它下面那些"反正被盖住看不见"的界面 `SetActive(false)`,这些界面就不再提交 DrawCall;全屏面板关闭时自动恢复。业务代码零改动,策划在编辑器里挂个组件就能配。**
---
## 第一章 它到底省了多少(T5 实测数据)
| 项目 | 优化前 | 优化后 |
| --- | --- | --- |
| 大厅整帧批次数 | 约 100 | **35** |
| 其中 SSAO 占用 | 4 个 DC | 0(见 `03_渲染设置与资源清理/`) |
| LevelGiftPanel 打开时 | 自身 101 批 | 见 `01_UIEffect材质共享降DC/` |
遮挡机制本身的收益验证方式很直观:**打开全屏面板时,去 Hierarchy 里看 MainPanel / HomePanel 是不是变灰了**(变灰 = `SetActive(false)` = 不再产生 DC)。这是用户在 T5 上亲手确认过的。
---
## 第二章 5 分钟配置(策划操作,不用碰代码)
### 方式 A:用编辑器工具(推荐)
菜单:`Tools/UI/全屏面板遮挡工具`
窗口打开时会**自动扫描全工程**,列出所有已挂组件的面板。三个功能区:
| 操作 | 怎么做 |
| --- | --- |
| **给新面板加遮挡** | 把面板预制体拖进上方"添加组件"槽 → 点【添加组件】 |
| **改"被遮挡时保持显示"** | 在结果列表里直接勾/取消那个复选框,**勾完立刻写回预制体**,不用再点保存 |
| **取消某个面板的遮挡** | 点该行【移除组件】→ 二次确认弹窗 → 移除 |
| **定位预制体** | 点面板名字 = Ping 到 Project 窗口;点【打开】= 进预制体编辑模式 |
列表还会告诉你每个面板的**组件启用状态**。如果显示"已禁用",说明组件被人在 Inspector 里取消勾选了,这时**不会生效**,窗口会用黄字提醒你。
### 方式 B:手动挂(工具不在手边时)
1. 打开面板预制体,选中**根节点**(必须和 `UI_PanelBase` 在同一个节点上);
2. Inspector → Add Component → 搜 `FullScreenCover`;
3. 完事。不需要写任何代码,也不需要注册。
> ⚠️ **必须挂在根节点**。框架是在 `_TryInitGameObject` 里用 `transform.GetComponent<FullScreenCover>()` 找的(非递归),挂在子节点上**完全无效**且不会报错——这是最容易踩的空坑。
### `isKeepShowWhenCovered` 这个勾到底啥意思
它是**"我不要被隐藏"的豁免权**,配在**被遮挡的那个面板**身上,不是配在全屏面板身上。
```
通行证(全屏,挂了 FullScreenCover)打开
↓ 会去隐藏所有比它层级低的面板
主界面 MainPanel → 没勾豁免 → 被隐藏(省 DC)✅
某个常驻活动图标面板 → 勾了豁免 → 保持显示(仍然出 DC)
```
**什么时候才该勾**:这个面板必须一直可见,哪怕被全屏页盖住。比如某些常驻的悬浮入口、全局提示条。
**绝大多数面板都不要勾**。勾了就等于放弃这个面板的省 DC 收益。T5 现在的 7 个面板**全部没勾**(keepShow=0),这是正确状态。
> 历史备注:T5 早期有 6 个面板被误勾成 keepShow=1,后来在提交 `c61b9012` 里统一改回 0。当时的判断是"商店和通行证业务互斥,不会同时出现",但勾了这个之后**未来任何更高层的全屏页叠加都会失效**,所以还是改回来了。
---
## 第三章 哪些面板能挂 / 绝对不能挂
### ✅ 可以挂
**完全铺满屏幕、打开后下层一点都看不见**的面板:
- 通行证、商店、抽卡、宠物、技能、神话英雄、等级礼包(T5 现有 7 个)
- 活动大页、图鉴、邮件全屏页、设置大页
### ❌ 绝对不能挂
| 类型 | 为什么不能挂 |
| --- | --- |
| **半屏弹窗**(确认框、奖励弹窗、底部抽屉) | 它盖不住下层,但会把下层隐藏 → **玩家看到下半截界面凭空消失** |
| **带半透明背景的面板** | 同理,透出来的部分会是黑的/空的 |
| **顶部飘字、Toast、加载条** | 这些在高层级,本身不会去隐藏别人;挂了也没意义 |
| **战斗中需要看到主界面的面板** | 比如伤害飘字叠加在大厅上的情况 |
**判断口诀**:把这个面板打开,**肉眼能不能看到它下面的任何界面**?能看到 → 不能挂。
---
## 第四章 原理(通俗版)
Unity 的 UI 只要 GameObject 是**激活状态**,哪怕被别的界面完全盖住、一个像素都看不见,它**照样提交 DrawCall**——GPU 老老实实画一遍然后被上面的盖掉。这是纯浪费。
`FullScreenCover` 做的事就是:**打开全屏面板的瞬间,把下面那些白画一遍的界面关掉(`SetActive(false)`);关闭全屏面板时再打开回来。**
关键点:
1. **不是销毁,是隐藏**。对象还在内存里,还在面板管理器的注册表里,重新激活是瞬间的,不需要重新加载资源。
2. **只隐藏"比自己层级低"的面板**。判定条件是 `panel.panelLayerIdx < _layerIdx`(严格小于),所以同层和更高层的面板不受影响。
3. **业务语义不变**。被隐藏的面板调 `isShowing()` 仍然返回 `true`(框架特意这么写的),业务逻辑感知不到自己被藏了。
层级从低到高(`UILayerIdx` 枚举):
```
layer_0 背景层 → layer_1 游戏层 → layer_2 UI基础层 → layer_3 功能层
→ layer_4 弹窗层 → layer_5 提示层 → layer_6 特效层 → layer_7 通知层 → layer_8 加载层
```
所以挂在 `layer_4`(弹窗层)的通行证,打开时会隐藏 `layer_0~layer_3` 里正在显示的面板(大厅 MainPanel、HomePanel 等)。
---
## 第五章 原理(程序版:改了哪 4 个文件)
补丁:`fullscreen_cover_mechanism.patch`(4 文件,+474 行)
| 文件 | 改动 |
| --- | --- |
| `FullScreenCover.cs` | **新建**。17 行纯配置组件,只有一个 `isKeepShowWhenCovered` 字段 |
| `iface/IPanelBase.cs` | **新增 5 个成员**:`panelLayerIdx`、`isKeepShowWhenCovered`、`isHiddenByCover`(只读)+ `SetCoverHidden()`、`RestoreFromCover()`(方法) |
| `PanelBase.cs` | **核心逻辑**,见下表 |
| `FullScreenCoverTool.cs` | **新建**编辑器工具(Assets/Editor 下,不进包) |
`PanelBase.cs` 的 6 个改动点:
| 位置 | 代码 | 作用 |
| --- | --- | --- |
| 字段 | `_fullScreenCover` / `_hiddenByCover` / `_panelsHiddenByMe` | 组件缓存 / 我被藏了吗 / 我藏了谁 |
| `_TryInitGameObject` | `_fullScreenCover = transform.GetComponent<FullScreenCover>();` | 加载完预制体后取组件(**只取根节点**) |
| `Show()` | `if (_fullScreenCover != null && _fullScreenCover.enabled) HidePanelsUnderThis();` | 打开时隐藏下层 |
| `Show()` | `if (_hiddenByCover) RestoreFromCover();` | 被藏着的面板主动 Show 时先恢复自己 |
| `___CloseForPanelMgr()` | 关闭动画回调里 `RestorePanelsHiddenByMe();` | 关闭时恢复下层 |
| `_Destroy()` | 开头 `RestorePanelsHiddenByMe();` | 兜底:没走 Close 直接销毁也要恢复 |
| `isShowing()` | `return _view.gameObject.activeSelf \|\| _hiddenByCover;` | 被藏着也算"正在显示" |
两个恢复函数都做了**幂等**处理:`RestorePanelsHiddenByMe()` 遍历时检查 `panel.isHiddenByCover`,`RestoreFromCover()` 开头检查 `if (!_hiddenByCover) return;`。重复调用不会出错。
`HidePanelsUnderThis()` 的筛选条件(4 个全满足才隐藏):
```csharp
if (panel.panelLayerIdx < _layerIdx // 1. 层级比我低
&& !panel.isKeepShowWhenCovered // 2. 没勾豁免
&& !panel.isHiddenByCover // 3. 还没被别人藏着(避免重复记录)
&& panel.isShowing()) // 4. 当前正在显示
```
---
## 第六章 移植到新项目
### 6.1 前提核对(3 项,缺一不可)
| 前提 | 怎么查 | 不满足怎么办 |
| --- | --- | --- |
| 用的是 MFrame 的 `PanelBase<V>` + `PanelMgr` 面板体系 | 搜 `class PanelBase<V> : UIBase, IPanelBase` | 不是的话本机制**不能直接搬**,得按 6.3 的思路重写 |
| 有 `UILayerIdx` 层级枚举,且每个面板知道自己层级 | 搜 `enum UILayerIdx` 和 `_layerIdx` | 需要自己补一个层级来源 |
| 面板预制体根节点上有 `UI_PanelBase` | 打开任一面板预制体看根节点 | 组件要挂在同一节点 |
### 6.2 移植步骤
1. **应用补丁**(推荐,能看清每行改了什么):
```powershell
cd <新项目的 T5_game 对应目录>
git apply --stat <归档路径>/02_全屏面板遮挡省DC/fullscreen_cover_mechanism.patch
git apply --check <归档路径>/02_全屏面板遮挡省DC/fullscreen_cover_mechanism.patch
git apply <归档路径>/02_全屏面板遮挡省DC/fullscreen_cover_mechanism.patch
```
`--check` 不报错才继续。报错通常是目标工程的 `PanelBase.cs` 版本与 T5 有差异 → 改用第 2 步。
2. **兜底:直接覆盖 4 个成品文件**(本目录里的 `FullScreenCover.cs`、`IPanelBase.cs`、`PanelBase.cs`、`FullScreenCoverTool.cs`)。
⚠️ 覆盖 `PanelBase.cs` 前**必须先 diff**,确认新项目的 PanelBase 没有 T5 之外的自定义改动,否则会覆盖掉别人的代码。
3. **改命名空间**:如果新项目不叫 `MFrame`,全局替换。
4. **编译通过后配置面板**:用 `Tools/UI/全屏面板遮挡工具` 逐个给全屏面板挂组件。**一次只挂一个,挂完立刻进游戏验证**(打开→看 Hierarchy 变灰→关闭→看下层恢复)。
5. **验证收益**:`Window → Analysis → Frame Debugger`,对比挂载前后同一画面的批次数。
### 6.3 如果新项目不是 MFrame 体系
机制本身不依赖 MFrame,只需要三个能力:
1. 面板管理器能**枚举所有正在显示的面板**及其**层级**;
2. 每个面板能**被隐藏 / 被恢复**(`SetActive(false/true)`);
3. 面板**打开时**和**关闭完成时**各有一个可插入的时机点。
把 `HidePanelsUnderThis()` / `RestorePanelsHiddenByMe()` 两个方法照搬过去,接到这三个点上即可。**第七章的两个陷阱同样适用,务必一起看。**
---
## 第七章 ★两个已知陷阱(T5 真实事故复盘)
这两个都是**机制本身的设计边界**,不是 bug。移植到新项目时**必须一并注意**。
### 陷阱一:已经 Close 掉的面板被"复活"(T5 副本异常,真实发生)
**现象**:副本界面挂了 `FullScreenCover`,点"开始挑战"后出现异常,看起来像"主界面找不到了"。
**根因**(提交 `cdbbca4b` → `d9fc47e0` 处置):
| 时刻 | 发生了什么 | 状态 |
| --- | --- | --- |
| T0 | 打开 RaidSelectPanel(挂了 FullScreenCover + Scale 关闭动画 0.15s) | MainPanel/HomePanel 被 `SetActive(false)`,记进 RaidSelect 的 `_panelsHiddenByMe` |
| T1 | 点"开始挑战" → 战斗加载完成回调里业务主动执行 `PanelMgr.Close(MainPanel)` + `Close(RaidSelectPanel)` | MainPanel **业务语义已关闭**,但物理对象还在(`autoDestroy=false`,Close 只是 inactive) |
| T2 | 0.15 秒后 RaidSelectPanel 关闭动画播完 → `RestorePanelsHiddenByMe()` | 遍历列表,看到 MainPanel 的 `isHiddenByCover == true` → **`SetActive(true)` 把它复活了** |
| T3 | 后续所有检查逻辑 | MainPanel 处于"**业务说它关了、物理对象却活着**"的矛盾状态,检查分支全乱 |
问题出在 `RestorePanelsHiddenByMe()` 的守卫**只看"是否被遮挡隐藏",不看"是否已被业务关闭"**:
```csharp
if (panel != null && panel.isHiddenByCover) // ← 少了 isClosed 判断
{
panel.RestoreFromCover();
}
```
**T5 的处置**:提交 `d9fc47e0 debug` 里直接把 RaidSelectPanel 的 `FullScreenCover` 组件**摘掉了**(保守做法,不动框架)。所以现在的 7 个挂载面板里没有 RaidSelectPanel。
**⚠️ 框架层隐患至今未修**:`PanelBase.cs` 里**没有任何 `_closed` 守卫**(已确认,全文搜 `_closed`/`isClosed` 零命中)。只要再出现"某面板在被遮挡状态下被业务 Close,且它 `autoDestroy=false`",同样的复活就会重演。
**推荐的框架层修复(约 4 行,不破坏遮挡机制)**:
```csharp
// ① IPanelBase.cs 新增只读成员
/// <summary>业务是否已关闭(区别于"被遮挡隐藏")</summary>
bool isClosed { get; }
// ② PanelBase.cs 新增字段与实现
private bool _closed;
bool IPanelBase.isClosed => _closed;
// ③ PanelBase.Show() 里,_data = data; 之后加一行
_closed = false;
// ④ PanelBase.___CloseForPanelMgr(Action complete) 方法体第一行加
_closed = true;
// ⑤ isShowing() 改为
return !_closed && (_view.gameObject.activeSelf || _hiddenByCover);
// ⑥ RestorePanelsHiddenByMe() 的守卫改为
if (panel != null && panel.isHiddenByCover && !panel.isClosed)
```
**为什么 `_closed = true` 要放在 `___CloseForPanelMgr` 而不是 `Close()`**:
`PanelBase.Close()` 只是转发给 `PanelMgr.Instance.Close(_registerModule)`,而业务代码里很多地方**直接调 `PanelMgr.Close(...)`**(比如 `EntrancePanel._CloseAllPanel`),根本不经过 `PanelBase.Close()`。`___CloseForPanelMgr` 是 PanelMgr 回调面板的**唯一内部关闭入口**,所有关闭路径都必经此处,守卫放这里才没有漏网。
**这个修复的安全性**:只增加一个"已关闭就别复活"的判断,不改隐藏逻辑、不改层级判定、不删任何资源。已关闭的面板本来就不该被显示出来。
### 陷阱二:被遮挡(inactive)的面板如果挂动画 tween,Close 会卡死
Unity 的硬规则:**`MonoBehaviour.Update()` 在 `SetActive(false)` 的对象上完全不运行。**
MFrame 的 `PanelTween_Scale` 这类动画 tween 是由 `AbsCom.Update()` 驱动的,关闭回调挂在 tween 播完之后。所以:
```
面板被遮挡 → inactive → 此时业务 Close 它
→ tween 的 Update 不跑 → 动画永远停在第一帧
→ 关闭回调永不触发 → RestorePanelsHiddenByMe() 永不执行
→ 这个面板永远关不掉,它藏着的下层面板也永远出不来
```
**T5 当前安全的原因**:MainPanel 挂的是 `PanelTween_None`(同步回调,不依赖 Update),所以走的是"立即完成"路径,不会卡。
**规避方法(三选一)**:
1. **给可能被遮挡的面板(尤其是主界面)用 `PanelTween_None`**,别用动画版 —— T5 现在就是这个状态;
2. 业务在 Close 一个可能被遮挡的面板前,先确认它 `isHiddenByCover`,是的话先让它恢复;
3. 框架层在 `___CloseForPanelMgr` 开头补一句:若 `_hiddenByCover` 则先 `RestoreFromCover()` 再播关闭动画。
**排查这个陷阱的特征**:面板"关不掉"、下层界面"再也不出现"、没有任何报错、Hierarchy 里那个面板一直是灰的。看到这三个特征同时出现,直接查这条。
---
## 第八章 排查技巧
### 8.1 ⚠️ 团结引擎的 guid 坑(文本反查会落空)
T5 用的是团结引擎(Tuanjie),`.cs.meta` 里的 guid 是**加密长串**(56 字符,含 `/` `+` `=`):
```
guid: Dysd4X//V3yyisRZkbBdkO/Yqfm+oXtw45Vx0v70tF8fEn5k7g7wkdI=
```
而 **prefab 里写入的不是这个字符串**,是转换后的 32 位 hex:
```
FullScreenCover 在 prefab 中的 hex = 0a2deb478a85eef4883e8465d48c426f
```
所以「拿 `.meta` 的 guid 去 grep prefab」**永远搜不到**(我亲自踩过,两种写法都失败)。
**正确做法(两条,任选)**:
```powershell
# 做法一:搜组件的序列化字段名(最简单,不依赖 guid)
Get-ChildItem "Assets\Resources" -Recurse -Include *.prefab |
Where-Object { Select-String -Path $_.FullName -Pattern 'isKeepShowWhenCovered' -Quiet } |
ForEach-Object { $_.Name }
# 做法二:用转换后的 hex(能顺便确认组件引用完整)
Get-ChildItem "Assets\Resources" -Recurse -Include *.prefab |
Where-Object { Select-String -Path $_.FullName -Pattern '0a2deb478a85eef4883e8465d48c426f' -Quiet }
```
两种做法在 T5 上都返回**同样的 7 个面板**,可以互相验证。
> 移植到别的项目后 hex 会不同(跟 guid 绑定),用做法一最稳。
### 8.2 验证机制是否生效(3 步)
1. **Hierarchy 变灰法**(最快):Play → 打开全屏面板 → 看 Hierarchy 里 MainPanel/HomePanel 是否变灰 → 关闭 → 是否恢复。变灰即生效。
2. **Frame Debugger 对照法**(量化):打开全屏面板,数批次数;然后临时把组件取消勾选(Inspector 里去掉 `FullScreenCover` 的启用勾)再数一次。差值就是省下的 DC。
3. **组件状态自查**:`Tools/UI/全屏面板遮挡工具` 扫一遍,看有没有"已禁用"的黄字警告。
### 8.3 临时禁用做对照实验(不改预制体)
不需要移除组件,两种无损办法:
- **编辑器里**:选中预制体根节点 → Inspector 里把 `FullScreenCover` 组件**左上角的勾去掉** → Apply。框架判断的是 `_fullScreenCover.enabled`,取消勾选即完全失效。实验完再勾回来。
- **运行时代码**(临时调试用):
```csharp
var cover = panel.view.GetComponent<MFrame.FullScreenCover>();
if (cover != null) cover.enabled = false; // 之后 Show 就不会隐藏下层
```
### 8.4 出问题时看哪里
| 症状 | 优先查 |
| --- | --- |
| 打开全屏面板后下层界面消失且不恢复 | 陷阱二(tween 卡死)+ 该面板 `autoDestroy` 配置 |
| 已关闭的界面自己冒出来了 | 陷阱一(复活),查是谁在 Close 之后又 SetActive |
| 挂了组件但完全没效果 | ①组件是否挂在**根节点** ②组件是否被禁用 ③面板层级是否真的比目标高 |
| 半屏弹窗导致下层消失 | 挂错了,该面板不该挂(第三章) |
---
## 第九章 T5 当前挂载现状(截至归档时)
7 个面板,全部 `keepShow=0`(正确配置)、组件全部启用:
| 面板预制体 | 业务 | keepShow |
| --- | --- | --- |
| `UI_DrawCardPanel` | 抽卡 | 0 |
| `UI_LevelGiftPanel` | 等级礼包 | 0 |
| `UI_MythHeroPanel` | 神话英雄 | 0 |
| `UI_PassPanel` | 通行证 | 0 |
| `UI_PetPanel` | 宠物 | 0 |
| `UI_ShopPanel` | 商店 | 0 |
| `UI_SkillPanel` | 技能 | 0 |
**`UI_RaidSelectPanel`(副本选择)不在列表里** —— 它曾经挂过(`cdbbca4b` 加上,keepShow=1 → `c61b9012` 改 0 → `d9fc47e0` 整个组件摘掉),原因就是第七章陷阱一的事故。
**如果第七章的框架层守卫修好了,RaidSelectPanel 可以重新挂回去**(它是全屏面板,本身符合挂载条件),能再省一批 DC。在守卫没修之前**不要挂**。
---
## 本目录文件清单
| 文件 | 说明 |
| --- | --- |
| `FullScreenCover.cs` | 组件本体(17 行,挂预制体根节点) |
| `IPanelBase.cs` | 接口(新增 5 个成员) |
| `PanelBase.cs` | 核心实现(遮挡/恢复全部逻辑在这) |
| `FullScreenCoverTool.cs` | 编辑器工具(放 `Assets/Editor/`,不进包) |
| `fullscreen_cover_mechanism.patch` | 上述 4 文件的 git 标准补丁(+474 行) |
| `FullScreenCover_使用方法与避坑.md` | 本文 |
---
## 第十章 T11 移植增强记录(2026-09-10)
> 本目录的 4 个 `.cs` 成品文件已同步为 **T11_game 验证过的增强版**(含第七章两个陷阱的框架层防护)。
> 原始 T5 版本保留在 `fullscreen_cover_mechanism_T5原始.patch`,仅供历史对照,移植时不要用它。
### 相比 T5 原版的 2 处增强(均落在 PanelBase.cs / IPanelBase.cs)
**增强 1:`_closed` 复活守卫**(= 第七章陷阱一的框架层修复,按原文档推荐方案落地)
- `IPanelBase` 新增 `bool isClosed { get; }`;
- `PanelBase` 新增 `_closed` 字段:`Show()` 时置 false,`___CloseForPanelMgr()` 开头置 true;
- `isShowing()` 改为 `return !_closed && (_view.gameObject.activeSelf || _hiddenByCover);`;
- `RestorePanelsHiddenByMe()` 恢复守卫加 `!panel.isClosed`——**已关闭的面板永不会被"复活"**。
**增强 2:被遮挡面板关闭前先恢复**(= 第七章陷阱二的规避方法 3)
- `___CloseForPanelMgr()` 开头:若 `_hiddenByCover` 则先 `RestoreFromCover()` 再播关闭动画。
- 防止面板在 inactive 状态下关闭时 tween 的 `Update` 不跑、关闭回调永不触发、下层面板永远出不来。
### 移植到新项目时的文件对应关系
| 本目录文件 | 目标位置 |
| --- | --- |
| `FullScreenCover.cs` | `my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/` |
| `IPanelBase.cs` / `PanelBase.cs` | 同上(`iface/` 子目录)——**建议只看 `fullscreen_cover_t11_guard_mechanism.patch` 手工合入**,别整文件覆盖 |
| `FullScreenCoverTool.cs` | `Assets/Editor/`(不进包) |
### T11 实际改动验证
`fullscreen_cover_t11_guard_mechanism.patch` 由 T11_game 仓库 `git diff` 直接导出,覆盖 `PanelBase.cs`(+135/-3)与 `IPanelBase.cs`(+28)两文件;`FullScreenCover.cs`、`FullScreenCoverTool.cs` 为全新文件,直接复制即可。
@@ -0,0 +1,65 @@
using System;
namespace MFrame
{
/// <summary>
/// UI面板基础接口
/// </summary>
/// <typeparam name="T">面板数据类型参数</typeparam>
public interface IPanelBase : IDestroy
{
/// <summary>
/// 显示面板
/// </summary>
/// <param name="data">面板所需数据</param>
void Show(PanelBaseData data, System.Action<IPanelBase> completeHandler);
void ___CloseForPanelMgr(Action complete);
bool IsAutoDestroy();
/// <summary>
/// 是否正在显示中
/// </summary>
/// <returns></returns>
bool isShowing();
/// <summary>
/// 是否正在加载中
/// </summary>
/// <returns></returns>
bool isLoading();
/// <summary>
/// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板)
/// </summary>
UILayerIdx panelLayerIdx { get; }
/// <summary>
/// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true)
/// </summary>
bool isKeepShowWhenCovered { get; }
/// <summary>
/// 是否正被上层全屏面板隐藏
/// </summary>
bool isHiddenByCover { get; }
/// <summary>
/// 业务是否已关闭(区别于“被遮挡隐藏”)
/// </summary>
bool isClosed { get; }
/// <summary>
/// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall
/// </summary>
void SetCoverHidden();
/// <summary>
/// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等)
/// </summary>
void RestoreFromCover();
}
}
@@ -0,0 +1,366 @@
using System.Collections.Generic;
using UnityEngine;
namespace MFrame
{
/// <summary>
/// 面板创建委托,用于动态创建UI面板实例
/// </summary>
/// <returns>返回创建的面板实例</returns>
public delegate IPanelBase PanelCreate(PanelBaseData data, System.Action<IPanelBase> completeHandler);
/// <summary>
/// UI面板基类,提供基础UI功能和资源加载管理
/// 继承自UIBase,用于实现具体的UI面板
/// </summary>
public class PanelBase<V> : UIBase, IPanelBase where V : UI_PanelBase
{
/// <summary>
/// UI层级索引,控制面板在UI层级中的显示顺序
/// </summary>
protected UILayerIdx _layerIdx;
/// <summary>
/// 面板数据,用于存储和传递面板所需的数据
/// </summary>
protected PanelBaseData _data;
/// <summary>
/// 面板视图组件,负责处理UI的具体显示和交互逻辑
/// </summary>
protected V _view;
/// <summary>
/// 预制体资源路径
/// </summary>
protected string _url;
protected string _registerModule;
// 全屏遮挡以组件形式配置:在面板预制体根节点挂 FullScreenCover 组件即生效(策划可自助添加/移除)
/// <summary>
/// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板)
/// </summary>
private FullScreenCover _fullScreenCover;
/// <summary>
/// 是否正被上层全屏面板隐藏
/// </summary>
private bool _hiddenByCover;
/// <summary>
/// 被本面板(全屏遮挡)隐藏的下层面板列表
/// </summary>
private readonly List<IPanelBase> _panelsHiddenByMe = new List<IPanelBase>();
/// <summary>
/// 业务是否已关闭(防止被遮挡隐藏的已关闭面板被“复活”)
/// </summary>
private bool _closed;
/// <summary>
/// 构造函数,初始化面板基本属性
/// </summary>
/// <param name="autoDestroy">是否在关闭时自动销毁</param>
public PanelBase(bool autoDestroy) : base(null)
{
_autoDestroy = autoDestroy;
}
/// <summary>
/// 在关闭界面的时候是否进行销毁
/// </summary>
/// <returns></returns>
public bool IsAutoDestroy()
{
return _autoDestroy;
}
// —— 全屏遮挡:IPanelBase 接口实现 ——
UILayerIdx IPanelBase.panelLayerIdx => _layerIdx;
bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered;
bool IPanelBase.isHiddenByCover => _hiddenByCover;
bool IPanelBase.isClosed => _closed;
/// <summary>
/// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall)
/// </summary>
public void SetCoverHidden()
{
_hiddenByCover = true;
if (_view != null)
{
_view.gameObject.SetActive(false);
}
}
/// <summary>
/// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过)
/// </summary>
public void RestoreFromCover()
{
if (!_hiddenByCover)
{
return;
}
_hiddenByCover = false;
if (_view != null)
{
_view.gameObject.SetActive(true);
}
}
/// <summary>
/// 尝试异步加载UI预制体
/// </summary>
/// <param name="url">预制体资源路径</param>
/// <param name="layerIdx">目标UI层级</param>
/// <param name="data">面板初始化数据</param>
protected virtual void _TryInit(string registerModule, string url, UILayerIdx layerIdx, PanelBaseData data, System.Action<IPanelBase> completeHandler = null)
{
_registerModule = registerModule;
_url = url;
// 初始化并启用资源加载器
_layerIdx = layerIdx;
ResLoaderMgr.Instance.TryLoadAsync(_registerModule, _url, (float progress) =>
{
_OnProgress(progress);
}, (Object prefab) =>
{
if (prefab != null)
{
// 加载成功后实例化预制体并设置到指定层级
_TryInitGameObject(prefab as GameObject, layerIdx, data, completeHandler);
}
else
{
// 处理加载失败的情况
_OnLoadFail();
}
});
}
/// <summary>
/// 初始化 UI预制体
/// </summary>
/// <param name="layerIdx">目标UI层级</param>
/// <param name="data">面板初始化数据</param>
protected virtual void _TryInitGameObject(GameObject prefab, UILayerIdx layerIdx, PanelBaseData data, System.Action<IPanelBase> completeHandler = null)
{
LayerItem litem = GameGlobalData.GetLayerItem(_layerIdx);
GameObject go = UnityUtils.InstantiatePrefab(prefab, litem.transform);
transform = go.transform;
// 获取视图组件并完成初始化
_view = transform.GetComponent<V>();
// 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null)
_fullScreenCover = transform.GetComponent<FullScreenCover>();
_view.AddUpdateHandler(this.Update);
_OnLoadComplete();
Show(data, completeHandler);
}
public V view
{
get
{
return _view;
}
}
protected virtual void _OnProgress(float progress)
{
}
/// <summary>
/// 资源加载完成回调,子类可重写此方法实现自定义加载完成逻辑
/// </summary>
protected virtual void _OnLoadComplete()
{
foreach (var btn in this._view.btnsClose)
{
if (btn != null)
this.AddBtnOnClick(btn, this.Close);
}
}
/// <summary>
/// 资源加载失败回调,子类可重写此方法实现自定义失败处理逻辑
/// </summary>
protected virtual void _OnLoadFail()
{
}
/// <summary>
/// 打开面板,可传入自定义数据
/// </summary>
/// <param name="data">面板初始化数据</param>
public void Show(PanelBaseData data, System.Action<IPanelBase> completeHandler)
{
_data = data;
_closed = false;
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);
});
}
/// <summary>
/// 判断面板是否正在显示
/// </summary>
/// <returns></returns>
public bool isShowing()
{
if (_view == null)
{
return false;
}
// 被上层全屏面板隐藏时仍视为“打开中”,保持业务语义不变;已关闭的面板不算
return !_closed && (_view.gameObject.activeSelf || _hiddenByCover);
}
public bool isLoading()
{
return _view == null;
}
protected virtual void _OnShow()
{
}
/// <summary>
/// 关闭面板
/// </summary>
public void Close()
{
PanelMgr.Instance.Close(_registerModule);
}
/// <summary>
/// 关闭面板(内部使用)
/// </summary>
public void ___CloseForPanelMgr(System.Action complete)
{
// 标记业务已关闭,防止关闭过程中被遮挡机制“复活”
_closed = true;
// 若正被上层全屏面板隐藏(inactive 会导致关闭动画不更新而卡死),先恢复再播关闭动画
if (_hiddenByCover)
{
RestoreFromCover();
}
_view.Close(() =>
{
// 关闭完成后恢复被本面板隐藏的下层面板
RestorePanelsHiddenByMe();
_OnClose();
complete?.Invoke();
});
}
protected virtual void _OnClose()
{
}
/// <summary>
/// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板,
/// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。
/// </summary>
private void HidePanelsUnderThis()
{
Dictionary<string, PanelMsgItem> items = PanelMgr.Instance.items;
if (items == null)
{
return;
}
foreach (KeyValuePair<string, PanelMsgItem> 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();
}
}
}
/// <summary>
/// 恢复被本面板隐藏的下层面板(幂等,可重复调用)
/// </summary>
private void RestorePanelsHiddenByMe()
{
int len = _panelsHiddenByMe.Count;
for (int i = 0; i < len; i++)
{
IPanelBase panel = _panelsHiddenByMe[i];
// 已关闭的面板不恢复,防止“复活”已关闭的界面
if (panel != null && panel.isHiddenByCover && !panel.isClosed)
{
panel.RestoreFromCover();
}
}
_panelsHiddenByMe.Clear();
}
/// <summary>
/// 销毁面板实例,清理资源防止内存泄漏
/// </summary>
protected override void _Destroy()
{
// 兼容未走 Close 直接销毁的路径,确保下层面板一定被恢复
RestorePanelsHiddenByMe();
base._Destroy();
// 清理资源加载器
ResLoaderMgr.Instance.TryRemoveLoads(_registerModule);
// 清除面板数据
_data = null;
_fullScreenCover = null;
if (_view != null)
{
_view.RemoveUpdateHandler(this.Update);
GameObject.Destroy(_view.gameObject);
_view = null;
}
}
}
}
@@ -0,0 +1,567 @@
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;
+
+/// <summary>
+/// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。
+/// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。
+/// 策划可在此窗口自助管理,无需改代码。
+/// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。
+/// </summary>
+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<CoverInfo> results = new List<CoverInfo>();
+ private Vector2 scroll;
+ private GameObject addTarget;
+ private bool scanned;
+ private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体";
+
+ [MenuItem("Tools/UI/全屏面板遮挡工具")]
+ private static void ShowWindow()
+ {
+ var win = GetWindow<FullScreenCoverTool>("全屏面板遮挡");
+ 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<GameObject>(path);
+ if (prefab == null)
+ {
+ continue;
+ }
+ FullScreenCover[] comps = prefab.GetComponentsInChildren<FullScreenCover>(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();
+ }
+
+ /// <summary>动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。</summary>
+ private static string GetScriptGuid()
+ {
+ GameObject probe = new GameObject("FullScreenCoverGuidProbe");
+ try
+ {
+ MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent<FullScreenCover>());
+ if (ms == null)
+ {
+ return null;
+ }
+ return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms));
+ }
+ finally
+ {
+ Object.DestroyImmediate(probe);
+ }
+ }
+
+ /// <summary>逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。</summary>
+ 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;
+ }
+
+ // —— 修改预制体 ——
+
+ /// <summary>以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。</summary>
+ private static void EditPrefab(string path, System.Action<GameObject> 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<FullScreenCover>() != null)
+ {
+ already = true;
+ }
+ else
+ {
+ root.AddComponent<FullScreenCover>();
+ }
+ });
+
+ ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover"));
+ ScanAll();
+ }
+
+ private void RemoveComponent(string path)
+ {
+ EditPrefab(path, root =>
+ {
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
+ foreach (FullScreenCover comp in comps)
+ {
+ Object.DestroyImmediate(comp, true);
+ }
+ });
+ }
+
+ private void SetKeepShow(string path, bool value)
+ {
+ EditPrefab(path, root =>
+ {
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(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
+{
+ /// <summary>
+ /// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。
+ /// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall),
+ /// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。
+ /// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。
+ /// </summary>
+ 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 组件即生效(策划可自助添加/移除)
+ /// <summary>
+ /// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板)
+ /// </summary>
+ private FullScreenCover _fullScreenCover;
+
+ /// <summary>
+ /// 是否正被上层全屏面板隐藏
+ /// </summary>
+ private bool _hiddenByCover;
+ /// <summary>
+ /// 被本面板(全屏遮挡)隐藏的下层面板列表
+ /// </summary>
+ private readonly List<IPanelBase> _panelsHiddenByMe = new List<IPanelBase>();
+
+ // —— 全屏遮挡:IPanelBase 接口实现 ——
+ UILayerIdx IPanelBase.panelLayerIdx => _layerIdx;
+ bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered;
+ bool IPanelBase.isHiddenByCover => _hiddenByCover;
+
+ /// <summary>
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall)
+ /// </summary>
+ public void SetCoverHidden()
+ {
+ _hiddenByCover = true;
+ if (_view != null)
+ {
+ _view.gameObject.SetActive(false);
+ }
+ }
+
+ /// <summary>
+ /// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过)
+ /// </summary>
+ public void RestoreFromCover()
+ {
+ if (!_hiddenByCover)
+ {
+ return;
+ }
+ _hiddenByCover = false;
+ if (_view != null)
+ {
+ _view.gameObject.SetActive(true);
+ }
+ }
+
/// <summary>
/// 尝试异步加载UI预制体
@@ -101,6 +150,8 @@ namespace MFrame
transform = go.transform;
// 获取视图组件并完成初始化
_view = transform.GetComponent<V>();
+ // 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null)
+ _fullScreenCover = transform.GetComponent<FullScreenCover>();
_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
}
+ /// <summary>
+ /// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板,
+ /// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。
+ /// </summary>
+ private void HidePanelsUnderThis()
+ {
+ Dictionary<string, PanelMsgItem> items = PanelMgr.Instance.items;
+ if (items == null)
+ {
+ return;
+ }
+ foreach (KeyValuePair<string, PanelMsgItem> 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();
+ }
+ }
+ }
+
+ /// <summary>
+ /// 恢复被本面板隐藏的下层面板(幂等,可重复调用)
+ /// </summary>
+ 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();
+ }
+
/// <summary>
@@ -211,8 +326,8 @@ namespace MFrame
/// </summary>
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
/// <returns></returns>
bool isLoading();
+ /// <summary>
+ /// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板)
+ /// </summary>
+ UILayerIdx panelLayerIdx { get; }
+ /// <summary>
+ /// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true)
+ /// </summary>
+ bool isKeepShowWhenCovered { get; }
+
+ /// <summary>
+ /// 是否正被上层全屏面板隐藏
+ /// </summary>
+ bool isHiddenByCover { get; }
+
+ /// <summary>
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall
+ /// </summary>
+ void SetCoverHidden();
+
+ /// <summary>
+ /// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等)
+ /// </summary>
+ void RestoreFromCover();
}
}
@@ -0,0 +1,567 @@
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;
+
+/// <summary>
+/// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。
+/// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。
+/// 策划可在此窗口自助管理,无需改代码。
+/// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。
+/// </summary>
+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<CoverInfo> results = new List<CoverInfo>();
+ private Vector2 scroll;
+ private GameObject addTarget;
+ private bool scanned;
+ private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体";
+
+ [MenuItem("Tools/UI/全屏面板遮挡工具")]
+ private static void ShowWindow()
+ {
+ var win = GetWindow<FullScreenCoverTool>("全屏面板遮挡");
+ 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<GameObject>(path);
+ if (prefab == null)
+ {
+ continue;
+ }
+ FullScreenCover[] comps = prefab.GetComponentsInChildren<FullScreenCover>(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();
+ }
+
+ /// <summary>动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。</summary>
+ private static string GetScriptGuid()
+ {
+ GameObject probe = new GameObject("FullScreenCoverGuidProbe");
+ try
+ {
+ MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent<FullScreenCover>());
+ if (ms == null)
+ {
+ return null;
+ }
+ return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms));
+ }
+ finally
+ {
+ Object.DestroyImmediate(probe);
+ }
+ }
+
+ /// <summary>逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。</summary>
+ 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;
+ }
+
+ // —— 修改预制体 ——
+
+ /// <summary>以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。</summary>
+ private static void EditPrefab(string path, System.Action<GameObject> 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<FullScreenCover>() != null)
+ {
+ already = true;
+ }
+ else
+ {
+ root.AddComponent<FullScreenCover>();
+ }
+ });
+
+ ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover"));
+ ScanAll();
+ }
+
+ private void RemoveComponent(string path)
+ {
+ EditPrefab(path, root =>
+ {
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
+ foreach (FullScreenCover comp in comps)
+ {
+ Object.DestroyImmediate(comp, true);
+ }
+ });
+ }
+
+ private void SetKeepShow(string path, bool value)
+ {
+ EditPrefab(path, root =>
+ {
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(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
+{
+ /// <summary>
+ /// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。
+ /// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall),
+ /// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。
+ /// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。
+ /// </summary>
+ 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 组件即生效(策划可自助添加/移除)
+ /// <summary>
+ /// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板)
+ /// </summary>
+ private FullScreenCover _fullScreenCover;
+
+ /// <summary>
+ /// 是否正被上层全屏面板隐藏
+ /// </summary>
+ private bool _hiddenByCover;
+ /// <summary>
+ /// 被本面板(全屏遮挡)隐藏的下层面板列表
+ /// </summary>
+ private readonly List<IPanelBase> _panelsHiddenByMe = new List<IPanelBase>();
+
+ // —— 全屏遮挡:IPanelBase 接口实现 ——
+ UILayerIdx IPanelBase.panelLayerIdx => _layerIdx;
+ bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered;
+ bool IPanelBase.isHiddenByCover => _hiddenByCover;
+
+ /// <summary>
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall)
+ /// </summary>
+ public void SetCoverHidden()
+ {
+ _hiddenByCover = true;
+ if (_view != null)
+ {
+ _view.gameObject.SetActive(false);
+ }
+ }
+
+ /// <summary>
+ /// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过)
+ /// </summary>
+ public void RestoreFromCover()
+ {
+ if (!_hiddenByCover)
+ {
+ return;
+ }
+ _hiddenByCover = false;
+ if (_view != null)
+ {
+ _view.gameObject.SetActive(true);
+ }
+ }
+
/// <summary>
/// 尝试异步加载UI预制体
@@ -101,6 +150,8 @@ namespace MFrame
transform = go.transform;
// 获取视图组件并完成初始化
_view = transform.GetComponent<V>();
+ // 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null)
+ _fullScreenCover = transform.GetComponent<FullScreenCover>();
_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
}
+ /// <summary>
+ /// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板,
+ /// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。
+ /// </summary>
+ private void HidePanelsUnderThis()
+ {
+ Dictionary<string, PanelMsgItem> items = PanelMgr.Instance.items;
+ if (items == null)
+ {
+ return;
+ }
+ foreach (KeyValuePair<string, PanelMsgItem> 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();
+ }
+ }
+ }
+
+ /// <summary>
+ /// 恢复被本面板隐藏的下层面板(幂等,可重复调用)
+ /// </summary>
+ 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();
+ }
+
/// <summary>
@@ -211,8 +326,8 @@ namespace MFrame
/// </summary>
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
/// <returns></returns>
bool isLoading();
+ /// <summary>
+ /// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板)
+ /// </summary>
+ UILayerIdx panelLayerIdx { get; }
+ /// <summary>
+ /// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true)
+ /// </summary>
+ bool isKeepShowWhenCovered { get; }
+
+ /// <summary>
+ /// 是否正被上层全屏面板隐藏
+ /// </summary>
+ bool isHiddenByCover { get; }
+
+ /// <summary>
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall
+ /// </summary>
+ void SetCoverHidden();
+
+ /// <summary>
+ /// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等)
+ /// </summary>
+ void RestoreFromCover();
}
}