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

14 KiB
Raw Permalink Blame History

移植与验证手册

配套包: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:

// ✅ 可以改: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 仓库 下载对应版本源码;
  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 属性(补丁插在它后面):

public bool useViewMatrix => m_TransitionFilter != TransitionFilter.None
                             || m_GradationMode != GradationMode.None
                             || m_DetailFilter != DetailFilter.None
                             || m_EdgeMode == EdgeMode.Shiny;

Runtime/UIEffectBase.cs —— 找到 GetModifiedMaterial() 里的这两行(补丁要替换它们):

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(不是 <项目>),别站错目录。

# 站到目标工程的 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 调整剥层级,例如:

# 补丁里 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(兜底,要求版本一致)

$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

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 行)

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 的代码都会污染所有共享者。

# 在目标工程业务脚本目录执行,命中即需逐个确认
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 不存在的降级写法(老版本插件)

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 序列化系统引用,正常不会被裁剪,但换平台后建议验一次:

// 临时加在 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 → 3040)。 如果只降了一两个批次,说明该面板里 UIEffect 配置本来就各不相同(属正常,见 §7 原因 1)。

5.2 材质实例数对比(更硬的证据)

Play 模式下执行(临时挂个测试按钮或用 Console 执行):

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,风险最低。


第六章 回滚

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 版本,不要用它覆盖更高版本的插件文件。