# 移植与验证手册 > 配套包:`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\[\]" | 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()` 后用**。 血泪教训(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(); 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 不同就是不同图集。 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 版本,**不要**用它覆盖更高版本的插件文件。