DC优化
This commit is contained in:
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user