21 KiB
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:手动挂(工具不在手边时)
- 打开面板预制体,选中根节点(必须和
UI_PanelBase在同一个节点上); - Inspector → Add Component → 搜
FullScreenCover; - 完事。不需要写任何代码,也不需要注册。
⚠️ 必须挂在根节点。框架是在
_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));关闭全屏面板时再打开回来。
关键点:
- 不是销毁,是隐藏。对象还在内存里,还在面板管理器的注册表里,重新激活是瞬间的,不需要重新加载资源。
- 只隐藏"比自己层级低"的面板。判定条件是
panel.panelLayerIdx < _layerIdx(严格小于),所以同层和更高层的面板不受影响。 - 业务语义不变。被隐藏的面板调
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 个全满足才隐藏):
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 移植步骤
-
应用补丁(推荐,能看清每行改了什么):
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 步。 -
兜底:直接覆盖 4 个成品文件(本目录里的
FullScreenCover.cs、IPanelBase.cs、PanelBase.cs、FullScreenCoverTool.cs)。 ⚠️ 覆盖PanelBase.cs前必须先 diff,确认新项目的 PanelBase 没有 T5 之外的自定义改动,否则会覆盖掉别人的代码。 -
改命名空间:如果新项目不叫
MFrame,全局替换。 -
编译通过后配置面板:用
Tools/UI/全屏面板遮挡工具逐个给全屏面板挂组件。一次只挂一个,挂完立刻进游戏验证(打开→看 Hierarchy 变灰→关闭→看下层恢复)。 -
验证收益:
Window → Analysis → Frame Debugger,对比挂载前后同一画面的批次数。
6.3 如果新项目不是 MFrame 体系
机制本身不依赖 MFrame,只需要三个能力:
- 面板管理器能枚举所有正在显示的面板及其层级;
- 每个面板能被隐藏 / 被恢复(
SetActive(false/true)); - 面板打开时和关闭完成时各有一个可插入的时机点。
把 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() 的守卫只看"是否被遮挡隐藏",不看"是否已被业务关闭":
if (panel != null && panel.isHiddenByCover) // ← 少了 isClosed 判断
{
panel.RestoreFromCover();
}
T5 的处置:提交 d9fc47e0 debug 里直接把 RaidSelectPanel 的 FullScreenCover 组件摘掉了(保守做法,不动框架)。所以现在的 7 个挂载面板里没有 RaidSelectPanel。
⚠️ 框架层隐患至今未修:PanelBase.cs 里没有任何 _closed 守卫(已确认,全文搜 _closed/isClosed 零命中)。只要再出现"某面板在被遮挡状态下被业务 Close,且它 autoDestroy=false",同样的复活就会重演。
推荐的框架层修复(约 4 行,不破坏遮挡机制):
// ① 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),所以走的是"立即完成"路径,不会卡。
规避方法(三选一):
- 给可能被遮挡的面板(尤其是主界面)用
PanelTween_None,别用动画版 —— T5 现在就是这个状态; - 业务在 Close 一个可能被遮挡的面板前,先确认它
isHiddenByCover,是的话先让它恢复; - 框架层在
___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」永远搜不到(我亲自踩过,两种写法都失败)。
正确做法(两条,任选):
# 做法一:搜组件的序列化字段名(最简单,不依赖 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 步)
- Hierarchy 变灰法(最快):Play → 打开全屏面板 → 看 Hierarchy 里 MainPanel/HomePanel 是否变灰 → 关闭 → 是否恢复。变灰即生效。
- Frame Debugger 对照法(量化):打开全屏面板,数批次数;然后临时把组件取消勾选(Inspector 里去掉
FullScreenCover的启用勾)再数一次。差值就是省下的 DC。 - 组件状态自查:
Tools/UI/全屏面板遮挡工具扫一遍,看有没有"已禁用"的黄字警告。
8.3 临时禁用做对照实验(不改预制体)
不需要移除组件,两种无损办法:
- 编辑器里:选中预制体根节点 → Inspector 里把
FullScreenCover组件左上角的勾去掉 → Apply。框架判断的是_fullScreenCover.enabled,取消勾选即完全失效。实验完再勾回来。 - 运行时代码(临时调试用):
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 为全新文件,直接复制即可。