Files
Planner_Tools/Unity/DC优化/02_全屏面板遮挡省DC/FullScreenCover_使用方法与避坑.md
2026-09-10 17:29:24 +08:00

399 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 为全新文件,直接复制即可。