Files
2026-09-10 17:29:24 +08:00

287 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 移植与验证手册
> 配套包:`DC优化归档/` | 验证过的环境:Unity 团结引擎(Tuanjie)+ UI Effect **5.11.3** + URP
> 改动量:2 个文件、126 行新增、2 行删除。业务代码 **0 改动**。
---
## 第一章 动手前的前提核对(3 项,缺一不可)
### 1.1 UIEffect 必须是"本地源码包",不能是 PackageCache 只读包
打开目标工程的 `Packages/manifest.json`,找 `com.coffee.ui-effect`:
```jsonc
// ✅ 可以改:file: 指向工程内源码目录
"com.coffee.ui-effect": "file:../my_libs/unityLib/UIEffect-main/Packages/src",
// ❌ 改了白改:版本号形式会被拉进 Library/PackageCache/(只读,Unity 重新解包会覆盖)
"com.coffee.ui-effect": "5.11.3"
```
如果是 ❌ 形式,先转成本地包:
1. 从 [github.com/mob-sakai/ParticleEffectForUGUI 同作者的 UIEffect 仓库](https://github.com/mob-sakai/UIEffect) 下载对应版本源码;
2. 放到工程内目录(如 `<项目>/unity/my_libs/unityLib/UIEffect-main/Packages/src`);
3. `manifest.json` 改成 `file:` 相对路径;
4. Unity 重新编译,确认 `Library/PackageCache/` 下的 `com.coffee.ui-effect@*` 不再被引用。
> T5 的实际路径:`d:\Project\T5\T5_game\unity\my_libs\unityLib\UIEffect-main\Packages\src`
> 需要改的两个文件都在它的 `Runtime/` 下。
### 1.2 版本核对:确认锚点代码存在
补丁靠"上下文行"定位,版本差异大就会 apply 失败。打开目标工程的两个文件,确认下面两段存在:
**`Runtime/UIEffectContext.cs`** —— 找到 `useViewMatrix` 属性(补丁插在它后面):
```csharp
public bool useViewMatrix => m_TransitionFilter != TransitionFilter.None
|| m_GradationMode != GradationMode.None
|| m_DetailFilter != DetailFilter.None
|| m_EdgeMode == EdgeMode.Shiny;
```
**`Runtime/UIEffectBase.cs`** —— 找到 `GetModifiedMaterial()` 里的这两行(补丁要替换它们):
```csharp
var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
```
两段都在 → 走**方式 A**(git apply)。
`useViewMatrix` 不存在(更老的版本)→ 走**方式 C**(手工贴),并按 §3.3 的降级写法处理 `rootId`。
### 1.3 记录基线 DC 数字
改之前先量一次,否则改完无法证明有效:
- 打开目标面板(选 UIEffect 用得最多的那个,T5 用的是 `UI_LevelGiftPanel`);
- `Window → Analysis → Frame Debugger`,Enable;
- 记下左侧事件树里 `Canvas.BuildBatch` 下的 `Draw Mesh` 数量(或右下角 Batches 数字);
- 截图存档。
---
## 第二章 三种移植方式
### 方式 A:git apply(推荐,最快最安全)
补丁里的路径是 `a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/...`,
即**相对目标工程 git 根目录**。T5 的 git 根是 `<项目>/T5_game`(不是 `<项目>`),别站错目录。
```powershell
# 站到目标工程的 git 根目录
cd <目标工程 git 根目录>
# 先干跑,看能不能对上(不写文件)
git apply --check --3way "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\uieffect_configId.patch"
# 没问题再真打
git apply --3way "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\uieffect_configId.patch"
# 确认只动了这 2 个文件
git status --porcelain
```
**目标工程 UIEffect 放在别的路径**(比如 `Assets/Plugins/UIEffect/`)时,用 `--directory` 或 `-p` 调整剥层级,例如:
```powershell
# 补丁里 a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/X.cs
# 目标工程实际是 Assets/Plugins/UIEffect/Runtime/X.cs → 剥掉前 5 层再拼目标前缀
git apply -p5 --directory="Assets/Plugins/UIEffect" --3way "<补丁路径>"
```
`--check` 报错 `does not match index` → 目标文件已被改过或版本不同 → 转方式 B/C。
### 方式 B:直接覆盖两个 .cs(兜底,要求版本一致)
```powershell
$dst = "<目标工程>\unity\my_libs\unityLib\UIEffect-main\Packages\src\Runtime"
Copy-Item "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\UIEffectContext.cs" $dst -Force
Copy-Item "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\UIEffectBase.cs" $dst -Force
```
覆盖前**务必先备份原文件**(或确认在 git 里可回退):这两个文件是完整成品,覆盖会连带丢掉目标工程对它们的任何其它本地修改。
> `.meta` 文件不用管:这是"修改已存在的文件",不是"新增资源",guid 不变,prefab/场景引用不受影响。
### 方式 C:手工贴代码块(版本不同时的唯一选择)
三处改动,照着贴:
**C-1 `UIEffectContext.cs` 文件头加 using**
```csharp
using System;
using System.Collections.Generic;
using System.Reflection; // ← 新增这一行
using Coffee.UIEffectInternal;
using UnityEngine;
```
**C-2 `UIEffectContext.cs` 在 `useViewMatrix` 属性之后插入 121 行**
完整代码见本包 `01_UIEffect材质共享降DC/UIEffectContext.cs` 的 **267~385 行**(`configId` 属性 + `Mix` / `HashValue` / `HashFields` / `GradientHash` + `s_ConfigFields` / `s_FieldCache` 两个静态缓存)。
整块是纯新增,不改任何原有代码,粘到 `useViewMatrix` 与 `private Texture2D _gradationRampTex;` 之间即可。
**C-3 `UIEffectBase.cs` 的 `GetModifiedMaterial()` 换 key**(T5 里约 196~203 行)
```csharp
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
// 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
// root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
var configId = context.configId;
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
if (!MaterialRepository.Valid(hash, _material))
```
注意三个容易漏的细节:
- `baseMaterial.GetHashCode()` 也要一起改成 `baseMaterial.GetInstanceID()`(`GetHashCode` 对 Object 不是稳定 ID);
- 只改 `hash` 那一行、不改 `rootId`,会让 transition 类特效在不同 root 下错误共享材质(视觉错位);
- `effectId` 属性**保留不删**,它是 public API,`UIEffectReplica` 还在 override 它。
---
## 第三章 移植后必做的 5 项检查
### 3.1 编译通过 + 无警告
Unity Console 干净。报 `UIEffectContext does not contain a definition for 'useViewMatrix'` → 见 §3.3。
### 3.2 业务代码有没有绕过 context 直接改材质(**最重要**)
材质从"每实例独有"变成"同配置共享"后,**任何直接改 `effectMaterial` 的代码都会污染所有共享者**。
```powershell
# 在目标工程业务脚本目录执行,命中即需逐个确认
cd <目标工程>\unity\Assets
Select-String -Path (Get-ChildItem -Recurse -Filter *.cs).FullName `
-Pattern "effectMaterial|GetComponent<UIEffect>|UIEffect\[\]" | Select-Object Path, LineNumber, Line
```
判定规则:
- 只读(`var m = e.effectMaterial; if (m == null)`)→ 安全;
- 写入(`e.effectMaterial.SetColor/SetFloat/SetTexture/...`)→ **危险**,必须改成"改 UIEffect 组件的序列化字段(走 context)"或"给该实例单独指定不同配置";
- T5 实测:业务代码 0 命中,无需处理。
### 3.3 `useViewMatrix` 不存在的降级写法(老版本插件)
```csharp
var configId = context.configId;
// 老版本没有 useViewMatrix:保守起见让 root 始终参与,共享范围变小但结果一定正确
var rootId = transitionRoot != null ? (uint)transitionRoot.GetInstanceID() : 0u;
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
```
或者自己在 `UIEffectContext` 里补一个等价属性(判断条件见 §1.2 的代码块)。
### 3.4 UIEffectReplica / UIEffectPreset 兼容性
已核对源码(`UIEffectReplica.cs` 的 `context` override),补丁后两条路径行为不变:
| Replica 工作模式 | context 来源 | 补丁前共享依据 | 补丁后共享依据 | 结论 |
| --- | --- | --- | --- | --- |
| 跟随场景内的 target | `target.context`(同一对象) | `effectId = target.effectId` | 同一个 context → configId 必然相同 | 仍共享 |
| 跟随 preset(或 target 不在场景) | 自己的 `base.context` | `effectId = preset.GetHashCode()` | preset 配置相同 → configId 相同 | 仍共享 |
如果目标工程用了 Replica,改完抽查一处 Replica 的显示是否正常即可(T5 只有 1 个 prefab 涉及)。
### 3.5 IL2CPP / 高 stripping 下的一致性(出真机包前做)
`configId` 依赖反射遍历字段。字段本身被 Unity 序列化系统引用,正常不会被裁剪,但**换平台后建议验一次**:
```csharp
// 临时加在 UIEffectBase.GetModifiedMaterial 里,真机跑一次看日志
Debug.Log($"[UIE] configId={configId} hash={hash} name={name}");
```
同一配置的元素在真机上应打出**相同的 configId**,且与编辑器一致。验完删掉这行日志。
---
## 第四章 控制台报错处理
### 4.1 `Non-matching Profiler.EndSample`
```
Non-matching Profiler.EndSample (Every EndSample call must have a preceding BeginSample call...)
```
成因:`ShaderVariantRegistry.cs` 的 `RegisterVariant` / `GetVariant` 里有**提前 return 分支没配对 EndSample**。
处理:文件在 `Runtime/Internal/Utilities/ShaderVariantRegistry.cs`。检查每个 `Profiler.BeginSample(...)` 之后的**所有** `return` 路径,return 前补 `Profiler.EndSample();`。
> T5 的 5.11.3 版本该文件配对本身已正确(`RegisterVariant` 在 301 行 Begin,307/327/333 三处 End 覆盖全部分支;`GetVariant` 在 338 行 Begin,358/370 两处 End),**无需修改**,所以本包没有归档这个补丁。
> T11 当时是旧版本才需要补。
### 4.2 `NullReferenceException` 出现在 UIEffect 相关调用栈
九成是**删了 UIEffect 组件但业务代码还在 `GetComponent<UIEffect>()` 后用**。
血泪教训(T5 实测踩过):**删任何 UI 组件前,先 grep 业务代码有没有引用它**,确认无引用再删。
本补丁不删组件,正常不会触发;若你在做 DC 优化时顺手清理了"看起来没用的" UIEffect,请务必回查。
---
## 第五章 验证方法(怎么证明 DC 真的降了)
### 5.1 Frame Debugger 对比(最直接)
1. 打开同一个面板(跟 §1.3 基线同一个);
2. `Window → Analysis → Frame Debugger` → Enable;
3. 看 `Canvas.BuildBatch` 下的 `Draw Mesh` 数量,与基线对比;
4. **关键验证点**:在事件树里点几个原本各自独占一批的灰度图标,看右下角 **Material / Texture** 是否变成同一个(材质名相同即共享成功)。
预期:同配置特效密集的面板,批次数量级下降(T5 等级礼包面板 ~101 → 30~40)。
如果只降了一两个批次,说明该面板里 UIEffect 配置本来就各不相同(属正常,见 §7 原因 1)。
### 5.2 材质实例数对比(更硬的证据)
Play 模式下执行(临时挂个测试按钮或用 Console 执行):
```csharp
var all = Resources.FindObjectsOfTypeAll<Material>();
Debug.Log($"UIEffect 材质数: {all.Count(m => m.shader != null && m.shader.name.Contains("UIEffect"))}");
```
补丁前:数量 ≈ 场景内 UIEffect 组件数;补丁后:数量 ≈ **不同配置的种类数**。
### 5.3 视觉回归(必做)
逐个检查这几类特效显示是否正常——它们走的是 `rootId` 参与哈希的分支,是本次改动唯一有行为差异的地方:
- Transition(过渡/溶解)
- Gradation(渐变)
- Detail Filter(细节滤镜)
- Edge Mode = Shiny(发光边缘)
普通灰度(ToneFilter.Grayscale)不涉及 root,风险最低。
---
## 第六章 回滚
```powershell
cd <目标工程 git 根目录>
# 方式 A/B 打的补丁:一条命令还原这两个文件
git checkout -- "unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs" `
"unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs"
```
路径不同的按实际改。回滚后回 Unity 编译即可,不涉及资源/序列化数据,**无副作用**(材质缓存 key 是运行时的,不落盘)。
---
## 第七章 "改完了 DC 还是没降"的 7 个原因
按命中概率排序,逐条排:
1. **配置本来就各不相同**:一个面板里 10 个 UIEffect 用了 10 种参数组合 → 10 个材质,属预期。想再降只能统一美术配置(比如灰度就用同一套参数)。
2. **中间夹了不能合批的元素**:合批要求"同一 Canvas + 同材质 + 同纹理 + 层级相邻"。UIEffect 元素之间插了别的图(不同图集)→ 批次被打断,即使材质相同也合不了。
3. **Mask / RectMask2D 切割**:每个 Mask 内部独立成批,跨 Mask 永不合批。
4. **不同图集(SpriteAtlas)**:材质相同但纹理不同 → 不合批。Frame Debugger 里看纹理名 `sactx-...-atlas-<hash>`,hash 不同就是不同图集。
5. **Canvas 被拆成多个**(子物体挂了 `Canvas` 组件 / `SubCanvas`)→ 各自建批。
6. **TextMeshPro 与 Image 混排**:TMP 用字体图集,跟普通 Image 不同纹理,天然分批。
7. **补丁没生效**:改的是 `Library/PackageCache/` 下的副本(§1.1 没做对),或 Unity 没重新编译(改完必须等编译完成、必要时 `Assets → Reimport All`)。
排查工具与看图技巧见 `05_排查方法论/DC与帧率排查经验.md`。
---
## 第八章 升级插件时的注意事项
UIEffect 版本升级会**覆盖这两个文件**,补丁丢失。所以:
1. 补丁改动务必**提交进 git**,commit message 写明"UIEffect configId 降 DC 补丁"(T5 是 `c61b9012`,T11 是 `fa5f0cdb`);
2. 升级插件后重跑本手册第一章的锚点核对,再按方式 A/C 重新打;
3. 本包 `01_UIEffect材质共享降DC/` 里的两个成品 `.cs` 是 5.11.3 版本,**不要**用它覆盖更高版本的插件文件。