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

21 KiB
Raw Permalink Blame History

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 个全满足才隐藏):

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. 应用补丁(推荐,能看清每行改了什么):

    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() 的守卫只看"是否被遮挡隐藏",不看"是否已被业务关闭":

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),所以走的是"立即完成"路径,不会卡。

规避方法(三选一):

  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」永远搜不到(我亲自踩过,两种写法都失败)。

正确做法(两条,任选):

# 做法一:搜组件的序列化字段名(最简单,不依赖 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,取消勾选即完全失效。实验完再勾回来。
  • 运行时代码(临时调试用):
    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 为全新文件,直接复制即可。