This commit is contained in:
ShallowT1Dream
2026-09-10 17:29:24 +08:00
parent 3071ec4b8f
commit 80aa4f0436
20 changed files with 32618 additions and 0 deletions
@@ -0,0 +1,286 @@
# 移植与验证手册
> 配套包:`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 版本,**不要**用它覆盖更高版本的插件文件。