简介:Cocos2d-x游戏中同时加载200个相同Spine动画容易出现卡顿,根源在于资源重复解析与状态更新开销过高。此优化包面向使用Spine3.8的开发者,提供一套可直接落地的性能改进方案,压缩包共238个文件,以C++源码为主,包括87个.h头文件、67个.cpp实现文件,以及编译过程中生成的70个.obj中间文件和Visual Studio工程配置,整体4.27MB,便于代码级审阅与调试。资源覆盖SkeletonJson、SkeletonBinary、AnimationState、SkeletonAnimation等核心模块,展示资源共享、延迟加载、动画池等策略,将多动画加载时的CPU和GPU占用大幅降低,实现200个相同动画瞬间完成加载且不卡帧。该方案已被2929人学习下载,对因Spine动画数量引发性能瓶颈的Cocos2d-x项目具有直接借鉴价值。
1. spine 加载多个相同动画优化:同一个动画为什么会让帧率崩掉?
在一个场景里放 30 个相同的小怪,每个都播放同一个 spine 走路动画,帧率直接从 60 掉到 20。很多人第一反应是“骨骼数太多”或者“图形 API 不支持硬件实例化”,但真正的问题往往出在加载和实例化策略上:同一个动画资源被重复加载了 30 份,或者 30 个 SkeletonAnimation 各自维护了一套完整的 AnimationState 和顶点网格。这个标题要解决的就是:当多个相同或相似的 spine 动画需要同屏时,如何在数据共享、内存复用、渲染合批三个层面把开销压到最低。它适合用 Unity 做战斗场景、塔防或放置类游戏的开发者,尤其是那些会用对象池批量刷怪、刷特效的团队。下面我会按“加载链路 → 复用方案 → 动画参数 → 踩坑排错 → 验证”的顺序,讲一套能直接落地的优化思路。
2. 吃透 spine 的加载模型:实例、资源与 AnimationState 的三角关系
2.1 一次加载与多次实例化:SkeletonData 和 SkeletonAnimation 的区别
Spine 在 Unity 中的运行时通常包含两层结构:SkeletonDataAsset是序列化到磁盘的骨骼、插槽、附件和动画元数据,SkeletonData是从这个 Asset 里反序列化出的不可变数据对象;而SkeletonAnimation是挂在 GameObject 上的组件,它内部持有Skeleton实例(可变状态),以及AnimationState(当前动画播放状态)。很多人以为“加载一个 Prefab”就是只读一份资源,但如果你在代码里对每个小怪都执行一次skeletonDataAsset.GetSkeletonData(true),或者 Prefab 里引用的SkeletonDataAsset没有被全局共享,那么每个实例都可能持有独立的SkeletonData副本。
来看一个常见的错误写法:
public GameObject LoadEnemy(int typeId) { var asset = Resources.Load<Spine.Unity.SkeletonDataAsset>( $"SpineAssets/Enemy_{typeId}"); var go = new GameObject("Enemy"); var sa = go.AddComponent<Spine.Unity.SkeletonAnimation>(); sa.skeletonDataAsset = asset; sa.Initialize(false); return go; }这段代码如果被调用了 30 次,Resources.Load每次返回同一个 Asset 对象,但Initialize(false)是否会让每个SkeletonAnimation各自创建一份SkeletonData?关键取决于SkeletonDataAsset是否被正确缓存。Spine-Unity 的SkeletonDataAsset默认有内部缓存,但一旦你的资产被打入多个 AssetBundle,同一个SkeletonDataAsset被两个 Bundle 分别包裹,运行时就会加载出两份数据。很多“内存怎么降不下去”的问题就是这么来的。
这里要记住的核心区别是:SkeletonData是只读的,包含骨骼层级、权重、动画曲线;Skeleton是每实例一份的可变状态,包含骨骼当前的 localTransform、插槽颜色、当前附件引用;AnimationState也是每实例一份,因为它要记录当前播放的 track、时间线和混合权重。正确的共享策略是:让所有实例引用同一个SkeletonDataAsset,而每个实例只拥有自己的Skeleton和AnimationState。这样加载开销只发生在第一次,后续实例创建都只是 CPU 轻量级的对象赋值。
2.2 内存和 CPU 都花在哪:从加载到换皮到播放的完整链路
一次 spine 动画的加载,先是读取.skel或.json文件,解析生成SkeletonData,然后通过AttachmentLoader把附件中的纹理加载到 GPU 或 CPU 内存。如果项目里每个怪物都有自己的图集,那么 30 个相同怪物本来只需要 1 份图集,但如果你为每个实例都触发了独立的解析流程,内存里就会同时出现 30 份SkeletonData和 30 份附件列表。即使SkeletonData被缓存了,你仍然要为每个实例创建GameObject、MeshRenderer、MeshFilter和SkeletonAnimation组件,这些本身就是不可忽略的 CPU 开销。
播放阶段的 CPU 开销主要来自两处。第一是AnimationState每帧计算骨骼姿态,从动画曲线采样,然后逐骨骼设置本地 transform。第二是SkeletonAnimation的LateUpdate里把骨骼姿态写入 Mesh 的顶点缓冲区,生成新的 vertex buffer。如果你的 30 个角色用的是同一份SkeletonData,但动画状态各自独立,那么第一步的计算就是 30 份。如果连动画也完全相同且进度同步,理论上可以只算一份姿态然后复制给所有实例,但 Spine 的标准运行时不会这么做,因为骨架骨骼状态是每个实例独立存储的,没法直接共享。
GPU 开销更直接:每个SkeletonAnimation默认生成一个独立的 Mesh,即使它们用同一张贴图,只要材质不是共享材质,或者 Mesh 顶点属性不满足 Unity 的合批条件,Draw Call 就是 30 个。这往往比 CPU 的采样开销更早成为瓶颈。尤其是在移动端,30 个尾帧不同的 Mesh 足以让整个帧耗时翻倍。
2.3 先分清瓶颈:是资源重复加载,还是渲染批次爆炸?
优化之前,先用 Profiler 看两类数据。第一类是内存快照:如果 30 个小怪的SkeletonData在 Memory Profiler 里显示 30 份,那是资源重复加载,优先解决共享问题。第二类是 Frame Debugger:如果 Draw Call 数量高企,而且每个小怪都独立成一条渲染命令,那是渲染批次爆炸,考虑图集合并或合批方案。还有一种隐蔽情况:内存里只有 1 份SkeletonData,但 CPU 时间线里Spine.Unity.SkeletonAnimation:LateUpdate的调用次数等于 30 且单次耗时很长,这时候瓶颈在骨骼姿势计算。
我一般会先用 Unity 自带的 Profiler 抓 5 秒真机数据,然后做一个对照实验:把 30 个小怪中的 29 个的AnimationState暂停,只让 1 个动画继续播放。如果帧率立刻回升,说明 CPU 侧的动画状态更新是主因;如果帧率没变化,问题在渲染侧。这个二分法能帮你少走很多弯路,而不是一上来就盲改材质。
2.4 图集与网格:每个实例的 Mesh 为什么会“吃”显存
Spine 的每个SkeletonAnimation最终会生成一个独立的Mesh,这个 Mesh 的顶点数量取决于当前帧可视的附件数量,而不是骨骼数量。比如一个小怪有 30 根骨骼、20 个插槽,但当前只显示了 10 个附件,生成的 Mesh 顶点可能只有 400 到 600 个。看起来不多,但 30 个实例就是 1.2 万到 1.8 万个顶点,每个顶点还要带 UV、颜色、权重信息,显存占用就上去了。更麻烦的是,因为骨骼动画每帧都在变化,Mesh 每帧都要重新上传给 GPU,这个上传带宽也可能成为瓶颈。
所以“加载多个相同动画”的优化,不只是省SkeletonData那份内存,还要从网格生成和上传角度想问题。Spine-Unity 提供了SkeletonRenderer.LateUpdate中的renderMeshes开关,如果某些实例远到看不见,可以完全关闭网格更新。但要注意关闭后动画数据仍然在计算,只是不写进 GPU。更好的做法是把距离阈值应用到AnimationState.TimeScale上,离远了降低动画更新频率,这是后话。
3. 在 Unity 里做 spine 多实例复用:三套可抄作业的方案
3.1 方案A:共享 SkeletonDataAsset,让所有实例读同一份骨骼数据
方案A是最基础的,也几乎是必须做的。做法是:把SkeletonDataAsset作为一个全局可访问的资源,要么挂在场景的一个 Manager 上,要么用Resources.Load之后缓存住,所有实例的skeletonDataAsset都指向它。要注意的是,Initialize不要执行带overwrite=true的重复加载,否则会强制重建内部数据。
public class SpineResourceManager : MonoBehaviour { public static SpineResourceManager Instance; private Dictionary<string, Spine.Unity.SkeletonDataAsset> _assetCache; void Awake() { Instance = this; _assetCache = new Dictionary<string, Spine.Unity.SkeletonDataAsset>(); } public Spine.Unity.SkeletonDataAsset LoadAsset(string key) { if (_assetCache.TryGetValue(key, out var asset)) return asset; asset = Resources.Load<Spine.Unity.SkeletonDataAsset>($"SpineAssets/{key}"); _assetCache[key] = asset; return asset; } public Spine.Unity.SkeletonAnimation SpawnEnemy(string key, Vector3 pos) { var asset = LoadAsset(key); var go = new GameObject("Enemy_" + key); go.transform.position = pos; var sa = go.AddComponent<Spine.Unity.SkeletonAnimation>(); sa.skeletonDataAsset = asset; sa.Initialize(false); return sa; } }逻辑说明:这个 Manager 用字典缓存SkeletonDataAsset,同一个 key 只加载一次。SpawnEnemy里每次 AddComponent 后调用Initialize(false),第二个参数overwrite传 false,意思是如果这个组件之前已经初始化过就复用旧数据,这里新组件没有复用问题,但传 false 能避免某些版本里重复初始化导致的图集引用错乱。
参数说明:key是资源路径唯一标识,建议用怪物的配置 ID 而不是文件名,方便后续换配置时不用改加载逻辑。缓存字典要考虑场景切换时是否清理——如果是常驻大世界,建议不清理;如果按关卡卸载,要在 SceneLoaded 事件里清理引用的资源,否则内存会一直占着。这里有个坑:如果两个不同 key 的 asset 引用同一张图集,图集贴图是共享的,不用担心重复上传,但SkeletonDataAsset本身仍然是两份。
3.2 方案B:对象池 + AnimationState 重置,避免反复 new SkeletonAnimation
方案A只解决资源加载,但每次 Spawn 仍然会 AddComponent、创建 GameObject、创建 MeshRenderer 等,频繁生成和销毁会造成 GC 和 Instantiate 开销。更常见的做法是用对象池,把不用的小怪回收而不是销毁,同时把它的动画状态重置干净。
public class EnemyPool : MonoBehaviour { public GameObject enemyPrefab; private Queue<Spine.Unity.SkeletonAnimation> _pool = new Queue<Spine.Unity.SkeletonAnimation>(); public Spine.Unity.SkeletonAnimation Get() { Spine.Unity.SkeletonAnimation sa; if (_pool.Count > 0) { sa = _pool.Dequeue(); sa.gameObject.SetActive(true); } else { sa = Instantiate(enemyPrefab).GetComponent<Spine.Unity.SkeletonAnimation>(); } // 重置动画状态到初始 sa.AnimationState.ClearTracks(); sa.skeleton.SetToSetupPose(); sa.skeleton.SetSkin(sa.skeleton.Data.DefaultSkin); sa.skeleton.SetSlotsToSetupPose(); sa.AnimationState.SetAnimation(0, "idle", true); return sa; } public void Recycle(Spine.Unity.SkeletonAnimation sa) { sa.AnimationState.ClearTracks(); sa.gameObject.SetActive(false); _pool.Enqueue(sa); } }逻辑说明:ClearTracks会清掉当前所有 track 上的动画,避免回收时还挂着“死亡”动画,下一个实例取出来继续播放“死亡”最后一帧。然后SetAnimation(0, "idle", true)把 0 号轨道设为循环 idle,这样下一次使用就从头开始。中间的三行重置调用尤其重要:SetToSetupPose()让所有骨骼回到绑定姿势,SetSkin(sa.skeleton.Data.DefaultSkin)换回默认皮肤,SetSlotsToSetupPose()把插槽附件也恢复默认。这三行缺一不可,否则你会看到上一只怪手里还攥着武器。
参数说明:ClearTracks会触发 track 的 End 事件,如果项目里监听了这些事件,注意在回收时把监听器摘掉,否则会回调已经回收的对象上的方法。池的初始容量可以按场景最大敌人数量来定。用Queue是先进先出,但如果你希望重复使用最近回收的对象,可以用Stack,不过对于同质化的敌人来说顺序无所谓。还要注意一点:对象池里的GameObject虽然 SetActive(false),但它的SkeletonAnimation仍然挂在对象上,重新激活时不会自动触发Initialize,所以不要在Awake里做资源加载的事。
3.3 方案C:合并相同动画到一张图集,用批渲染来降 Draw Call
即使共享了SkeletonData,每个实例的 Mesh 也不同,Unity 的标准合批对 Mesh 要求很苛刻:必须是同一个材质、同一个贴图,而且顶点流格式必须完全一致。Spine 生成的 Mesh 虽然都是三角形组成的网格,但每个实例的顶点位置不同,且顶点属性频繁变化,所以不属于静态合批范围。Spine-Unity 官方提供的方法是“材质属性块”和“预乘贴图”,但真正管用的还是把多个角色合并到一张图集里,让它们都引用同一个 Atlas 和同一个 Material。这样 Unity 的动态合批有可能把相邻的实例合并到一起,前提是顶点数量不超过合批上限(通常是 900 顶点或 300 顶点,取决于 Unity 版本和合批算法)。
更彻底的做法是放弃多个SkeletonAnimation,改为用一张大的顶点网格承载多个骨骼的“快照”,然后一次性提交。但这就得自己写渲染器,工作量大。对大多数项目来说,方案C指的是图集层面的优化:把多个相同角色的皮肤或动画放进同一个图集,确保它们共享同一个Spine.AtlasAsset。在 Spine 编辑器的“打包设置”中,把多个角色放到同一个 Atlas 页,导出时它们就是一张贴图、一个材质。然后在 Unity 里确认每个SkeletonDataAsset的atlasAssets列表指向同一个AtlasAsset,这样 Draw Call 就能从“每实例一个”降到“每图集一个”,如果合批顺利,甚至更低。
参数注意:同一图集里不要塞太多不同分辨率的皮肤,否则 UV 区域大小不均会导致合批失败或产生不必要的填充浪费。图集大小建议按目标机型适配,老机型 1024 或 2048,新机型可以 4096。纹理压缩格式选择RGBA32,真机上再改为 ASTC 或 ETC2,能显著减少加载和内存占用。
3.4 方案怎么选:对比表
| 方案 | 主要解决的问题 | 改动量 | 适合场景 |
|---|---|---|---|
| 共享 SkeletonDataAsset | 内存中数据重复加载 | 小 | 所有同类动画实例 |
| 对象池 + 状态重置 | 频繁 Instantiate 和 GC | 中 | 刷怪、刷特效、高频创建 |
| 合并图集 + 共享材质 | Draw Call 过高 | 中到大 | 同屏实例多、单动画顶点少 |
三个方案不是互相排斥的。通常项目会先做共享资源和对象池,如果 Draw Call 依然高,再考虑图集合并。如果图集合批后仍然达不到目标帧率,就需要做自定义渲染合并,那已经不是“使用”层面的事,而是二次开发了。
4. 深入 spine 动画状态机:相同动画的参数化重放
4.1 用 SetAnimation 的 trackEntry 控制重复播放
当多个实例播放相同动画时,我们通常希望它们看起来“各不相同”,比如随机错开行走相位。Spine 的AnimationState底层是多个 track,每个 track 可以播放一个动画,并且有 mix、时间缩放、循环次数等参数。对于相同动画,我们可以通过设置不同的trackEntry.TrackTime或trackEntry.TimeScale来实现差异化。先看基本用法:
// 每个实例创建后,随机化初始动画时间 var entry = skeletonAnimation.AnimationState.SetAnimation(0, "walk", true); entry.TrackTime = Random.Range(0f, entry.AnimationEnd); entry.TimeScale = Random.Range(0.9f, 1.1f);这串代码让 30 个小怪的“walk”从不同相位开始,有些快一些,有些慢一些,视觉上不再是整齐划一的复制粘贴。参数说明:AnimationEnd是当前动画的时长(秒),随机范围内取值可以避免所有实例从 0 开始,这一招在塔防里非常常用。TimeScale是倍速,0.9 到 1.1 的抖动不会让人察觉异常,却能让整体画面自然得多。注意SetAnimation的第三个参数是loop,相同动画通常都用 true 循环;但如果某个动画是“攻击”这种一次性动作,不要用循环,而是让它到达末尾后自动清除,再用AddAnimation接回待机。
4.2 TimeScale、MixDuration 与循环播放对流畅度的影响
AnimationState.Data.DefaultMix是全局混合时间,默认是 1 秒,对于相同动画之间的切换,过长的 mix 会让人感觉动画“糊”在一起。我一般会在创建AnimationState时把默认 mix 设为一个较小的值,比如 0.15 秒:
sa.AnimationState.Data.DefaultMix = 0.15f;这个参数用来控制不同动画过渡的权重插值时长。相同动画重复播放时没有过渡问题,但从“受击”切回“待机”时需要有一个 0.1 到 0.2 秒的 mix,看起来才不硬。如果你发现 30 个实例都在做同一个“攻击”动画,但攻击的起手帧对不上,那多半是 mix 时间太长导致每个实例都一直处于上一个动画的残留姿态中。这时记得把 mix 调短。
还有一个容易被忽略的参数:trackEntry.TrackTime在循环播放中超过AnimationEnd后会自动取模,所以如果你手动改TrackTime,要确保它大于等于 0,否则动画可能会闪一帧空白。另外,多个相同动画实例的MixDuration如果设置不一致,同步效果会被破坏,尽量保持每个实例的Data.DefaultMix一致。
4.3 多实例时间偏移的实现与全局同步
有时我们不是想让它们随机错开,而是希望它们严格按时间线同步,比如“全员共同播放同一个施法动画”。这时不要用随机,而是用一个全局时钟来统一设定TrackTime:
public void SyncPlay(Spine.Unity.SkeletonAnimation sa, string anim, float globalTime) { var entry = sa.AnimationState.SetAnimation(0, anim, true); entry.TrackTime = globalTime % entry.AnimationEnd; }这种做法适合演出、BOSS 战或者 UI 特效。注意如果骨骼动画中有相对位移或者包含换装事件,同步时间可能引起骨骼位置跳变,因为TrackTime直接跳到中途,相当于采样了动画曲线上的一个点,而不是从 0 播放。如果你想让角色从开始帧完整播放,要用SetAnimation后等待一段时间再启用角色,而不是直接改时间。
4.4 动画事件与皮肤复用:减少实例化开销的附加手段
Spine 动画可以携带事件帧,比如“脚步落地”“挥刀出风”。当多个相同动画实例同时播放时,事件回调会被触发同样多次,如果在回调里创建特效或音效,就会瞬间产生大量对象。常见的做法是在回调里做合并和限流:只让前几个实例触发特效,或者登记事件后由全局管理器统一生成特效。这个优化往往比省动画计算更明显。
皮肤复用也值得注意:如果用SetSkin换皮,Spine 会重新计算插槽和附件映射,如果多个相同动画实例共享同一个皮肤但不同动画进度,可以预先为每种皮肤生成一个Skin缓存,避免每次创建实例都重新加载皮肤的附件引用。对于“相同动画”这个场景,皮肤通常是一致的,所以这一步主要是减少初始化期间的 CPU 峰值。
5. spine 加载多个相同动画的避坑与排查(常见问题)
5.1 现象:实例一多就卡顿,Profiler 显示加载峰值
现象:游戏运行中,每波敌人刷新时都会卡顿几秒,Profiler 里出现大量Resources.Load或SkeletonDataAsset的加载耗时。原因:资源没有缓存,每次刷怪都重新加载SkeletonDataAsset,或者加载后没有持有引用被 GC 释放了,下次又加载。解决:把所有 Spine 资源预加载到一个常驻 Manager,或者用 AssetBundle 预加载后缓存。切记不要用Resources.Load且不缓存,这样每次都走磁盘 IO。另外检查是不是 Prefab 里引用了不同的SkeletonDataAsset,如果每个 Prefab 都单独拖了一个 asset,即使同一贴图也是两份数据。
5.2 现象:共享资源后动画互相干扰,状态串了
现象:多个实例播放不同动画(一个在攻击,一个在待机),偶尔出现其中一个的骨骼姿势突然变成另一个的。原因:SkeletonData是共享的,但Skeleton和AnimationState独立,如果代码中不小心把某个Skeleton引用传给了另一个实例,或者用了同一个SkeletonAnimation组件去控制两个 GameObject,就会串。排查:检查是否在Awake中把skeletonAnimation赋值到静态变量,或者对象池回收时没有断开事件监听导致回调触发了旧的组件。解决:保证每个实例独立持有组件,回收时调用ClearTracks并移除事件监听器。
5.3 现象:图集合批后出现 Blend 错误或 UV 撕裂
现象:合并图集后,角色身上出现奇怪的透明边、颜色发黑或者贴图接缝处有像素错位。原因:Spine 图集导出时默认带预乘 Alpha,如果材质没有选择对应的Spine/Internal着色器,或者同一张图集里同时有不透明和透明素材,合批时 Blend 状态冲突。解决:统一使用 Spine 提供的着色器,并在 AtlasAsset 的材质上确认Blend模式是Normal还是Premultiply,两者不能混用。UV 撕裂通常是因为图集的 padding 或 extrude 设置太小,重新导出时把Edge Padding调到 2 以上。
5.4 现象:对象池复用时动画残留上一帧的缩放和附件
现象:从池里取出小怪,它身上还挂着上一局的武器附件,或者骨骼缩放不对。原因:SetAnimation只重置了动画状态,没有重置Skeleton中自定义的附件、插槽颜色、骨骼缩放。解决:在复用前调用skeleton.SetToSetupPose(),再调用skeleton.SetSkin("default")以及skeleton.SetSlotsToSetupPose()。前面 3.2 节已经给出了完整代码,这里再强调一点:如果项目里使用过SetAttachment动态挂大头、加特效,那么还需要手动移除所有非默认附件,否则SetSlotsToSetupPose()可能只恢复 setup pose 里登记的附件。
5.5 现象:内存占用没降,Draw Call 却升高了
现象:做了共享资源和对象池后,内存减少了,但 Profiler 里 Draw Call 依然很高。原因:所有实例仍各自拥有一个 Mesh,每个 Mesh 都调用一次 Draw。解决:先看 Frame Debugger 确认这些 Draw Call 是不是都来自同一个材质。如果是,可以通过修改 Mesh 的顶点排序,让 Unity 动态合批生效。具体做法是在创建SkeletonAnimation时,把所有实例的MeshRenderer的additionalVertexStreams统一到一张空 Mesh 上?不,这个不现实。更实际的是调整排序:优先渲染图集里的不同附件时,Unity 会根据 z 位置和材质实例自动合批。如果合批失败,检查每个实例的MeshRenderer的material是否被实例化——如果代码里动过meshRenderer.material,它就会复制出一份新材质,Draw Call 就分开了。正确的做法是只读sharedMaterial,或者用MaterialPropertyBlock修改颜色等属性。
6. 验证优化效果:在真机上用 Profile 数据说服自己
优化做完,第一件事不是看“感觉变顺了”,而是用数据确认。我习惯开一台中低端 Android 真机,用 Unity Profiler 录制 30 个相同敌人刷新的场景,对比优化前后的帧耗时、Draw Call、Mono 堆内存和SkeletonAnimation.LateUpdate的平均耗时。如果没有真机,至少要在 Editor 里关掉 VSync 并取消“Maximize on Play”,但 Editor 的合批行为和真机差别很大,只能作为相对参考,不能作为绝对结论。
录制时注意勾选 Profiler 的 Rendering 模块,重点看RenderForward.Draw这一行,如果它的耗时从 8ms 降到 2ms,说明合批有效;如果耗时没变,看看是不是 Vertex Buffer 上传占用了额外带宽。另一个技巧是抓 Memory Profiler 的内存快照,确认SkeletonData的实例数从 30 变成 1,这是“共享资源”是否成功的最直接证据。
一个我踩过的坑:优化后发现帧率反而低了。原因是把多个动画合并到一张图集后,GPU 加载贴图变大了,而且每帧要提交的网格顶点数并没有减少,反而因为合批失败导致更多的数据拷贝。后来把图集里的角色拆成“粒子换装”和“战斗”两张,战斗专用图集保持紧凑,才真正降下来。所以验证时不要只看一个指标,要同时看“帧耗时、内存、Draw Call”三角。
最后分享一个习惯:每次改完 spine 相关优化,我都会保留一份 baseline 数据文件,记录机型、Unity 版本、场景名称、批次时间戳。下次再调优时直接对比这个基线,而不是凭记忆说“好像流畅了”。希望这个流程能帮到你。
本文还有配套的精品资源,点击获取