UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技
UWA博客，UWA旗下官方博客。针对游戏及VR应用性能优化等内容记录、交流，以及案例分享。博客主要分为：厚积薄发、万象更新、博观约取、充电一刻、博物纳新等几个模块。
搞定了Adreno底层指标，顺便帮大家把“堆栈一键导出”安排上了
最近这小半年，去各大厂转了一圈，大家聊得最多的就是怎么在现有的盘子里继续“抠”性能、提效率。 过去这两周，我们UWA研发团队其实就闷头干了两件事：一是去死磕底层的硬件指标，帮大家照亮之前的性能盲区；二是往宽了走，想尽办法去优化体验，帮大家省掉那些跨部门沟通时的“无效时间”。 今天借着GOT Online新版本的发布，和大家同步一下我们最新交出的答卷。建议顺手转给组里的程序、TA和QA同学看看，应该能帮大家省下不少时间。 1. 往深了挖 把GPU的黑盒稍微撬开了一点 做移动端优化的同学肯定有体会，查CPU问题还能看堆栈，但遇到一放全屏特效就掉帧的GPU瓶颈，很多时候只能靠经验“盲猜”。要不就是得花一上午去挂载各种复杂的硬件厂商测试工具，不仅费劲，有时候连上真机还会闪退。 这事儿挺折磨人的。所以这次，我们在SDK里硬啃了一块骨头：正式新增了对高通Adreno 8xx系列GPU Counter的数据采集。 这意味着什么呢？以后测试时，不用再去外挂各种繁重的Profiler工具了，跑完GOT Online，直接在UWA的报告里就能看到GPU最底层的频率、利用率、带宽这些硬指标。到底是顶点处理算多了，还是片元着色器压力太大，数据直接摊在台面上，一秒定乾坤。 顺带一提，引擎兼容我们也跟进了，Unity 2020及以上版本，现在全平台都支持准确的DrawCall和Triangle统计了。 2. 往宽了走 能不能让修Bug的沟通少点“火药味”？ 测出问题只是第一步，能顺畅地把问题流转到对应的同学手里修掉，才是真正的有效沟通。我们在这期版本里，重点解决几个高频的体验痛点： 堆栈终于能一键导出了（极度推荐试试）： 以前QA同学在线上报告里看到某个异常耗时，要把长长的函数调用链一层层点开截图，再贴到Jira里。程序员看了截图经常觉得没头没尾。 现在，线上看盘的函数堆栈支持一键导出离线文件。QA直接把文件当附件丢给研发，程序在本地打开，树状结构清清楚楚，是谁的模块耗了多少毫秒，一目了然，彻底终结“扯皮”；同时也可以直接发给开发团队内部的Agent，智能定位耗时瓶颈，告别手动排查，极大节省分析损耗。 全局函数搜索，不用大海捞针了： 面对多线程里乱如麻的耗时数据，找个特定的逻辑就像大海捞针。我们在Unity Overview里加了“全局函数搜索”功能。想看某个子线程到底阻塞在哪？敲个回车，直接定位。 大世界的“上帝视角”： 很多做大箱庭、大世界项目的团队反馈，几十个场景挨个看报告太累了。所以我们在场景性能表里加了个“总览”维度。整个项目的性能盘子怎么样、哪个地块不合格，一屏扫完，并且可以直接导出Excel，方便主程和制作人开评审会时直接贴进周报里。 3. 一些你可能用得上的顺手小优化 除了上面那些，我们在很多日常操作上也做了顺水推舟的优化： 1.更全面的默认配置：默认Overview模式的堆栈采集里，我们加上了CanvasRenderer.Synctransform调用次数，因为我们发现这个函数导致的UI卡顿在越来多的项目中表现突出。这个函数把RectTransform世界坐标/旋转/缩放同步到CanvasRenderer的渲染矩阵，为后续BuildBatch网格合批、UI顶点渲染做数据准备，是UI高耗时的高发元凶。 2.UE门槛再降低：虚幻引擎的指标向来让人头大，我们在UE显存快照页增加了详细的释义和获取条件指引，刚接触UE的新人也能看得懂。 3.找报告更方便：项目支持了文件夹分类，GOT Online的历史报告也支持打Tag（标签）分类了，再也不用在几百份报告里肉眼找数据了。 4.新增Unity 2023版本Overview模式中资源管理模块的采集统计。 5.新增SetUWAToolsIP接口，支持小游戏边测边传IP配置与上传Pipeline控制。 最后说两句： 工具的价值，不仅在于它能算得多快，更在于它能不能真的融入团队的日常流转中，帮大家准时下班。 我们深知，在项目期引入或者尝试一个新工具是有成本的。为了让大家能毫无负担地验证这波更新的疗效，我们特别开放了「两周GOT Online免费试用通道」，并且会提供专属的性能专家诊断名额。 不管你们现在卡在什么性能瓶颈上，联系我们的商务同学，UWA的技术专家会一对一陪着你们跑通测试，直击问题。 关于UWA UWA是一家创业十年的高新技术企业，作为游戏行业的深耕者，UWA始终专注于为使用Unity、Unreal引擎的开发者提供丰富的优化产品，帮助开发者高效解决开发问题、定位性能瓶颈、提供解决方案，已支持超过一万款游戏项目。还打造了技术博客、问答、开源库、学堂等社区产品，为开发者提供便利和高效的支持。线上培训和线下教育的新业务，满足行业对人才培育的需求。
UE Toon渲染半影着色实现指南：纯色法与Ramp法详解
【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ 一、前言 在动画或者卡通渲染存在一个很常见的效果，光源的边界染上一个饱和度较高的颜色，这个效果业内一般称作半影着色： 《吹响吧！上低音号》 《别当欧尼酱了》 《绝区零》 在HDRP中也有类似的效果： Unity里的实现非常简单，就是拿一个纯色来给半影染色： 除了Unity的这种实现方式，还有一种常见的实现方法是用阴影来采样Ramp： https://zhuanlan.zhihu.com/p/588991753 https://zhuanlan.zhihu.com/p/688482736 二、实现思路 2.1 纯色法 首先在光源组件上添加新的参数： LightComponent.h 接下来正常把这些参数传给Shader就行，半影颜色的计算直接抄HDRP： ToonLightCommon.ush 在光照计算的时候调用ComputeShadowColor： DeferredLightingCommon.ush 接下来就能看到效果了： 半影着色_1 效果还挺不错的，多光影也可以完美兼容： 不过，有一个尴尬的问题，从上面的视频效果图可以发现，主光的半影着色并没有作用在Toon角色上，这也正常，毕竟Toon角色的主光是在ToonLightPass里单独计算的。 按照正常的逻辑，我们应该把染过色的Shadow传给ToonLightPass，但是，这样需要再用3个通道来存储阴影，之前的ToonShadow已经满了，这样得新创建一张，有些浪费了。 所以我干脆还是把半影的参数传到ToonLightPass，然后在ToonLightPass自己去计算。 这样做还有个好处，如果觉得半影颜色在角色上有点太强了，很容易可以调节强度： 半影着色_2 2.2 Ramp法 同样的，在光源上添加新的参数： 传参方式和以往有所区别，如果把Ramp贴图放到FLightShaderParameters里的话会报错： SceneManagement.h 所以我把贴图放到FDeferredLightUniformStruct里，FToonLightUniformParameters也一样： LightRendering.h ToonLightPassRendering.h 采样贴图的时候由于不同Pass里Buffer的名字不一样，所以在Shader上添加宏来区分： LightRendering.cpp ToonLightPassRendering.h ToonLightCommon.ush 在材质编辑器里也使用相同的函数来计算半影： 看看效果： 三、参考和链接 3.1 链接 本文的修改都上传GitHub了，对应下图的提交记录： https://github.com/Yu-ki016/UnrealEngine/tree/YK_Engine 示例工程： https://github.com/Yu-ki016/YKEngineExampleProject 3.2 参考 [1] Unity URP 8Cascade+PCF/PCSS 阴影+Compute Shader屏幕空间shadowmap+阴影渐变 [2] 图形引擎实战：8级风格化级联阴影 这是侑虎科技第1990篇文章，感谢作者Yu-ki016供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者主页：https://www.zhihu.com/people/yu-ki-016 再次感谢Yu-ki016的分享，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859）
1）AssetBundle粒度过粗导致Remapper占用上升 2）CPU发热问题优化 这是第479篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 本次推送的实战案例来自于使用UWA服务的项目的真实且典型的问题。UWA将关键线索、定位路径与处理建议整理成了可复用的案例笔记，便于大家快速对照、排查自身项目中的同类问题。 实战案例 Q：我们在UWA GOT Online报告里发现，PSS内存会在某些场景出现明显暴涨，通过Memory Profiler定位到Remapper占用很高。想咨询一下，是不是AssetBundle包分得太细导致的？ A：方向正好相反 —— 问题不是包分得太细，而是打得太粗了。 通过PSS走势图结合资源列表的生命周期，很快就能定位到导致内存暴涨的源头：资源列表里一个关键AssetBundle —— defaultpackage_share_assets_art_prefab_scenes.bundle。只要它被加载，内存就会出现明显暴涨。 进一步查看AssetBundle检测报告，就会发现异常信号：Bundle本体大小达到232MB，但包内并不包含大量纹理、网格等典型大体积资源；同时在Resource模式下，AssetBundle资源类的内存占用就有22MB，一个没有主流资源的包，本体却超过200MB，本身就说明包内资源组织有问题，也侧面印证了Memory Profiler里看到的Remapper高占用。 基于以上信息，建议重点排查该Bundle的打包结构，主要确认三件事：是不是把多个场景打进了同一个Bundle、是不是包含了大量当前场景实际用不到的资源、是不是公共资源归属不合理导致依赖聚合。从以往项目经验看，如果只保留当前实际需要的那一个场景，内存占用通常不会出现这种量级的上涨。 为什么“包打得太粗”会同时拉高Remapper和PSS？ 当多个场景被打进同一个AssetBundle时，即使当前只使用其中一个场景，Bundle中大量其他资源及其依赖关系仍会被加载到内存。与此同时，Bundle内对象数量和依赖关系增加，Unity维护对象映射和资源引用关系所需的数据结构也会随之膨胀，Remapper占用因此显著推高。部分资源在实际被访问后，还会继续贡献PSS。 所以经常会看到Remapper和PSS同时上涨，但两者并不完全等价。Remapper反映的是Unity内部对象映射和引用关系的开销，PSS反映的则是进程实际占用的物理内存，是同一问题在不同维度上的表现。 优化建议 控制AssetBundle粒度 场景类资源尽量保证一个场景对应一个Bundle，避免一次加载就引入大量无关资源和依赖关系。 用UWA AssetBundle检测工具排查结构 重点关注三类高风险Bundle：本体异常大的、依赖节点过于集中的和引用关系特别复杂的场景资源包。通过资源依赖路径通常可以快速定位问题。实际项目中，这类异常大的Bundle往往对应几种典型情况：如多个场景混打、公共资源归属不合理、场景资源与共享资源耦合过深、资源依赖链过长等，都是上线前性能验收阶段值得关注的检查项。 实战案例 Q：项目针对功耗问题已经做了多轮优化，目前GPU压力和带宽问题得到了明显控制，GPU温度也相应下降。但CPU侧温度问题仍然突出，请问后续应如何优化CPU发热问题？ A：从GOT Online报告中的温度曲线来看，当前设备发热主要由CPU侧贡献，其中部分区段温度有所回落。降频虽然降低了功耗，但同时也会降低处理器执行效率，导致相同工作量需要更长时间完成，因此需要重点关注。CPU发热优化的核心目标仍然是降低CPU压力，可以从以下三个方向入手。 一、逻辑代码层 优先排查LuaBehaviourUpdater.Update、LuaBehaviourUpdater.LateUpdate以及协程中的尖刺耗时，这类节点经常出现数十甚至数百毫秒的单帧开销，多数与资源加载和实例化等操作相关。另外在报告中发现，战斗场景下，LuaBehaviourUpdater.Update往往还会存在持续性的逻辑开销。从堆栈上观察到大量耗时集中在C#层，建议在关键业务节点增加PushSample/PopSample打点，以进一步定位真实热点函数。 二、物理模块 如果项目当前并未使用刚体模拟，仅依赖Raycast等物理查询功能，则可以考虑进一步优化Physics模块。 关闭自动物理模拟 将Physics的SimulationMode设置为Script，仅在需要时主动执行物理模拟，从而避免每帧固定产生的物理步进开销。 谨慎使用Auto Sync Transforms 对于大多数项目而言，更推荐关闭Auto Sync Transforms。因为该选项会在Transform发生变化时自动同步到物理系统，频繁移动对象时可能产生额外CPU开销。如果关闭后出现物理查询结果异常，可以在必要位置手动调用Physics.SyncTransforms()，从而兼顾性能与正确性。 三、动画模块：将部分计算转移至GPU 如果逻辑层和物理层的优化空间已经较为有限，可以进一步关注动画系统。在战斗场景中，Animator、SkinnedMeshRenderer以及骨骼蒙皮计算往往会占据较高CPU开销。将部分动画计算转移至GPU，通常能够有效降低CPU负载，从而降低发热。 需要注意以下两点： 1. GPU Skinning方案需要实测评估 Unity内置的GPU Skinning、Compute Shader GPU Skinning以及第三方GPU Skinning插件都各有适用场景。最终收益与角色数量、骨骼规模、平台特性以及Unity版本密切相关，因此建议结合项目实际情况进行测试验证，而不是直接采用固定方案。 2. 关注整体功耗而非单项耗时 CPU功耗与频率通常并非线性关系。很多情况下，CPU频率每下降一个档位，功耗下降幅度会明显大于性能下降幅度。因此，即使GPU负载略有增加，只要能够降低CPU大核占用率，整体发热与续航表现往往仍会得到改善。这也是许多大型手游后期采用GPU Skinning、Animation Baking等方案的重要原因。 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
在当前的移动游戏市场，开放世界、高写实画质已成为头部产品的标配。随之而来的是虚幻引擎在资源管理上的巨大压力，而中端机型（尤其为iOS平台）通常只有4-6GB的可用内存，引擎层、渲染层、逻辑层以及复杂的资源堆积，使得OOM（Out of Memory）成为了悬在开发者头上的达摩克利斯之剑。 传统的性能分析手段往往面临“数据孤岛”并存在明显短板：Stat Memory过于宏观、颗粒度粗；MemReport生成慢、输出量大，无法捕捉瞬时内存尖峰；驱动层显存分配更是完全黑盒。 今天，UWA正式发布内存分析功能升级，从引擎层到驱动层，打造了一套覆盖LLM内存明细、RHI显存穿透与Vulkan驱动层盲区的全链路分析方案，让内存优化从“盲猜”变成“可量化、可定位、可落地”的确定性工作。 —— 从此，没有“看不见的内存”。 1. LLM（Low Level Memory Tracker） 可视化赋能 虚幻引擎在4.18之后引入了LLM，这本是一个极强的底层追踪工具，但由于其原始输出是枯燥的统计数值，大部分团队仅将其用于基础的模块占比查看。我们对LLM数据进行了深度可视化封装，让开发者告别“对着日志大海捞针”的低效优化： 在UWA报告的系统层面，通过PSS/进程内存曲线实时监控整机内存、Swap与显存占用，直观捕捉内存泄漏、瞬时尖峰与系统内存吃紧风险，快速判断项目整体内存健康度。 报告既展示总内存变化趋势，也拆分出已追踪、未追踪内存明细，同时按平台、图形、堆内存等模块拆解占⽐，定位“⿊盒内存”与资源级占⽤问题。 在LLM内存细分模块中，我们将STAT_AsyncLoadingLLM、STAT_EnginePreInitLLM、STAT_UILLM等核心模块的内存数据，转化为直观的趋势曲线，完整记录整个运行周期内的内存峰值、变化节点与场景对应关系，一眼识别内存泄漏与瞬时尖峰。 2. Vulkan驱动层盲区破解 在Android平台上，Vulkan驱动层的内存分配策略，是性能不稳定的重要根源。UWA针对Vulkan进行了专项优化，能够精准捕捉VkDeviceMemory的分配行为。 同时，UWA深入RHI底层接口，对于定位“纹理残留”尤为关键。很多时候CPU侧对象已释放，但因资源引用链未断、RHI延迟释放或驱动层缓存机制，显存并未真正回收。UWA能够通过这种双端对比，直接指出那些“逻辑上已释放但物理上仍占位”的资源泄露点。 报告罗列详细的RHI资源，RenderTargetPool中的对象及其具体属性，清楚了解显存的占用情况。 3. 从宏观到微观 构建UE内存优化的“确定性闭环” 本次升级的三大模块并非孤立存在，而是形成了一套完整的优化闭环： 先通过LLM模块趋势，快速锁定内存大户与异常波动节点； 再通过Vulkan驱动层分析，解决平台特有的盲区与碎片问题。 最后通过RHI显存下钻，定位显存泄漏与冗余资源。 从宏观内存大盘到微观资源明细，从引擎层到驱动层，UWA打破传统工具的数据壁垒，把过去靠经验、靠运气的内存优化，变成可量化、可定位、可落地的确定性工作。 我们深知：性能优化不应是上线前的“救火”，而应是研发全周期的“防火”。让每一分内存都花在刀刃上，让每一次上线都充满底气。 关于UWA UWA是一家创业十年的高新技术企业，作为游戏行业的深耕者，UWA始终专注于为使用Unity、Unreal引擎的开发者提供丰富的优化产品，帮助开发者高效解决开发问题、定位性能瓶颈、提供解决方案，已支持超过一万款游戏项目。还打造了技术博客、问答、开源库、学堂等社区产品，为开发者提供便利和高效的支持。线上培训和线下教育的新业务， 满足行业对人才培育的需求。
16G高端机反而比6G低端机更吃内存，差值究竟从哪来？
1）同一个包，高端机为什么反而更吃内存？ 2）iOS内存预算与优化策略 这是第478篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 本次推送的实战案例来自于使用UWA服务的项目的真实且典型的问题。UWA将关键线索、定位路径与处理建议整理成了可复用的案例笔记，便于大家快速对照、排查自身项目中的同类问题。 实战案例 Q：我们用UWA GOT Online跑兼容性测试时发现一个反直觉的现象 —— 同一个包在16G机型上PSS Total比6G机型还高几十兆，Reserved Total两边一致，差异全在“其它”里。这“其它”是什么？ A：这不是测试误差，原因在于PSS Total的统计方式。PSS（Proportional Set Size）会将共享库按进程数进行均摊，不同设备由于系统服务、驱动模块以及ROM组件差异，导致共享库占用规模不同。因此，即使运行同一个游戏包，不同机型的PSS Total也可能出现数十MB甚至更大的差异。通常高端机系统层更复杂（高版本Framework、厂商定制更多、图形栈更重、预装服务多），加载的共享库种类和体积往往更大，分摊到游戏进程的份额自然也更多 —— 这就是为什么同一个包跑在高端机上PSS Total反而更高。这部分开销引擎层看不到，体现在PSS Total比Reserved Total多出来的差值里。 那么报告里引擎层的“其它”是什么？ 它与PSS多出来的部分不同 —— “其它”是Reserved内部未单独归类的资源，主要包括： 内存碎片预留 Scene Object（GameObject/Transform/ MonoBehaviour） 序列化文件 Cubemap等未统计资源 因此，同一个包在16G机型上比6G机型高出几十兆是正常现象，与包体无关。 内存分析建议： PSS Total反映系统整体压力，但其绝对值可能因共享库均摊而被“垫高”，跨机型横向对比容易误判； Reserved Total衡量引擎可控资源； 如果PSS Total与Reserved Total差值持续扩大，说明引擎层未统计的内存（例如Native Heap、图形驱动缓存、第三方SDK占用或文件映射内存）在增长，需要结合系统工具进一步排查。 实战案例 Q：我们目前使用GOT Online测了Android版本，Android系统上3G左右的内存峰值并没有触发大量闪退。后续要上iOS系统，有什么有优化策略吗？ A：优化思路基本通用，但iOS的内存压力通常更大。 为什么差异明显？ 当前主流Android版本的中高端机型普遍在8GB以上内存，3GB左右的内存峰值在多数设备上通常仍有一定安全余量，不一定会直接引发闪退； iOS虽然近几代iPhone也普遍在8GB内存，但由于系统采用严格内存管理机制，单个应用可使用的内存预算受系统控制。超过预算可能触发Jetsam机制被系统终止，闪退阈值通常低于3GB，而且触发时机精准。 很多项目反馈一致：Android闪退率低，但切换到iOS平台后，内存闪退问题集中暴露。因此iOS最好提前规划，避免上线再补救。 iOS优化策略： 缓存放得更保守 iOS CPU性能较强，可以接受更频繁的加载与释放。与其长期驻留资源，不如多做几次加载，把峰值压下去。 打包时开启Increase Memory Limit 对于iOS 15/16及以上系统设备，可评估开启Xcode的Increase Memory Limit，允许应用申请更高内存预算，在部分设备上可降低Jetsam闪退。但实际提升因设备型号和系统版本而异，需要真机验证。 复用Android优化成果 针对Android做的系统层优化（Shader、AB缓存、Native占用等）在iOS同样有效，无需重复实现。仅少数使用不同插件的情况需要单独处理。 建立低内存机型测试标准 开发阶段以3GB~4GB预算作为压测目标，而不是依赖高端测试机数据。可更早发现资源常驻、加载峰值和缓存策略问题，避免上线后集中爆发。 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
5 月刊｜GPM 2.0 实现全场景可视化溯源、多维度数据解析与根因精准定位
手游性能研发在长期迭代中，几乎都会遇到各类难以根治的疑难问题：玩家偶现闪退、卡顿，但线上日志无法还原真实场景；内存异常波动，难以区分系统占用与项目内存泄漏；iOS端故障频发，却缺少直观画面佐证；玩家反馈体验不佳，无法精准判别是操作逻辑问题还是性能瓶颈所致。 此前，UWA GPM 2.0已凭借全域性能采样、线上实时监测、异常数据统计能力，助力众多游戏团队实现性能常态化管控。如今，GPM 2.0重磅升级，新增iOS平台截帧、PSS内存细粒度解析、深度崩溃等问题分析、玩家点击事件追踪、卡顿分析汇总等五大核心能力，完成从单纯「数据监测」到「场景溯源+根因定位」的跨越式升级，彻底告别性能问题盲猜式排查。 01 iOS平台截帧 此前GPM 2.0已实现安卓、鸿蒙、PC平台的全场景线上溯源，依托实时截帧能力快速还原卡顿、崩溃等异常现场，让性能与画面问题有据可查。 本次更新，GPM 2.0针对性补齐手游核心营收阵地iOS平台核心需求，上线无损实时截帧功能，在不损耗帧率、不增加设备功耗的前提下，精准捕捉UI错乱、黑屏白屏、场景加载异常、贴图错误等异常瞬间画面。 GPM 2.0异常Session报告示意图（含iOS平台截帧） 结合平台原有时序数据记录能力，实现性能数据与真实画面双向联动对照，一眼锁定iOS端画面异常、适配故障与突发问题。同时，该能力支撑玩家行为深度洞察，打破iOS端“只看数据、无场景分析”的局限，可结合画面对照玩家实操状态，精准判别问题源于设备性能、版本适配还是玩家特殊操作，让iOS端性能优化与玩家体验打磨双向落地。 02 内存数据详细拆解 GPM 2.0可有效监测游戏整体内存波动、峰值与异常抖动，帮助团队快速发现内存超标、运行不稳等基础问题。但面对内存持续走高、场景退出不回落等现象，研发难以定位根源，无法区分问题来自程序占用、系统共享内存、堆栈冗余或资源堆积。诸多内存泄漏、资源未释放、常驻内存冗余等隐性隐患，需要耗费大量人力逐一排查，优化效率极低。 本次更新，GPM 2.0完成了内存监测全方位升级，告别粗放式统计，新增十大细分内存类型精细化监测，涵盖Total、VSS、RSS、swap、Java Heap、Native Heap、Code、Stack、Private Other和System全维度数据。 展示详细内存分配 依托时序曲线与场景串联核心能力，研发可直观查看各类内存的实时占用、波动变化及场景联动数据，精准锁定堆内存溢出、代码段冗余、堆栈占用过高等具体问题，让内存优化摆脱经验猜测，每一次调优都有精准数据支撑，高效攻克各类内存顽疾。 03 问题分析集中度聚合 由于线上崩溃分散在不同机型、系统、版本与渠道，人工梳理集中度和问题趋势，难以适配高效的迭代排查需求。为此，GPM 2.0进行了全面优化，平台可自动聚合多维度崩溃/ANR/错误日志数据，直观展示故障在各机型、系统、版本、渠道的分布情况，无需手动整理，快速锁定问题爆发范围。同时全新上线智能标签体系，以崩溃上报功能为例，可以自动为崩事件分类打标，区分新增崩溃、启动崩溃、问题优化/恶化等状态，助力研发快速筛选风险、定位高频故障。 此外，平台前置展示崩溃修复优先级、判定依据及受影响玩家榜单，基于崩溃类型、上报趋势、影响范围自动完成P0–P3分级，清晰标注判定逻辑，高频受害账号与故障样本清晰可查。整套体系将人工统计、分类、研判的工作自动化，省去报表整理、优先级核对等冗余环节，让研发聚焦问题修复，大幅提升崩溃治理效率。 04 玩家点击事件追踪 以往GPM 2.0可精准监控帧率波动、卡顿耗时等全局性能数据，清晰还原游戏整体性能表现，但性能数据无法对应玩家实际操作行为。玩家反馈的操作延迟、点击无响应、技能卡顿、UI交互失灵等核心体验问题，对于研发来说也至关重要。 GPM 2.0新增玩家操作全轨迹追踪能力，彻底打通性能数据与用户体验的壁垒，完整记录UI点击、屏幕滑动、技能释放、场景交互等全部操作行为，实现玩家轨迹、性能时序、异常截图三者精准绑定。 当出现体验故障时，可直接定位对应操作与交互动作，终结交互卡顿、操作延迟的“玄学排查”，让游戏体验优化精准落地、有据可依。 05 多维聚合定位高频卡顿 本次GPM 2.0全新上线卡顿分析功能，彻底解决测试与线上环境脱节、数据碎片化的排查难题。平台可自动收拢、整合所有报告中的卡顿帧堆栈核心信息，告别人工汇总梳理的低效模式。 同时系统主动从版本、游玩场景、设备机型、GPU型号等多核心维度，智能计算线上卡顿数据集中度，可视化呈现真实玩家场景下的卡顿高发区间与共性特征，精准捕捉测试环境遗漏的各类隐性故障。 研发无需逐一筛查海量数据，就能直观掌握玩家在哪些游戏场景、哪些操作流程、哪类设备环境下最容易出现卡顿问题，快速区分是场景资源加载、机型适配、GPU性能瓶颈还是版本代码问题导致的卡顿故障，精准锁定优化靶点，大幅降低卡顿问题的排查难度，高效完成游戏全局卡顿体验优化。 从iOS异常场景可视化、内存数据精细化拆解，到崩溃智能聚合、玩家行为全链路追踪、卡顿问题多维定位，GPM 2.0持续深化性能监测能力，以更完整的数据维度、更直观的场景还原、更精准的根因定位，为游戏研发提供高效可靠的线上问题排查工具。未来我们将继续围绕真实项目需求持续迭代，不断强化AI分析与智能诊断能力，与开发者共同打造更稳定流畅的游戏体验。 立即体验！ 专属通道限时开放 ➡ 预约技术专家1V1部署指导！我们将全程支持服务搭建、数据分析与反馈，确保您能够充分体验GPM 2.0带来的价值。 邮件：sales@uwa4d.com 微信号：17502188376 了解GPM 2.0全功能，可以点击《GPM 2.0 核心功能全景指南｜重塑游戏性能主动管控能力》查看详情。 关于GPM 2.0 UWA GPM 2.0是专为上线及测试阶段游戏项目打造的高性能监测平台。它不仅能深度捕捉宏观性能数据，更创新采用性能无损截帧技术，在不影响玩家体验的前提下，助力开发者全面掌握玩家端运行关键细节，从多维度优化游戏性能，实现从“玩家投诉后救火” 到 “问题发生前预警” 的核心转变。 GPM 2.0全景指南 《UWA GPM 2.0 核心功能全景指南｜重塑游戏性能主动管控能力》 《4 月刊｜GPM 2.0核心玩家守护功能上线》 《3 月刊｜GPM 2.0性能数据探索 & 单玩家追踪升级，精细化性能分析再突破》 《2 月刊｜GPM 2.0错误日志分析上线，PC端监测能力全维度突破》 《1 月刊｜日报周报一键获取+AI问答，玩家行为精准分析》 《12月刊｜上新！全局筛选+设备标记+序列帧回溯，GPM 2.0性能分析效率拉满》 《GPM 2.0 功能更新月报：新增导出表格、崩溃反混淆等实用功能，体验再升级！》 GPM 2.0新功能 集锦 《数字助理已到岗！GPM AI 智能体发布，游戏性能分析进入全自动时代》 《GPM 2.0全新功能发布｜GPU精准监测 + 精细化运营，重构游戏性能管控新范式》 《GPM 2.0 SDK 热更新｜不打扰玩家的“隐形升级术”！》 《GPM 2.0功能尝新｜AI识图精准捕获材质丢失，全链路护航游戏品质！》 《UWA GPM 2.0 崩溃上报功能全面升级｜全场景覆盖+免费开放，让崩溃排查不再卡壳》 《大R流失、问题难复现？GPM 2.0「指标 - Session直连」功能，让异常定位效率翻倍！》 《从 “被动救火” 到 “主动掌控”：GPM 2.0 如何重塑游戏性能竞争力？》 《GPM 2.0 告警功能：游戏性能的智能守护者》 《无限接近真相｜GPM 2.0 功能上新》 《崩溃现场全回溯｜GPM 2.0「崩溃上报分析」上线，终结“无法复现”的噩梦》 《GPM 2.0 全新发布「卡顿帧堆栈」功能｜精准高效定位游戏玩家端性能》 《深入解析「卡顿帧堆栈」》 《解锁GPM 2.0「卡顿帧堆栈」｜代码示例与实战分析》 《如何精准打点解决卡牌、SLG、开放大世界、放置类游戏卡顿难题》 《不妥协每一毫秒的性能 不浪费每一字节的流量》 《GPM 2.0 重新定义性能监测与优化｜从未知到已知，从被动到主动》 《UWA GPM 2.0 重新定义性能监测与优化｜如何高效预警和定位玩家端的性能问题》 GPM 2.0案例精讲 集锦 《携手护航亿级体验｜《迷你世界》× UWA GPM 2.0 性能管控最佳实践》 《我们的目标是不耗电、不发烫、不闪退》 《冒险闯关类玩家关注的四大体验》 《玩家端性能数据 一手掌握》 《造成设备功耗高的罪魁祸首》 《海外市场设备适配性优化实战》 《5000人测试背后的秘密》 GPM 2.0实操篇 集锦 《GPM 2.0 按游戏类型定制性能监控策略｜「告警」功能实战指南》 《GPM 2.0 让每一帧的流畅兑换真金白银》 《GPM 2.0 实操之Session篇之如果闪退是因为内存崩了》 《GPM 2.0 应用场景实例之解决让人头疼的问题》 《GPM 2.0 助力设备监测、行为分析与性能优化｜玩家共测新体验》 《GPM 2.0 API 功能助力定制你的性能优化》 《GPM 2.0 驱动优化与投放决策｜硬件性能一目了然》 《GPM 2.0 多维数据与实时截帧完美结合》
Unity预计算辐照度全局光照PRTGI实践与拓展（上）
【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ 在知乎平台上，最早由网易根据育碧在GDC上分享的《Global Illumination in Tom Clancy's The Division》提出了实时PRTGI（预计算辐射度全局光照）技术的实现思路。随后，AKG4e3提供了示例工程，其内容涵盖从理论到实践的PRTGI实现过程（参见《预计算辐照度全局光照（PRTGI）从理论到实战》）。方木君则在《Unity 移动端可用实时GI方案细节补充》中，对局部光源（Local Light）的重光照（Relight）及优化方案进行了扩展。 本文内容基于上述项目的Fork版本，在学习过程中进行了进一步完善与扩展。部分修改已记录于本人的Fork仓库（AkiKurisu/UnityPRTGI）中。但由于后续开发需要在Forward渲染路径下进行，相关修改已集成至其他项目，该Fork版本已停止维护。 后续还将结合其他在URP下实现的相关Feature进行整理与开源，敬请期待。 流程概述 先总结一下《预计算辐照度全局光照（PRTGI）从理论到实战》的项目中的流程，方便后续对比： 离线烘焙生成Surfel（总计512*Probe） 按Probe顺序存储Surfel 运行时Probe拿到对应的512个Surfel Relight所有Probe 烘焙提速 原作者使用Camera.RenderToCubemap来抓取Cubemap，这个函数在GPU上的开销实际不大，手动渲染每一个面的成本并没有减少，但可以考虑改成使用另一个拓展方法，来在构建RenderList的时候忽略非静态物体： public static bool RenderToCubemap( this Camera camera, Texture target, int faceMask, StaticEditorFlags culledFlags); 通过不同场景的性能测试，烘焙这里更拖延速度的是CPU侧设置Material Shader的开销，由于需要分别设置Shader来采样Position、Albedo、Normal数据，实际开销约等于 次Shader设置，时间复杂度为 所以优化方式就是使用一个Shader，烘焙时只需要设置一次，每个Probe烘焙时通过切换Keyword来抓取所需数据。 #pragma multi_compile _ _GBUFFER_WORLDPOS _GBUFFER_NORMAL float4 frag (v2f i) : SV_Target { #if defined(_GBUFFER_WORLDPOS) // Output world position return float4(i.worldPos, 1.0); #elif defined(_GBUFFER_NORMAL) // Output world space normal return float4(i.normal, 1.0); #else // Default output albedo half4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv) * _Color; return albedo; #endif } private static void SetGlobalGBufferCaptureMode(GBufferCaptureMode captureMode) { // Enable the specific keyword based on capture mode switch (captureMode) { case GBufferCaptureMode.WorldPosition: Shader.EnableKeyword("_GBUFFER_WORLDPOS"); Shader.DisableKeyword("_GBUFFER_NORMAL"); break; case GBufferCaptureMode.Normal: Shader.DisableKeyword("_GBUFFER_WORLDPOS"); Shader.EnableKeyword("_GBUFFER_NORMAL"); break; case GBufferCaptureMode.Albedo: Shader.DisableKeyword("_GBUFFER_WORLDPOS"); Shader.DisableKeyword("_GBUFFER_NORMAL"); break; } } // 对于每个Probe执行下面的代码 SetGlobalGBufferCaptureMode(GBufferCaptureMode.WorldPosition); camera.RenderToCubemap(_worldPosRT, -1, StaticEditorFlags.ContributeGI); SetGlobalGBufferCaptureMode(GBufferCaptureMode.Normal); camera.RenderToCubemap(_normalRT, -1, StaticEditorFlags.ContributeGI); SetGlobalGBufferCaptureMode(GBufferCaptureMode.Albedo); camera.RenderToCubemap(_albedoRT, -1, StaticEditorFlags.ContributeGI); 如此一来时间复杂度为O(1)，大大提升了复杂场景下的烘焙速度。 球谐优化 由于Unity 2023、Unity 6相关版本未在国内普及，其新增的APV（Adaptive Probe Volume）系统，国内相关技术讨论较少。然而，作为Unity官方新推出的GI方案，APV有大量值得学习的优化技巧。 PRT（Precomputed Radiance Transfer）和APV实际上师出同门，共通点是都不需要UV2、比LightMap方便，需要烘焙，仅支持静态场景，两者的核心差异在于： APV：每个Probe离线存储球谐系数，不支持动态光照（如TOD系统），但因为存储了SkyVisibility，可以实现动态的天光遮蔽。 PRT：存储Radiance数据和SkyVisibility，支持动态光照。 PRT缺点也很明显，因为要存储的数据更多了，使得实践上Probe放置的密度小于APV的密度，这使得高频的Diffuse信息会被忽略，所以更适合（半）室外。 尽管应用场景不同，APV作为Unity官方实现，其优化策略具有重要的参考价值。 原项目SH.hlsl中的球谐函数实现使用大量条件判断： // 老版本 - 大量分支判断 float SH(in int l, in int m, in float3 s) { if (l == 0) return kSHBasis0; if (l == 1 && m == -1) return kSHBasis1 * y; if (l == 1 && m == 0) return kSHBasis1 * z; // ... 更多条件判断 return0.0; } // 在循环中重复调用 for (int shIndex = 0; shIndex < 9; shIndex++) { contribution = SHProject(shIndex, dir) * totalRadiance * 4.0 * PI / SampleCount; // 每次调用都要执行完整的条件判断逻辑 } 这种GPU上的条件分支会导致Warp Divergence，且影响性能。 下面是参考Unity的SphericalHarmonics.hlsl的实现方式，使用向量化方式来优化。 对照用的公式，数学不好不会推也没事： 向量化版本： // 新版本 - 向量化计算 void EvaluateSH9(in float3 dir, out float sh[9]) { float x = dir.x; float y = dir.y; float z = dir.z; // L0 (constant) sh[0] = 1.0; // Constant term (will be multiplied by kSHBasisCoef[0]) // L1 (linear) sh[1] = y; // Y_1_-1 sh[2] = z; // Y_1_0 sh[3] = x; // Y_1_1 // L2 (quadratic) sh[4] = x * y; // Y_2_-2 sh[5] = y * z; // Y_2_-1 sh[6] = 3.0 * z * z - 1.0; // Y_2_0 (Equals 2.0 * z * z - x * x - y * y) sh[7] = x * z; // Y_2_1 sh[8] = x * x - y * y; // Y_2_2 // Apply kSHBasisCoef to get the final SH basis values [unroll] for (int i = 0; i < 9; ++i) { sh[i] = sh[i] * kSHBasisCoef[i]; } } // 优化后的使用方式 float shCoeffs[9]; EvaluateSH9(dir, shCoeffs); // 一次计算所有系数 [unroll] for (int shIndex = 0; shIndex < 9; shIndex++) { contribution = shCoeffs[shIndex] * totalRadiance * 4.0 * PI / SampleCount; // 直接数组访问，无分支判断 } 这种方式可以连续访问，性能更好。 除了向量化外，APV系统中一个巧妙的优化是在球谐系数上预除PI，减少Radiance转为Irradiance时的ALU开销： // Clamped cosine convolution coefs (pre-divided by PI) // See https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/ #define kClampedCosine0 1.0f #define kClampedCosine1 2.0f / 3.0f #define kClampedCosine2 1.0f / 4.0f static const float kClampedCosineCoefs[] = { kClampedCosine0, kClampedCosine1, kClampedCosine1, kClampedCosine1, kClampedCosine2, kClampedCosine2, kClampedCosine2, kClampedCosine2, kClampedCosine2 }; 这个优化基于Sébastien Lagarde的经典文章《Pi or not to Pi in game lighting equation》。什么时候改乘PI也是实时渲染中一个比较经典的问题，例如URP中的Lambert BRDF就没有除PI，目的是简化灯光流程，让灯光颜色调整时所见即所得。 3D纹理 原作者存储球谐是将27位float存在一个巨大的ComputeBuffer中，这导致需要使用定点数Encode并且使用较多的原子操作。 这个方式弊病很多，一方面没有利用GPU的优势（对3D纹理的硬件优化），另一方面原子操作导致写入前需要Clear，需要使用双缓冲来维护，内存翻倍。 // 使用定点数存储小数, 因为 compute shader 的 InterlockedAdd 不支持 float // array size: 3x9=27 RWStructuredBuffer<int> _coefficientSH9; // storage to volume if(_indexInProbeVolume >= 0) { constint coefficientByteSize = 27; int offset = _indexInProbeVolume * coefficientByteSize; for(int i = 0; i < 9; i++) { InterlockedAdd(_coefficientVoxel[offset + i * 3 + 0], EncodeFloatToInt(c[i].x)); InterlockedAdd(_coefficientVoxel[offset + i * 3 + 1], EncodeFloatToInt(c[i].y)); InterlockedAdd(_coefficientVoxel[offset + i * 3 + 2], EncodeFloatToInt(c[i].z)); } } 我将其修改为probeSizeX，probeSizeZ，probeSizeY*9大小，格式为RGB111110Float的3D纹理。虽然这样还是会有一定的CacheMiss，但相比使用ComputeBuffer来存储球谐系数性能更好，并且可以方便在FrameDebugger中查看。 // Layout: [probeSizeX, probeSizeZ, probeSizeY * 9] RWTexture3D<float3> _coefficientVoxel3D; if (_indexInProbeVolume >= 0) { // Write to 3D texture int3 texCoord = ProbeIndexToTexture3DCoord(_indexInProbeVolume, index, _coefficientVoxelSize); _coefficientVoxel3D[texCoord] = groupCoefficients[0]; } 需要注意Relight时为了计算MultiBounce我们依然需要访问上一帧的球谐系数，这使得在一个线程中可能存在同时访问和写入的可能，所以只是将ComputeBuffer修改为3D纹理后，还不能去除双缓冲，还需要之后的几步优化。 并行规约 由于改成3D纹理，我们需要解决原来作者没处理的球谐系数求和问题，这本质是GPU中的多线程求和问题即并行规约问题。 无需掌握底层原理，英伟达直接提供了最佳实践： 《Optimizing Parallel Reduction in CUDA》 在CS中实现起来非常简单，我们有512个Thread，刚好是2次幂，因此可以直接使用PPT中的方法3。 // Parallel reduction for (uint stride = 256; stride > 0; stride >>= 1) { if (groupIndex < stride) { groupCoefficients[groupIndex] += groupCoefficients[groupIndex + stride]; } GroupMemoryBarrierWithGroupSync(); } 由于利用了多线程能力，带宽换时间，性能大概提升2倍，还有两个进阶版本可以更有效利用带宽，但代码实在有些繁琐，用第三种基本足够了。 但需要注意这里如果直接存所有二阶球谐系数（27个float）到LDS后再并行规约，可能会导致超出阈值或使用了过多寄存器造成性能下降，为了消除该问题，我将二阶球谐的9个维度放在循环里分别进行规约。 UNITY_UNROLL for (int shIndex = 0; shIndex < 9; shIndex++) { float3 contribution = ...; groupCoefficients[groupIndex] = contribution; GroupMemoryBarrierWithGroupSync(); // Parallel reduction for non-power-of-2 size for (uint stride = ThreadCount / 2; stride > 0; stride >>= 1) { if (groupIndex < stride) { groupCoefficients[groupIndex] += groupCoefficients[groupIndex + stride]; } GroupMemoryBarrierWithGroupSync(); } // Write results if (groupIndex == 0 && _indexInProbeVolume >= 0) { uint3 texCoord = ProbeIndexToTexture3DCoord(_indexInProbeVolume, shIndex, _coefficientVoxelSize); _coefficientVoxel3D[texCoord] = groupCoefficients[0]; } GroupMemoryBarrierWithGroupSync(); } 但这样明显会增多同步次数，后续我们会再次来优化这部分。 分帧Relight 由于现有方法是需要每帧遍历所有Probe进行Relight，这导致场景越大或Probe密度越大，Relight成本越高，时间复杂度为$O(N_\text{probes})$。为了性能可控，我们可以利用Diffuse GI低频的特点，将Relight的步骤分摊到多帧。 void DoRelight(CommandBuffer cmd, PRTProbeVolume volume) { volume.SwapCoefficientVoxels(); // 如果是多帧Relight，则不需要清空体素 if (!multiFrameRelight) volume.ClearCoefficientVoxel(cmd); // May only update a subset of probes each frame using (ListPool<PRTProbe>.Get(outvar probesToUpdate)) { volume.GetProbesToUpdate(probesToUpdate); foreach (var probe in probesToUpdate) { probe.ReLight(cmd, _relightCS, _relightKernel); } } // Advance volume render frame volume.AdvanceRenderFrame(); } // 滚动获取当前帧要更新的Probe public void GetProbesToUpdate(List<PRTProbe> probes) { for (int i = _currentProbeUpdateIndex; i < _currentProbeUpdateIndex + probesToUpdateCount; i++) { probes.Add(Probes[i]); } } public void AdvanceRenderFrame() { // Advance the update index for next frame _currentProbeUpdateIndex = (_currentProbeUpdateIndex + probesToUpdateCount) % Probes.Length; } 回到文章《Global Illumination in Tom Clancy's The Division》的方案，这并不是使用简单的轮询法（Round Robin），而是将一组组Probe划分为一个个Sector，每帧Relight两组，并且对于相机周围的Probe再额外Relight一组。 这里是否要划分Sector笔者觉得不太重要，但Relight玩家相机附近的Probe确实是有必要的，我们可以修改为每帧计算相机附近的Probe，添加到上面的GetProbesToUpdate中。 /// /// Update local probe indices based on camera position /// private void UpdateLocalProbeIndices() { if (!_mainCamera || Probes == null || Probes.Length == 0) return; Vector3 cameraPos = _mainCamera.transform.position; // Only recalculate if camera has moved significantly if (Vector3.Distance(cameraPos, _lastCameraPosition) < CameraMovementThreshold) return; _lastCameraPosition = cameraPos; _localProbeIndices.Clear(); // Convert camera position to probe grid coordinates for more efficient distance calculation Vector3 gridPos = (cameraPos - transform.position) / probeGridSize; // Calculate distances from camera to all probes using grid coordinates using (ListPool<(int index, float distance)>.Get(outvar probeDistances)) { for (int i = 0; i < Probes.Length; i++) { if (Probes[i]) { // Calculate probe position in grid coordinates Vector3 probeGridPos = (Probes[i].transform.position - transform.position) / probeGridSize; // Use squared distance for efficiency (avoiding sqrt) float sqrDistance = (gridPos - probeGridPos).sqrMagnitude; probeDistances.Add((i, sqrDistance)); } } // Sort by distance and take the closest ones probeDistances.Sort(static (a, b) => a.distance.CompareTo(b.distance)); int count = Mathf.Min(localProbeCount, probeDistances.Count); for (int i = 0; i < count; i++) { _localProbeIndices.Add(probeDistances[i].index); } } } Forward+多光源适配 理论上只要添加_FOWARD_PLUS宏后就可以使用了，但从URP 14后会遇到一个离谱的编译问题。 Can't find included file `Packages/com.unity.render-pipelines.ps5/ShaderLibrary/API/FoveatedRendering_PSSL.hlsl` CS的编译似乎无视了SHADER_API_PS5宏，导致找不到平台文件报错，问题是散修开发者也没PS5平台的引擎拓展。 国内有开发者咨询了Unity官方，得到的AI答复是：URP14.0.7及之后的版本下计算着色器库文件引用问题。 因此在不修改源码的情况下，最佳的解决方案就是本地创建一个空的com.unity.render-pipelines.ps5库，里面写一个空的FoveatedRendering_PSSL.hlsl。 然后我们在LightLoop前加上下面的代码，初始化Cluster需要拿到Surfel的屏幕坐标和世界坐标： #if _FORWARD_PLUS float2 uv = ComputeNormalizedDeviceCoordinates(surfel.position, UNITY_MATRIX_VP); InputData inputData = (InputData)0; inputData.normalizedScreenSpaceUV = uv; inputData.positionWS = surfel.position; #endif uint pixelLightCount = GetAdditionalLightsCount(); LIGHT_LOOP_BEGIN(pixelLightCount) // 这里会创建Cluster // Light Loop... LIGHT_LOOP_END 体积雾适配 《Global Illumination in Tom Clancy's The Division》文章分享中也谈到了体积雾可以在Raymarch时同时采样PRT Volume，笔者这里使用了一个开源的体积光方案： CristianQiu/Unity-URP-Volumetric-Light 把其中采样APV的贡献改成采样PRT Volume后，就实现了类似的效果： 阴影缓存 《Global Illumination in Tom Clancy's The Division》和《实时PRTGI技术与实现》文章中都提到不在视锥内的物体会被CSM剔除，因此对于离屏物体，我们需要添加一个Shadow Cache来保留最近一次有效的主光源阴影信息。 // mainlight shadow float4 shadowCoord = TransformWorldToShadowCoord(surfel.position); if (!BEYOND_SHADOW_FAR(shadowCoord)) { // Shadow is valid, sample and update cache atten = SampleShadowmap( TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, GetMainLightShadowSamplingData(), GetMainLightShadowParams(), false ); // Update shadow cache with new valid result _shadowCache[surfelGlobalIndex] = atten; } else { // Shadow is invalid, use cached result if available atten = _shadowCache[surfelGlobalIndex]; } Surfel合并Brick 我们回过头看下现在的数据存储，对于每个Probe我们都存放了其512个Surfel数据，如果两个Probe挨着很近，那很大概率Surfel的数据是比较重复的，对于离得很近、方向基本一致的Surfel，我们实际可以清理一部分冗余数据。 《Global Illumination in Tom Clancy's The Division》文章中的全境封锁给予了一个方案，即根据Grid大小（4×4×4）和Surfel的法线的主方向来聚集为Brick。同一个Brick中的Surfel数据就可以提取一下特征（比如对于坐标相同、法线方向相近的Surfel进行合并）。 下面是数据结构： /// /// Represents the indices of a Surfel /// [Serializable] publicstruct SurfelIndices { publicint start; publicint end; } /// /// Represents a 4x4x4 brick containing merged Surfels /// publicclassSurfelBrick { publicreadonly List<int> SurfelIndices = new(); publicreadonly HashSet<PRTProbe> ReferencedProbes = new(); } SurfelBrick即为烘焙时的Brick存储结构，由于Surfel不再唯一对应一个Probe，我们还需要在烘焙期间存储Probe的引用关系，直到存储数据时再扁平化为索引。 从实现细节上来讲，对于每个Probe完成Sample后，需要将Surfel注册到一个具有HashGrid结构的BrickManager中（根据Surfel世界坐标和主方向计算Hash），BrickPool找到对应位置的SurfelBrick将其添加或合并，并记录引用的Probe。 其次因为Surfel被合并为Brick，Probe不再直接引用其烘焙阶段命中的512个Surfel，在序列化前我们需要额外的数据来存储Relight时Probe所需的数据。 参考文章《Global Illumination in Tom Clancy's The Division》，下面是一个示例： /// /// Factor structure: contains Brick index and the contribution weight of that Brick to the Probe /// [Serializable] publicstruct BrickFactor { publicint brickIndex; publicfloat weight; } /// /// Factor range: each Probe stores the range of Factors it uses /// [Serializable] publicstruct FactorIndices { publicint start; publicint count; } 这里BrickFactor对应了一个Brick对于一个Probe的贡献权重，可以离线通过Brick中所有Surfel的平均法线计算，空间换时间。FactorIndices即是一个Probe所对应的Factor范围。 最后我们根据Probe顺序将FactorIndices、BrickFactor、SurfelIndices和Surfel进行排序，尽可能保证数据访问时的连续性。 结合上面的数据结构，下面是从烘焙到使用的新流程： Probe发射512个射线采样生成Surfel Surfel合并聚集到Brick Brick平均法线计算Probe贡献系数，存到Factor中 存储Factor、Brick、以及合并后的全部Surfel 运行时Volume拿到全部Surfel、Brick、Factor数据，提交GPU Relight所有Brick Relight所有Factor 为了验证数据正确，这里优先编写一下Brick的Gizmos视图，方便在编辑器看到各个Brick对选中Probe的贡献值以及Brick中各个Surfel方向是否朝向一致。 我们对比下性能，因为Surfel数据大量进行了合并，Relight Brick开销非常小，而Probe在Relight时采样的Brick数量也远远小于原先的512个，因此开销也有所下降。但需要注意这个合并实际会让GI精度下降，所以对于室内部分，我认为肯定是需要结合SSGI使用的。 注意这里开启了Multi Frame Relight来控制每帧更新的Probe数量（这里为1帧15个Probe）。 在我们完成Surfel和Probe的Relight分离后，还可以额外获得两个免费的优化效果： 由于不再存在3D纹理的写入和读取冲突，Relight不再需要历史帧缓冲，可以减少一张3D纹理使用。 由于ProbeRelight所需的Brick变少了（从512减到小于256），足够我们将二阶球谐27个float直接写入LDS中。现在的并行规约代码如下： #define ThreadCount 256 // Max Brick Num groupshared float3 groupCoefficients[9][ThreadCount]; // Compute all SH coefficients at once using vectorized approach float shCoeffs[9]; EvaluateSH9(dir, shCoeffs); float weight = 4.0 * PI / sampleCount; // Process each SH coefficient and store to LDS UNITY_UNROLL for (int shIndex = 0; shIndex < 9; shIndex++) { float3 contribution = shCoeffs[shIndex] * totalRadiance * weight; groupCoefficients[shIndex][groupIndex] = contribution; } GroupMemoryBarrierWithGroupSync(); // Parallel reduction for non-power-of-2 size for (uint stride = ThreadCount / 2; stride > 0; stride >>= 1) { if (groupIndex < stride) { UNITY_UNROLL for (int shIndex = 0; shIndex < 9; shIndex++) { groupCoefficients[shIndex][groupIndex] += groupCoefficients[shIndex][groupIndex + stride]; } } GroupMemoryBarrierWithGroupSync(); } 那么上半部分就是这些了，下半部分我们继续优化和拓展PRTGI。 参考文章 [1] 实时PRTGI技术与实现 [2] 预计算辐照度全局光照（PRTGI）从理论到实战 [3] Unity移动端可用实时GI方案细节补充 [4] Global Illumination in Tom Clancy's The Division [5] Pi or not to Pi in game lighting equation 这是侑虎科技第1983篇文章，感谢作者AkiKurisu供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者主页：https://www.zhihu.com/people/akikurisu 再次感谢AkiKurisu的分享，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859）
1）首启耗时过长？三大环节快速定位瓶颈 2）UI粒子卡顿？机型分级适配优化方案 这是第477篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 本次推送的实战案例来自于使用UWA服务的项目的真实且典型的问题。UWA将关键线索、定位路径与处理建议整理成了可复用的案例笔记，便于大家快速对照、排查自身项目中的同类问题。 实战案例 Q：目前我们项目首次启动耗时在20多秒，首启体验较差，但二次启动速度就正常很多。我们知道其中一部分耗时来自资源下载，现在希望把首次启动时长压缩到10秒以内，想请教一下小游戏启动耗时主要受哪些因素影响？具体应该从哪些方面着手优化？ A：小游戏首次启动耗时主要分为三大核心环节：首包资源下载、WASM代码下载与编译，以及引擎和首场景初始化。找准具体耗时阶段后，才能更有针对性地进行优化。 1. 首包下载阶段 启动速度与首包体积密切相关，可借助资源分析工具搭配分包策略，清理包内闲置冗余素材。不同小游戏平台首包限制标准存在差异，行业内通常建议将首包体积控制在5MB以内，以此缩减下载耗时，提升启动成功率。 2. WASM代码阶段 尽量精简WASM代码包体积，可以使用代码分包插件，启动时只需加载主包，后续再延迟加载子包，可大幅降低下载与编译耗时；不过需要注意在Develop中不支持分包，如果已经分包建议以Release测试为准；还可以勾选“Strip Engine Code”并尽可能提高裁剪等级、清理项目中的无用模块或第三方插件以降低代码量。 3. 引擎初始化阶段 可以借助平台性能调试工具定位主线程阻塞点，优化首场景加载逻辑，遵循“先出画面、后补内容”的原则，将非核心资源和次要逻辑延后异步加载，从而有效缩短用户可感知的白屏时间。 针对以上问题，可借助UWA GOT Online输出小游戏性能报告，拆解启动全流程各阶段耗时明细，从而快速定位首启缓慢的根本原因。 实战案例 Q1：除了启动速度问题，我们还存在UI粒子特效的性能难题。不仅小游戏端存在这类问题，移动端APP同样也会受到影响。目前美术团队在使用专门的UI粒子管理工具，工具会在每帧执行粒子模拟与刷新逻辑，一旦场景内UI粒子数量增多，整体性能开销就会明显上涨。如果单纯删减粒子效果，美术团队又会觉得画面表现达不到预期，想问问有没有比较合适的平衡方案？ A：想要平衡粒子特效的视觉效果与设备性能，不能单纯依赖删减特效，更推荐采用轻量化优化思路。 在制作UI粒子时，建议优先使用简单面片粒子（Quad粒子），尽量避免使用Mesh粒子或复杂模型粒子，从源头降低渲染压力。同时需要合理控制粒子整体数量，减少多层级嵌套的粒子子系统。 高消耗复杂特效可酌情替换为序列帧动画，以更低的运算成本还原接近的视觉效果；但序列帧动画通常会带来更高的显存与内存占用，大批量使用时，低端设备容易出现卡顿甚至崩溃问题，因此需要合理控制替换规模。 Q2：这个思路我们之前也尝试过，但实际落地时还是会比较纠结。粒子效果一旦精简，美术团队通常会觉得画面表现下降明显；但如果完全保留，在部分中低端机型上又很容易出现掉帧。像这种UI特效，实际项目里一般会怎么做平衡？ A：确实如此，这也是小游戏开发中非常常见的情况。 相较于“一刀切”地关闭特效，更合理的方案其实是做机型分级适配。高端设备保留完整粒子效果；中端设备适当降低粒子数量、刷新频率与发射规模；低端设备则简化或关闭部分非核心UI粒子特效。 通过按照设备性能进行差异化配置，既能够尽可能保留美术设计效果，也能有效控制整机性能开销，在画面表现与运行流畅度之间取得平衡。 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
1）为什么Mesh合得越大，GPU压力反而更大 2）Bloom和DOF，到底有多吃GPU 这是第476篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 本次推送的实战案例来自于使用UWA服务的项目的真实且典型的问题。UWA将关键线索、定位路径与处理建议整理成了可复用的案例笔记，便于大家快速对照、排查自身项目中的同类问题。 实战案例 Q：我们发现GPU端顶点处理开销很高，想咨询下是什么原因导致的，以及有什么解决方案？从UWA GOT Online报告中可以看到，大地图场景总三角面约36万个，而当前帧有效显示区域对应的三角面规模仅约4万个。 A：这是典型的大范围Mesh合并导致CPU侧裁剪粒度下降的问题。 将大量空间跨度较大的小网格合并为单个超大Mesh后，Unity在CPU侧进行可见性裁剪时，会以整个Renderer的包围盒作为判断单位。只要该包围盒与摄像机视野发生相交，整份Mesh数据就可能继续进入GPU处理流程。 项目中已经存在比较明显的超大Mesh问题。从报告中的Mesh资源详情可以看到，某Mesh资源占用达到11MB，包含约21万顶点。结合场景分布情况来看，大概率存在跨区域的大范围Mesh合并行为。 这种粗暴合并方式，会显著降低CPU侧裁剪效果。即便屏幕内仅显示其中很小一部分区域，大量原本不需要参与当前帧渲染的数据，依然可能进入GPU前端处理阶段。 虽然现代GPU具备硬件级裁剪能力，部分不可见三角形会在后续阶段被剔除，但额外的：顶点处理开销、管线调度开销和数据传输开销已经产生，最终导致GPU压力和带宽升高。 解决方案 不建议手动将大量跨区域物件直接拼接为单个超大Mesh。 相比人工大范围Mesh合并，更推荐优先使用Unity原生的官方机制，例如： Static Batching（静态合批） GPU Instancing（GPU实例化） SRP Batcher 进行合批时，需要重点控制空间局部性，避免将跨区域、跨可视范围的大量物件直接合并为同一个Renderer。 更合理的做法是：保留适当的网格拆分粒度，让引擎能够正常进行CPU侧裁剪。这样可以在CPU阶段提前剔除不可见物体，从而减少无效数据提交与GPU管线压力。 需要注意的是： 降低DrawCall，并不一定等于GPU性能提升。如果盲目追求远距离合批，导致包围盒范围过大，反而会削弱CPU侧裁剪效果，让大量原本不可见的数据持续进入GPU管线。 最终出现： DrawCall降了，但GPU压力反而更大了。 实战案例 Q：Bloom、DOF后处理功耗高、GPU性能压力大，该如何优化？同时画质分级该如何配置？ A：报告信息显示如下： Bloom优化 Bloom的性能开销，主要来源于高分辨率渲染纹理的多级下采样与上采样流程。 从当前报告来看，1920×958的高画质渲染分辨率配置合理，建议将Bloom下采样倍率从1/2调整为1/4。 在场景高亮元素较少、画面细节可接受的前提下，优先改用1/4倍率下采样，能减少高分辨率渲染纹理的读写与缓存开销，显著降低显存带宽压力。 修改后可对照GOT Online报告功耗曲线与真机画面表现，在保证画面效果基本稳定的前提下选用更低的下采样倍率。 DOF优化 景深DOF对GPU算力与显存带宽消耗极高，优化原则如下： 先确认业务是否必须开启，若视觉效果提升有限，可直接关闭； 优先选用Gaussian DOF，避免使用Bokeh DOF，后者性能开销远高于前者； 中低画质档位机型建议直接禁用DOF； 避免使用DOF模拟画面边缘压暗效果，可改用URP原生的Vignette（暗角）后处理，性能开销更低。 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
Unity小游戏优化简谱 | 吃透底层逻辑，告别掉帧与流失
做Unity小游戏的开发者，大概都遇到过这样的困境：明明在编辑器里跑得好好的，打包成WebGL上线后，却出现掉帧、发热、启动慢、甚至闪退的问题。用户一卡就流失，好不容易拉来的流量也留不住。这篇文章，就从小游戏的底层原理讲起，帮你一步步定位性能瓶颈，把这些“隐形杀手”逐个击破。 1. 前言 本章从“什么是小游戏”出发，简要介绍小游戏的概念、适用范围与开发原理，并梳理Unity小游戏性能优化的必要性及问题定位的基本思路，为后续各章节的深入讨论提供背景框架。 1.1 什么是小游戏 “小游戏”是一个较为宽泛的概念。广义上，它通常指轻量、上手快、适合碎片化时间游玩的游戏；而对开发者而言，它更多是指依托于特定平台运行的游戏应用。例如微信在2017年推出的爆款小游戏“跳一跳”，无需额外安装，直接在微信内即可游玩。如今，微信、抖音、快手、QQ、支付宝等多个平台都提供了小游戏入口，品类也日益丰富。 这类基于常用平台的小游戏往往无需下载、安装或卸载，即点即玩，体验更轻便。除即时体验外，小游戏还能借助平台的账号体系、好友关系等能力，具备较强的社交属性，例如排行榜与邀请等功能。相较于APP手游，小游戏在类型上更偏轻松休闲，但也不乏玩法更复杂的重度产品。近年来，不少APP游戏也移植了小游戏版本，借助平台流量实现快速获量与更高的用户留存。 1.2 适用范围 本文主要针对Unity WebGL转微信小游戏（下文简称微小）的性能优化场景，文中的经验数据、工具链说明和优化建议大多基于此方案。部分结论（如性能差异倍数、内存阈值、WASM编译内存估算等）不一定适用于Cocos/Laya/Godot等其他引擎，也不一定适用于抖音、快手等非微信平台。读者在参考时请结合自身项目的引擎、宿主平台和运行环境具体分析。 1.3 小游戏的开发原理 各类小游戏平台通常面向多样化的开发需求，支持开发者使用不同的游戏引擎或技术方案进行开发，常见包括Unity、Cocos、Godot、Laya等。小游戏平台并不直接绑定具体的游戏引擎，而是以引擎构建的产物作为接入与运行的基础。各引擎均有对小游戏的适配与支持，能输出合适的产物在小游戏环境中运行，具体适配原理可参考对应引擎的官方文档，此处不再展开。当前简谱主要针对使用Unity引擎开发并转换的小游戏项目。 以上常见的游戏引擎可分为两类：基于JavaScript/TypeScript的HTML5游戏引擎，以及原生目标的游戏引擎，Unity属于后者。两种类型的适配方式有所不同。对于Unity项目而言，适配方式基于WebAssembly技术，将游戏代码导出为WASM代码，通过胶水层代码运行在浏览器环境中。这种方式的优势在于保持原有引擎工具链和技术栈不变，开发者无需重写游戏核心逻辑，通过转换工具即可完成小游戏适配。 1.4 为什么要关注Unity小游戏的性能 玩家遇到掉帧、卡顿、发热、闪退等性能问题时，无论游戏美术是否精良、玩法是否有趣，体验都会直接变差，黏性随之下降。小游戏易上手、节奏快，遭遇性能问题后玩家流失会更快。 此外，相比于APP，小游戏即开即用的特点使玩家对启动耗时更加敏感 —— 从点开到进入正式场景的时间过长，用户流失的可能性就会越大。 从技术角度来看，Unity小游戏以WebAssembly（WASM）+ WebGL为核心技术方案，运行性能会极大影响可承载的游戏内容玩法。开发者需要关注不同技术栈带来的性能差异，以及不同系统平台（Android、iOS、Windows PC）之间的性能差异。 1.4.1 Unity WebGL与APP的运行性能差异 CPU性能差异： Unity WebGL以WASM虚拟机的形式运行在类浏览器环境中，CPU算力受限于虚拟机的执行效率。 在多数小游戏运行环境和Unity WebGL转小游戏方案中，常规C# 多线程/Job多线程能力受限，不能按原生APP的线程模型使用，导致AI、动画、渲染等模块无法获得多线程加速。新版Unity Web平台已提供WebAssembly threads支持入口，但标准C# 多线程仍有限制，且依赖浏览器/宿主对SharedArrayBuffer等能力的支持。 这是导致Unity WebGL与APP存在性能差距的最主要因素。作为常见经验值/保守预算，Unity WebGL通常约为APP手游性能的1/3，开发者应特别关注CPU侧的性能瓶颈。但需明确，不同机型、Unity版本、是否开启iOS高性能模式、业务瓶颈在CPU还是GPU，都会显著影响这一比例。 GPU性能差异： Unity以WebGL API进行渲染，其中WebGL 1.0相当于OpenGL ES 2.0，WebGL 2.0相当于OpenGL ES 3.0。 WebGL在原生渲染API之上封装存在少量开销，但基本渲染能力与原生APP接近。 GPU Instancing、SRP Batcher等渲染特性需要WebGL 2.0。当APP手游使用了这些特性而小游戏未开启WebGL 2.0时，性能差距会进一步拉大。 1.4.2 WASM与JS的运行差异 WASM是静态类型的二进制指令格式（虚拟机字节码），其类型系统使JIT优化能更准确地预判运行期类型，因此能更快达到JIT指令优化后的峰值。在计算密集型、类型稳定的场景中，WASM通常更容易获得稳定性能，但具体差距依赖引擎、宿主、代码形态和平台，不能简单以固定倍数对比。 Unity引擎目前未针对浏览器环境做充分优化（如WASM与宿主接口互调频次偏高、未做充分的代码路径裁剪），整体较为臃肿，部分应用场景反而逊于JS的轻量实现。 因此，两者在实际使用中不能简单以语言算力对比，需以实测游戏为准。 1.4.3 系统平台之间的性能差异 Android与Windows PC：Android平台（以微信小游戏为例，通常基于XWeb/Chromium体系，WASM虚拟机内核大体可理解为V8）与Windows PC均使用V8作为WASM虚拟机内核，均支持JIT，在相同算力条件下两者性能接近。但需注意移动平台散热更差，对性能要求更苛刻。不同平台和宿主版本的实际内核可能存在差异，不宜一概而论。 iOS：iOS默认为普通模式，不支持JIT，可用于超休闲游戏；中重度游戏建议开启iOS高性能模式以支持JIT，但该模式需要更多精力进行调优，尤其在启动发烫与内存方面。 1.5 如何确定问题 定位思路 发现性能问题后的第一步就是确定问题。如果阅读过UWA提供的《Unity移动端游戏性能优化简谱》，读者可能已对Unity常见性能问题有一定认知。在小游戏侧，移动端的知识体系大部分仍然适用，但也有不少小游戏独有的问题需要特别留意。总之，确定问题本身也是一项挑战。 可用工具 工欲善其事必先利其器，直接精确的性能数据能更直观地反映性能瓶颈，事半功倍。从小游戏角度，可用工具主要分为三个层面： 平台层：各小游戏官方平台基本都提供自带的性能调试与监控工具（如微信/抖音开发者工具）。 引擎层：Unity Profiler、Memory Profiler等，用于定位引擎内部的CPU与内存瓶颈。 系统层：Android Studio Profiler、Xcode Instruments等，用于从进程视角分析整体资源消耗。 此外，UWA已对微信、抖音等小游戏平台提供支持。UWA GOT Online工具可通过SDK快速集成到测试项目中，真机测试完成后在极短时间内完成数据上传与解析，自动生成一系列可视化图表。同时，基于UWA丰富的优化经验和数据库进行评分，针对各性能模块提供分析建议和参数变化趋势。 通用原则 —— PC开发者工具 vs 真机测试：小游戏开发中，PC端开发者工具的便捷性很容易让人依赖其数据进行性能判定，但工具环境与真机环境在多方面存在显著差异，贯穿CPU、内存、GPU、启动耗时等所有性能维度。例如：纹理压缩格式（ASTC等）在PC上会回退为RGBA32导致内存虚高、WASM编译与JIT行为在iOS/Android真机上与PC模拟差异明显、PC的高性能CPU会掩盖真机上更易暴露的瓶颈。因此，任何时候都应优先以真机测试数据为最终依据，PC开发者工具仅用于初步调试和功能验证。后续各章节中也会反复强调这一原则。 问题定位检查清单 确认问题现象：掉帧/卡顿/发热/闪退/启动慢，明确优化目标 使用平台调试工具（微信/抖音开发者工具）获取初步性能数据 使用Unity Profiler/Memory Profiler定位引擎层瓶颈 真机测试（Android + iOS），勿仅依赖PC开发者工具数据 借助UWA GOT Online等第三方工具获取深度分析报告与优化建议 对比APP版本与原生性能基线，区分通用问题与小游戏特有问题 2. 启动耗时 启动耗时是玩家对小游戏最直接的性能第一印象。本章围绕启动流程中的三个核心环节 —— 首包资源下载、WASM代码下载与编译、引擎初始化及首帧逻辑 —— 逐一分析各阶段的优化方法，并汇总可落地的检查清单。 2.1 启动流程概览 当玩家上手一款小游戏时，最先影响到体验的性能指标就是启动耗时。在小游戏快节奏的环境下，启动过长可能导致玩家尚未体验游戏内容就已流失。根据微信小游戏官方数据，普通小游戏启动时间为7~10s，不经优化的Unity WebGL游戏启动可达该时间的2~3倍以上。优化目标是将首屏启动耗时控制在5~10s甚至更短。 以微信小游戏（以下简称微小）为例，Unity WebGL转换的微信小游戏主要依靠Unity Loader进行初始化，其工作流程如下： 不同平台小游戏的启动细节可能略有差异，但总体可归纳为三个核心环节：首包资源下载、WASM代码下载和编译、引擎初始化与开发者首帧逻辑。 此外，UWA GOT Online的小游戏报告中也提供了启动耗时的相关指标、推荐值和优化建议，可帮助快速定位耗时较高的阶段： 2.2 首包资源下载 首包是什么：一般名称为xxx.webgl.data.unityweb.bin.txt，存放在CDN服务器上。首次进入游戏时下载、解压后保持在内存，二次启动可直接走缓存。因此对启动耗时而言，主要关注首次下载或首包更新时的下载耗时。 首包内容构成：首包文件大小直接影响下载耗时，可使用AssetStudio查看其中包含的资源，通常包括： Unity Default Resources — 引擎默认资源（Arial字体、默认Mesh、纹理等） IL2CPP Meta Data — C#代码经IL2CPP生成的类、方法等元信息 unity_builtin_extra — Always Included的Shader BuildSettings中所有Active的场景 Resources文件夹中的资源及其引用的其他资源 全局设置及引用到的资源（如Splash图片等） 优化方法： 1. 排查与精简：结合AssetStudio查看首包内容，排查其中内存占用较高或不符合预期出现的资源，尽量只保留首场景中的必要资源。下图为AssetStudio中看到的部分首包内容。 举例来说，加载过程的提示文字可能会使用自定义字体，如果用全量字体其内存占用对于首包来说可能过大。然而实际在加载过程中用到的文字量有限，可考虑单独拆分出一个更小的字体专门用于首包。 2. 转化工具设置：勾选转换工具面板的“压缩首包资源”选项，可对首包进行进一步br压缩。微信还提供了首包资源优化功能，可清理首包中项目并未实际使用的资源，进一步瘦身。 3. 目标值：一般建议将首包资源大小控制在5MB以下。 2.3 WASM代码下载与编译载 代码包是什么：一般名称为xxx.webgl.wasm.code.unityweb.wasm.br，默认进行br压缩。在启动阶段，wasmcode与首包资源并行下载，共同占用下载带宽；下载完成后进行编译，编译过程本身也消耗CPU资源。综合来看，核心优化方向是降低WASM代码体积，一般建议将原始代码包控制在30MB以下。 优化方法： 1. 代码分包（最常用）：在开发者工具中安装wasmCodeSplit插件，原理是将原WASM拆分为主包（启动加载）和子包（延迟加载）。小游戏先加载较小的主包进入主场景，再异步加载剩余分包，可大幅降低下载与编译耗时。具体操作可参考官方文档。 注意：如果直接从拓展商店中安装插件失败，出现如下报错，可以直接从设置-拓展设置-编辑器拓展中找到wasmCodeSplit插件进行安装。 2. 代码裁剪：Unity未针对WebGL平台做特别裁剪，默认会将引擎、业务代码、第三方插件全部编译为WASM二进制。建议勾选“Strip Engine Code”并将“Managed Stripping Level”设为High，可有效缩减代码包体积。 3. IL2CPP Size选项：使用Unity 2021以上版本时，可在PlayerSettings中将IL2CPP选项设为更小尺寸（SIZE），减少函数量。 4. 删除多余插件：排查项目使用的第三方插件，手动移除不必要的Unity模块（如物理模块、Unity数据统计等），从源头减少代码量。 2.4 引擎初始化与开发者首帧逻辑 该阶段做什么：引擎自身模块与数据初始化、首个场景加载以及MonoBehaviour的Awake/Start流程。此阶段CPU处理密集，但网络处于空闲状态，可利用预下载功能提前缓存后续资源（详见第3章资源加载）。 优化要点： MonoBehaviour脚本在首帧的Start/Awake中应尽量少做逻辑，优先把画面呈现出来 初始场景不宜过大，通常只呈现Splash场景即可 初始场景中如需加载后续主场景或配置，建议采用分帧策略，切勿在Start/Awake中阻塞 调试工具 —— Android CPU Profiler 微信提供了Android CPU Profiler功能，可用于排查此阶段的CPU逻辑开销。使用方法如下： 从右上角“…”打开菜单 → 开发调试 → 选择Start CPU Profile开始采集 采集一段时间后点击Stop CPU Profiler，生成.cpuprofile文件 文件路径通常为：Android/data/com.tencent.mm/MicroMsg/appbrand/trace 数据可通过Chrome、Edge，或开发者工具中的JavaScript探测器打开分析 注意：若希望在堆栈中看到可读函数名，需勾选Profiling-funcs或使用Development模式，但会带来一定性能开销。否则函数仅显示数字ID，需借助symbols文件进行映射（微信提供Python替换脚本可自动处理）。此外，若要排查启动阶段，可手动修改game.js，让游戏在启动时增加一段黑屏等待时间，便于在启动期间打开调试。 2.5 启动耗时检查清单 首包资源<5MB，仅包含首场景必要资源 WASM原始代码包<30MB 开启代码分包（wasmCodeSplit插件） 勾选“Strip Engine Code”，Managed Stripping Level设为High Unity 2021 + 设置IL2CPP选项为更小尺寸（SIZE） 首场景只保留Splash，Awake/Start不阻塞主线程 初始场景中后续加载采取分帧策略，不在首帧同步完成 3. 资源加载 启动耗时章节提到首包内容应尽可能精简，其余资源需放在CDN中延迟加载，因此资源的按需加载是小游戏中至关重要的环节。本章从加载方案选择、缓存机制、AssetBundle适配三个维度展开，帮助开发者在加载效率与内存占用之间找到平衡。 3.1 常规加载方案 三种方案对比：目前常见的加载策略有三种 —— Addressable方案、AssetBundle方案、Unity Instant Game方案。无论哪种方案，微信小游戏环境均不支持本地Bundle加载，最终都采用上传CDN方式在游戏运行时异步按需下载。 Addressable（AA）：Unity官方推荐的资源管理方案，支持远程加载与本地缓存，适合大多数项目。 AssetBundle（AB）：传统分包方案，在内存和加载效率方面优于AA，尤其适合相对重度的游戏。目前UWA接触的小游戏项目中使用AB方案的偏多。 Instant Game：Unity官方提供的自动加载方案，仅适合原生APP未使用资源按需加载、总包体较小的轻度游戏，具体参考团结引擎文档。 对于AA和AB两种方案，一般可以沿用原APP游戏工程的管理方案，转化工作量相对较小。后续讨论以AssetBundle方案为主，其余方案可参考官方文档。 AB打包参数建议：使用AssetBundle进行资源打包时，推荐以下设置： BuildAssetBundleOptions.AppendHashToAssetBundleName：开启后Bundle名会携带Hash，是小游戏中资源缓存及缓存淘汰机制的重要依据（详见3.2节）。 BuildAssetBundleOptions.ChunkBasedCompression：LZ4压缩方式，加载速度与包体大小更均衡。 DisableWriteTypeTree：如无新老Unity版本兼容需求，建议开启以提升加载速度并降低内存。 加载接口：小游戏不支持AssetBundle本地加载，从服务器下载Bundle主要使用以下接口： UnityWebRequestAssetBundle.GetAssetBundle UnityWebRequest 不建议使用WWW.LoadFromCacheOrDownload或WWW等带Cache接口，WebGL模式下会通过JS模拟文件系统，带来额外内存消耗。 3.2 资源缓存 缓存机制：Unity Loader插件已内置资源缓存与淘汰功能，无需开发者自行实现。UnityWebRequest和UnityWebRequestAssetBundle接口均会自动触发缓存，业务侧无需关心资源是否有缓存，正常调用API即可。可通过Loader插件返回的Log判断当前是走下载还是缓存。 预下载：Loader插件提供预下载功能，目的是充分利用网络带宽。启动流程中「引擎初始化与首场景准备」阶段CPU处理密集，但网络处于空闲，利用预下载可在此阶段提前下载并缓存资源，后续即可直接走本地缓存。预下载文件总体积建议控制在3~5MB以内，文件数量不超过10个（此阶段最多10个并发，超出将排队）。 缓存过滤：并非所有文件都适合缓存。可在导出面板中配置不自动缓存的文件类型（如默认不缓存.json文件），也可在minigame/unity-namespace.js中手动处理缓存逻辑。 版本管理：若资源文件已缓存到本地但后续版本更新，不特殊处理会继续加载旧缓存。需要以Hash区分资源版本 —— 本地有旧缓存时先清理再写入新版本。只需将资源URL携带Hash作为版本依据即可，默认Hash长度为32；若游戏自行计算CRC，也可设为CRC长度。注意Hash必须以‘-’或‘_’作为分隔符，其他符号无法正确处理版本信息。 缓存上限与淘汰：缓存体积随游戏进程逐渐增大，默认上限为200MB。可通过 maxStorage调整上限，也可前往微信后台申请空间提升，最高可达1GB。达到上限后Loader按LRU规则清理早期缓存。为避免频繁触发，默认额外多清理30MB（可通过defaultReleaseSize修改）。清理时支持忽略指定文件使其不被自动清理，仅可主动删除，具体通过minigame/unity-namespace.js中isErasableFile函数控制。 常用接口： public string WX.PluginCachePath：获取自动缓存的文件存储路径，返回值：${wx.env.USER_DATA_PATH}/__GAME_FILE_CACHE public string WX.GetCachePath(string url)：传入URL或文件相对路径，返回缓存路径（无缓存则返回空字符串） public void CleanAllFileCache(Action action)：清理所有自动缓存的文件 public void CleanFileCache(int fileSize, Action action)：从缓存目录中释放指定大小的文件 public void RemoveFile(string path, Action action)：从缓存目录中删除指定文件 3.3 小游戏中的AssetBundle AB内存差异：AssetBundle打包后的文件由文件头（属性数据、Asset信息、依赖关系等）和主数据（平台对应的Asset数据）两部分组成。在Standalone、Android、iOS等具有本地文件系统的平台上，只需加载文件头，运行时按需读取资产数据。而WebGL平台受浏览器权限限制不能直接访问本地IO，加载AB时需将两部分全部加载到内存中。因此，原生APP只需关注AB文件头内存，但小游戏还需额外关注AB文件本身的内存占用。 WXAssetBundle方案：小游戏平台提供了专用AB接口来解决上述问题。以微信小游戏为例，使用WXAssetBundle将文件系统接口桥接到微信的文件系统，使AB可读写到小游戏缓存目录，从而避免AB文件整体进入内存。经测试，使用Unity原生AB接口时，AB文件本体内存会进入UnityHeap并被Reserve；更换为WXAssetBundle后，该内存不再出现在UnityHeap中，而是进入本地缓存（仍需关注默认200MB上限）。 在UWA GOT Online小游戏报告中，也专门统计了WXAssetBundle接口的内存数据。下图为同一AssetBundle、不同加载接口的对照测试 —— 前半段为WXAB接口加载/卸载，后半段为Unity原生AB接口。可见前者几乎不会使Reserved Total升高，后者则有明显变化。 打包策略：无论使用哪种AB接口，打包策略都值得关注。分包粒度太大，单个AB下载易卡顿；粒度太小则文件数量过多，IO和下载效率下降。建议根据资源使用频率和生命周期采用混合策略 —— 常更新的资源（如UI元素）打包至较小Bundle便于热更新，不常更新的资源（如场景资源）可打包至较大Bundle。通常单个AB包建议控制在10MB以内，总体AB驻留数量在1000个以内。 AB利用率：UWA提出了AB利用率的概念 —— 在特定测试流程中，某AB文件被加载进内存但真正用到的资产（含主动加载与被动依赖）比例很低，则判定为低利用率。这类AB的打包策略可能存在浪费，定位到具体AB后可进一步排查，对包内资产进行更精细的划分。 3.4 资源加载检查清单 打包参数：开启AppendHashToAssetBundleName，使用ChunkBasedCompression (LZ4) 非跨版本兼容需求时，开启DisableWriteTypeTree 使用UnityWebRequest/UnityWebRequestAssetBundle加载（不用WWW等带Cache接口） 预下载文件总体积控制在3~5MB，文件数≤10个 单个AB包<10MB，总体AB驻留数量<1000 个 检查AB利用率，避免大量未使用资产进入内存 关注缓存上限（默认200MB，可申请提升至1GB），必要时调整maxStorage 4. 内存 内存是小游戏中与闪退直接挂钩的核心性能指标，其分布结构、扩容机制和平台限制均与原生APP存在显著差异。本章从小游戏进程视角出发，梳理内存的整体构成与各子模块的优化要点，并对比iOS不同运行模式下的内存分布差异。 4.1 进程视角的内存限制 Android：小游戏通常运行在独立进程中（如微信小游戏为 com.tencent.mm:appbrand0/1/2），内存相对宽松。一般建议低端机<1.2GB，中高端机<1.5GB。可通过ADB或UWA Gears工具查看进程内存，下图为Gears采集到小游戏运行时的PSS内存走势。 iOS：整体限制更严格，建议低端机<1GB，中高端机<1.4GB，最终需结合Xcode Instruments实际数据具体分析，微小通常关注com.apple.WebKit.WebContent进程。 iOS根据运行模式的不同，内存限制也有所差异： 普通模式（默认）：WASM运行无JIT，计算性能受限。小游戏进程与微信绑定，同在WeChat进程中。 高性能模式：CPU算力明显提升，但内存限制更严格 —— 2GB内存设备上限约1GB，3GB及以上设备上限约1.5GB，工程上建议控制在1.2~1.3GB以内。此时小游戏运行在独立进程com.apple.WebKit.WebContent中。 高性能+模式：在高性能模式基础上进一步升级，保留游戏独立进程的同时将渲染重新挪回微信进程，渲染效果和渲染内存消耗均得到改善。推荐使用WebGL2、内存压力大的游戏开启该模式。 4.2 小游戏内存结构 Unity WebGL以WebAssembly+WebGL技术为基础，游戏内存分配完全托管在浏览器环境中。适配到小游戏后，进程成为“容器”，内存组成结构基本一致。典型游戏的内存分布如下图所示： 各内存区域简要说明： UnityHeap：托管堆、本机堆与原生插件底层内存，初始值为转化面板中的“UnityHeap预留内存”，运行时只增不减且存在内存碎片。这是大部分小游戏项目中最主要的内存来源，是排查与优化的重点。 WASM编译：代码编译与运行时指令优化产生的内存，占比相对较高。一般无工具直接检测该部分精确占用。常见经验上可按未压缩WASM代码包的约8~12倍进行数量级估算，但实际比例受平台内核（iOS/Android）、是否开启JIT、分包策略及编译优化等级影响较大，以真机实测为准。 GPU内存：纹理或模型Upload GPU后的显存占用。压缩纹理格式支持受宿主、浏览器、设备GPU和转换方案影响，PC开发者工具中可能回退为RGBA32，必须以真机实测为准。 音频：Unity将音频传递给容器后，播放时占用的内存。 基础库+Canvas：小游戏公共库、Canvas画布等固定开销，基本无法针对性优化。 其它：Emscripten文件系统模拟等相关占用。 4.2.1 UnityHeap 扩容机制：UnityHeap是为实际使用的内存部分提前预留的上限，实际占用指标为DynamicMemory。当DynamicMemory接近预留上限时（如初始500MB，达到约490MB以上），会触发扩容 —— 在内存中新分配一个更大的堆空间（如550MB），将旧内容复制过来后再释放旧堆。短时间内产生内存块复制导致的内存尖峰，极易造成闪退，尤其在iOS上更为严重。 初始值设定：不宜过小（频繁扩容导致尖峰），也不宜过高（≥1024MB直接导致启动失败）。官方参考值：轻度游戏（休闲类）256MB，中度游戏（模拟经营、卡牌成长）496MB，重度游戏（SLG、MMO）768MB。 DynamicMemory构成：DynamicMemory=MonoHeap+NativeReserved+原生插件内存，需分别关注各部分走势，避免泄漏。 Mono堆内存是C#脚本托管对象（string、List、自定义类实例、委托等）的核心内存区域。GC会正常回收对象，Mono使用量可以有增有降；但受WebAssembly内存沙箱机制影响，堆容量一旦被峰值撑高便难以回缩，表现为峰值保持。因此在小游戏上要特别关注Mono的分配，避免单帧内出现大量分配把峰值顶上去。 NativeReserved由Unity Native产生，属于引擎内部对象。动画、字体、Shader等资源类内存均包含在此。纹理、网格等传给GPU的资源原本计入GPU内存，但若开启了Read/Write Enable选项，CPU端还会额外存储一份，计入NativeReserved。此外，使用Unity原生AssetBundle接口时AB文件本体内存也会进入NativeReserved，使用WXAssetBundle可将其转移至本地缓存。 从WXAB和原生AB的加载对比测试数据中可以看到：使用原生AB接口会使NativeReserved明显上升，连带推高DynamicMemory；即使卸载后NativeReserved回落，DynamicMemory也已被撑高，剩余空间相当于被预留住了。 针对Native部分的排查可使用Unity Memory Profiler工具，需开启Development Build及AutoConnect。 第三方原生插件内存（如Lua）通常没有单独数值展示，可观察DynamicMemory与Mono、Native部分的差值是否偏大。若对插件管理较明确，可通过对比测试（开关插件观察内存变化）进一步定位来源。 4.2.2 WASM编译 小游戏启动时下载WASM代码包并进行编译，编译后占据的内存很大。常见经验上可按未压缩WASM代码包的约8~12倍进行数量级估算（如微信官方数据：iOS上30MB未压缩代码约需300MB运行时编译内存），但实际比例受平台内核、是否开启JIT、分包策略及编译优化等级影响较大，以真机实测为准。 最常见有效的优化方案是代码分包 —— 将原WASM代码包拆分为主包和子包： 主包（wasmcode）：启动时首先加载，大小约为原包的1/3~1/2，建议控制在3~5MB，需尽量覆盖大多数游戏场景和流程。 子包（wasmcode1/wasmcode2）：分别对应Android和iOS平台，约7~15MB，在游戏运行一段时间后自动加载或缺失函数时再加载。 分包后总大小可能比原包更大（因多出复制文件），但只有主包一定加载，其余延迟或按需下载，对运行几乎无影响。 4.2.3 常见资源类型内存 对于Unity开发者而言，更直观、更容易上手修改的是纹理、网格、动画、字体等常见资源类型。各类资源在小游戏内存中的归属如下： GPU内存：纹理、网格、RenderTexture NativeReserved（UnityHeap内）：动画、字体、Shader、开启了Read/Write的纹理/网格、使用原生AB接口的AB文件本体 独立内存：音频资源 《Unity移动端游戏性能优化简谱》中对各类资源内存优化已有系统介绍，以下仅提炼各类型的核心优化要点： 纹理：内存大户，重点关注三个方面 —— ①格式：避免RGBA32/ARGB32等未压缩格式，优先使用ASTC/ETC2等移动端压缩格式（注意小游戏中格式支持因宿主和真机而异，务必真机验证）；②分辨率：1024以上的纹理需评估是否必要，高分辨率在小屏幕上往往看不出差异却占4倍内存；③Read/Write Enabled：开启后CPU端额外存一份，计入NativeReserved，非必要一律关闭。此外，开启Mipmap会使纹理内存增至约1.33倍，需权衡带宽收益后决定。 网格：关注顶点数和面片数是否超出实际表现需求，顶点属性（UV2、Color、Tangent等）在不必要时可剥离以减小数据量。网格同样存在Read/Write Enabled问题，开启后CPU端多存一份。 动画：动画Clip本身占用内存，且播放时涉及骨骼Transform更新等CPU开销。关注Animation Clip数量与时长，适当压缩关键帧或降低采样率。大量Animator处于Active状态还会额外增加CPU负担。 音频：小游戏中音频内存独立于UnityHeap，但也需关注。建议使用压缩格式（如MP3/Vorbis），并根据音频类型选择Loader（Decompress On Load会将完整数据常驻内存，Streaming则按需读取但CPU开销更高）。 字体：使用TextMeshPro时，SDF Atlas以Alpha8格式存储，内存较低；但若使用动态字体（.ttf直接渲染），会产生较大内存占用。建议优先使用TMP静态字体方案。首包中使用的字体应尽可能精简字符集。 粒子系统：粒子数量、发射器复杂度直接影响CPU端计算和内存分配，中低端机上应限制Max Particles并精简粒子层级。 以上资源内容可以在UWA GOT Online小游戏报告中进行详细排查，在Resource模式中可获取具体的资源列表。 注意：新上手小游戏平台的开发者常遇到纹理内存远超预期的现象 —— 明明做了纹理压缩，测试数据却显示很高。这通常是PC开发者工具中ASTC等压缩格式回退为RGBA32所致，纹理压缩格式务必以真机测试为准（详见1.5节关于PC工具与真机测试的原则说明）。 4.3 iOS高性能与高性能+模式对比 iOS的高性能模式和高性能+模式中内存分布存在明显差异，UWA通过对比测试帮助读者形成更直观的认识。 高性能模式：小游戏运行在独立进程com.apple.WebKit.WebContent中。几乎所有类型的内存都会进入WebContent进程（不会使WeChat进程内存上升），仅音频资源例外。真正导致闪退的进程是WebContent进程。内存分布大致如下： 高性能+模式：官方描述为“开创性地在保留游戏独立进程的基础上将渲染重新挪回了微信进程”。测试结果显示，属于GPU的纹理、网格、RT不再进入WebContent进程，而是进入WeChat进程。但开启了Read/Write的纹理/网格在CPU端仍有一份内存位于NativeReserved中，属于WebContent进程。 再次验证，小游戏闪退只与WebContent进程有关，一般建议控制在1.3GB以内；WeChat进程即使大幅超过这个范围也不会闪退。内存分布如下： 可见高性能+模式的内存占用相比高性能模式确有下降，若高性能模式下内存吃紧可考虑开启高性能+。但具体仍需自行评估客户端版本、是否存在新的画面或性能问题。 4.4 内存检查清单 UnityHeap初始值合理设置（参考：轻度256MB/中度496MB/重度768MB，避免<256MB或≥1024MB） 关注DynamicMemory走势，避免泄漏和频繁扩容引发的内存尖峰 Mono堆避免单帧内大量分配 纹理避免开启Read/Write Enable（否则CPU端多存一份计入NativeReserved） 考虑使用WXAssetBundle代替原生AssetBundle接口，避免AB本体进入UnityHeap WASM代码包按约8~12倍体积数量级估算编译期内存，以真机为准，关注分包效果 纹理压缩格式以真机测试为准，勿依赖PC开发者工具数据 内存压力大的游戏考虑开启iOS高性能+模式 5. CPU压力 受限于WASM虚拟机的执行效率以及常规C# 多线程/Job多线程能力受限，小游戏上CPU性能约为原生APP的1/3（实际比例因机型、Unity版本、iOS模式、瓶颈类型等因素而异），是小游戏平台上最易出现高压问题的维度。本章聚焦小游戏中独有或更容易放大的CPU瓶颈场景，通用排查思路可参考《Unity移动端游戏性能优化简谱》中的CPU相关章节。 5.1 蒙皮动画 小游戏中较常见的CPU性能压力来自蒙皮动画（Skinned Mesh Renderer）。在原生APP上，蒙皮计算可在工作线程处理，对主线程影响较小；而在小游戏中常规多线程能力受限，MeshSkinning.Update的耗时直接影响主线程，不经处理可能成为瓶颈。下图为某款SLG项目在Mi 10上的运行数据，在小怪较少的战斗场景中也会有持续4ms的MeshSkinning.Update开销，相对偏高。 优化方向有两个： GPU Skinning：将蒙皮计算从CPU移至GPU处理。团结引擎已内置该功能，也可使用第三方GPU Skinning方案。上述SLG场景中替换为GPU Skinning方案后，在小怪数量更多、更复杂的战斗场景开销也仅有1~2ms，且表现无明显差别。 控制蒙皮规模：进一步限制骨骼数量和顶点数量，适当牺牲表现精度以换取性能。 5.2 逻辑代码 部分在APP上运行稳定的计算逻辑，直接移植到小游戏中效率可能明显偏低，需要适量精简。这一部分与项目业务逻辑强相关，难以给出统一方案，但可遵循以下排查与优化思路： 配合Profiler打点定位耗时的函数，优先处理耗时最高的热点 关注每帧都在执行的逻辑（Update/FixedUpdate），检查是否有不必要的重复计算 WebGL中Lua不支持JIT，避免将Lua用于重度计算逻辑 回顾1.5节中关于PC工具与真机测试的原则：PC端CPU性能远超真机，务必以真机Profiler数据为准 5.3 UI模块 UGUI是许多Unity项目中CPU开销的重要来源，在小游戏单线程环境下影响更为明显。主要关注以下几个高耗时函数： EventSystem.Update：UGUI事件系统的每帧更新，耗时偏高时通常与两个因素有关 —— 一是输入回调中挂载了耗时过高的逻辑函数，可通过Profiler打点进一步定位；二是大量UI元素默认开启了Raycast Target选项，而实际上多数Image、Text并不需要响应点击事件，关闭该选项可直接降低事件遍历开销。 Canvas.SendWillRenderCanvases：Canvas的渲染重建入口，当UI元素的Transform、颜色、层级等发生变化时触发。该函数耗时与发生变化的UI元素数量成正比。优化方向包括：将动态UI和静态UI拆分到不同Canvas中，避免静态元素被频繁连带重建；减少不必要的Layout Group、Content Size Fitter等自动布局组件使用。 Canvas.BuildBatch：UI元素合批为Mesh的主要耗时点，通常紧随SendWillRenderCanvases之后。合并批次时若发生打断（如同一Canvas下使用了不同材质/图集），会显著增加重建耗时。规划好UI的图集和材质共用策略、保持合批连续性，可有效降低该开销。 CanvasRenderer.SyncTransform：当UI元素的Transform发生变更时触发，频繁调用时会连带导致渲染更新开销增高。需注意某些动画或逻辑中对UI元素Transform的频繁修改。 通用建议：动态/静态UI分离到不同Canvas、关闭不需要的Raycast Target、减少自动布局组件使用、谨慎使用UI Particle（会作为UI元素高频更新并重建Mesh）。 5.4 实例化与销毁 在游戏运行过程中，频繁的Instantiate和Destroy操作在小游戏上开销明显，原因在于单线程环境下内存分配与对象初始化的耗时直接阻塞主线程。 场景/模块切换：进入新场景或新界面时，建议采用分帧加载策略，避免在一帧内完成大量对象的Instantiate导致明显卡顿。可结合对象池（Object Pool）复用高频创建销毁的对象（如子弹、特效、UI列表项），减少频繁的内存分配与回收。 Activate/Deactivate vs Instantiate/Destroy：对于需要频繁显隐的对象，使用SetActive的代价远低于Instantiate/Destroy，但大量同时处于Active状态的对象仍会增加Update等逻辑开销，需要在两者之间平衡。 Resources.UnloadUnusedAssets：卸载未使用资源是一项耗时操作，建议在场景切换完成后的静默期执行，避免在游戏流程中频繁调用。 5.5 CPU压力检查清单 关注SkinnedMeshRenderer数量和MeshSkinning.Update耗时 考虑使用GPU Skinning（团结引擎已支持）或第三方方案 控制蒙皮骨骼数和顶点数，适当牺牲精度换取性能 逻辑代码配合打点定位高耗时函数，针对性精简 WebGL中避免用Lua处理重度逻辑（不支持JIT） UI：动态/静态分离到不同Canvas，关闭非必要的Raycast Target UI：减少自动布局组件使用，规划好图集合批策略 频繁创建销毁的对象使用对象池，场景切换采用分帧加载 6. GPU压力 小游戏上GPU运行效率与APP接近，相对于CPU较不容易成为性能瓶颈，但GPU压力同样关系到帧率、发热与功耗。本章提炼《Unity移动端游戏性能优化简谱》中GPU章节的核心要点，并补充微信小游戏平台在iOS上经验证的几个GPU压力关键来源（HDR、RT使用、纹理精度等）。 注意：1.5节中关于PC开发者工具与真机测试的原则在GPU侧尤为重要 —— ASTC等压缩纹理格式在PC上可能回退为RGBA32，WebGL 2.0特性（GPU Instancing、SRP Batcher）在PC与真机上的表现也可能存在差异，GPU数据务必以真机实测为准。 6.1 判断GPU压力 当整体帧耗时明显超出目标帧，但CPU侧各模块耗时之和与之存在较大差距时，通常倾向于判定存在GPU压力。在原生APP上可借助GPU Clocks或同步等待函数标记辅助判断，但小游戏环境下渲染线程模型存在差异，更建议结合实际表现综合评估 —— 例如CPU耗时已优化到位而帧率仍不达标、或降低渲染分辨率后帧率明显回升，均可作为GPU Bound的佐证。 6.2 顶点阶段压力 顶点阶段压力取决于渲染面片数与顶点着色器复杂度。GPU Primitive参数中，若剔除图元占比高达70-80%，说明存在大量浪费，需排查模型制作是否合理、大模型是否未拆小导致整体提交GPU、网格是否过于精细而屏占比有限等。 优化方法包括：LOD分级控制远距离面片数、精简复杂模型、CPU端提前剔除不可见物体、在中低端机上关闭多光源/阴影/多Pass等使面片数翻倍的特性。 6.3 片元阶段压力 片元阶段的计算量取决于每帧绘制的像素数，由渲染分辨率和Overdraw共同决定。 渲染分辨率：分级控制分辨率是最实用的GPU优化手段。低端机降至0.7-0.8倍，像素数可减半，3D场景通常可接受轻微模糊。需注意实际渲染分辨率与设备DPR（Device Pixel Ratio）相关 —— 高DPR屏幕即使设置较低倍率仍可能产生高像素负担，建议按像素总数而非固定比例制定分档标准。URP项目中可利用Camera Stack将3D场景渲染到低分辨率RT中，避免UI同时变模糊。 Overdraw：指像素被重复绘制的次数。不透明物体应通过Render Queue调整渲染顺序来控制；粒子系统和UI是半透明Overdraw的主要来源 —— 中低端机精简粒子层级、限制Max Particles、裁剪纯透明贴图面积、考虑动画帧烘焙替代；UI侧注意全屏遮挡时关闭底层、Mask替换为RectMask2D等。此外，后处理、Blit、Copy等操作同样会产生全屏级别的Overdraw。特别地，开启HDR会引入额外的全分辨率RT拷贝操作以及额外的显存开销，在小游戏环境下对GPU压力影响显著，建议谨慎评估是否必要。 6.4 着色器与后处理 着色器：优先关注屏占比较高、Shader相对复杂的渲染对象（地表、建筑、特效），而非屏占比低的角色。项目中“万能Shader”常导致GPU对大量未使用特性进行全额计算，应通过关键字开关或拆分Shader优化。 后处理：是GPU压力的常见来源 —— Bloom中低端机可从1/4分辨率开始下采样；SMAA开销较高不建议在中低端使用；DOF移动端开销大需谨慎；尽量使用Local Volume按需开启而非全局常驻。 6.5 GPU带宽 GPU带宽主要影响能耗发热而非帧率。Mali官方数据显示约1GB/s带宽造成80-100mW功率开销，在游戏总功率中占比可观。在小游戏中，渲染产生的GPU带宽开销在DRAM带宽中占绝大部分，主要来自：频繁的RT使用与切换、高精度纹理资源的采样、以及HDR或后处理引入的额外RT拷贝。 优化手段包括：尽量复用RT减少切换、使用合理压缩格式降低单像素传输量、3D场景务必开启Mipmap（改善效果远超纹理压缩）、避免全局强制各向异性过滤、减少不必要的Copy和后处理采样。 6.6 GPU压力检查清单 关注渲染面片数，排查剔除图元占比过高的场景 按像素总数分档控制渲染分辨率，中低端机可降至0.7-0.8倍 排查Overdraw（粒子系统、UI、后处理），定位热点资源 关注屏占比高且Shader复杂的渲染对象，拆分“万能Shader” 后处理效果分级取舍，中低端机关闭开销较高的效果 关注GPU带宽 —— 纹理压缩、Mipmap、减少不必要的Copy和采样 GPU数据（纹理格式、WebGL 2.0特性等）务必以真机测试为准 7. 功耗优化 小游戏的功耗优化整体上可参考移动端的优化思路与经验，且由于移动设备散热条件有限，功耗问题在小游戏上同样不容忽视。功耗不仅关乎设备续航，更与发热直接挂钩 —— 当设备温度超过系统温控阈值后，会触发CPU/GPU降频，导致帧率骤降，形成“高功耗→发热→降频→卡顿”的恶性循环。因此功耗优化本质上也是性能稳定性优化，核心手段与CPU、GPU章节的优化思路一脉相承。 7.1 帧率对功耗的影响 首先要明确一个基本规律：在其它条件不变的情况下，帧率与功耗近似成正比，60FPS的功耗约为30FPS的两倍。这意味着高端机型上若不加限制地跑满帧率，反而可能因为功耗过高导致发热更严重。建议根据项目类型制定合理的目标帧率（如休闲游戏30FPS，中度游戏30~60FPS），也可对不同场景动态调整 —— 例如主界面、挂机场景降低帧率，核心战斗场景再恢复到目标帧率，从而有效控制整局功耗。 7.2 功耗的来源 小游戏运行时的功耗主要来自以下几个方面： CPU：逻辑计算、蒙皮、动画、物理、脚本等，是小游戏功耗最主要的来源。 GPU：顶点处理、片元着色、纹理采样、带宽传输等。 屏幕显示：屏幕亮度与分辨率直接关联功耗，但开发者通常无法直接控制。 网络传输：持续的网络请求与数据收发也会产生额外功耗。 对于开发者而言，能够直接着手优化的主要是CPU与GPU两部分。 7.2.1 CPU侧功耗优化 在原生APP上，通常需要分别排查CPU主线程与子线程中的压力来源。但小游戏环境下常规多线程能力受限，因此重点关注CPU主线程上各模块的耗时即可。排查思路与本文「CPU压力」章节一致：通过Profiler定位高耗时模块（渲染、UI、物理、动画、逻辑脚本等），找到项目中压力较大的模块并予以针对性优化。由于功耗与CPU占用正相关，降低主线程耗时本身就是在降低功耗。 7.2.2 GPU侧功耗优化 GPU侧功耗主要从GPU计算压力与GPU带宽两个维度进行排查，与本文「GPU压力」章节的思路一致：控制同屏面片数、合理设置渲染分辨率、减少Overdraw、精简Shader复杂度，同时关注纹理采样次数与带宽占用。降低GPU负载同样直接降低功耗。 7.3 功耗优化检查清单 根据项目类型制定合理的目标帧率，避免高端机无限制跑满 对不同场景动态调整帧率（主界面/挂机降帧，核心玩法恢复目标帧率） 通过CPU主线程Profiler定位高耗时模块并针对性优化 从GPU计算压力与GPU带宽两个维度排查GPU侧功耗 将高温环境（户外、充电时游玩）纳入测试覆盖范围 iOS高性能模式下尤其关注启动阶段的发热，避免长时间连续高负载 8. APP转小游戏经验分享：同步转异步功耗优化 本章分享一个实际项目中将APP迁移到小游戏时遇到的典型问题 —— AssetBundle同步加载的兼容性改造，以及我们在实战中摸索出的一套低成本解决方案。 8.1 问题背景 在原生APP项目中，同步加载与异步加载在实际运行时的体验差异通常不大，开发团队在早期也不一定会严格区分两者的使用场景。但在小游戏环境中，AssetBundle文件本身不支持同步加载（从已加载的AssetBundle中加载具体资源仍可使用同步接口），这意味着原本在APP中大量使用的同步加载逻辑都需要逐一改造。如果项目在原生APP阶段没有提前做好这方面的规范和管理，后续转换时的工作量会相当可观。 8.2 快速定位方法 在Unity编辑器中将小游戏工具面板的Play Mode切换为Web Play Mode，此时在Editor中运行游戏，一旦触发AssetBundle的同步加载就会直接抛出WebGL platform not support sync load method这类报错，帮助快速发现哪些加载链路存在问题。 进一步地，还可以在同步加载接口的报错位置额外增加一条报空日志，显式输出对应资源的路径信息，这样每次报错时就能直接看到是哪个AB文件在哪个时机被同步加载了，排查效率大幅提升，不必逐个翻看代码调用链。 8.3 集中预加载方案 从已加载的AssetBundle中加载具体资源仍然可以使用同步接口。基于这一点，核心思路就清晰了：只要在使用同步接口加载资源之前，确保对应的AssetBundle已经通过异步方式加载到内存中且未被卸载，那么原来的同步资源加载逻辑就完全可以保持不变。 按照这个思路，我们的做法是在游戏流程中找到一个统一的、早于所有实际资源加载的时机点，把所有会被同步加载使用的AssetBundle提前用异步接口统一加载一遍。只要保证后续真正使用同步接口加载资源时这些AB仍在内存中且未被卸载，整个项目中原有的同步加载逻辑就不需要逐一修改，也不会在小游戏环境中触发报错。 例如原先在Initialize阶段使用了同步加载会导致报错，我们找到了一个更早的时机点，通过PreloadAsync先用异步接口集中加载了timeConfig在内的多个后续会用同步加载的资源，那么后续的逻辑无需修改也不会报错了。 这种方式本质上是将“同步改异步”的改造工作从散落在各处的资源加载点集中收拢到了一个统一的预加载节点，既降低了修改工作量，也避免了逐处改造时容易遗漏或引入新问题的风险。 9. 结束语 这篇文章之所以称为简谱，实在是因为这些笔墨远不能达到面面俱到，很多内容还未涉及到，或者限于篇幅和重点不能深入讨论。它更多的是立足于如何以用好一套完善完整的性能工具为基础，构建发现问题-解决问题-监控问题的优化思维和优化体系，使得性能优化的工作事半功倍。更多的优秀内容，欢迎在UWA社区中进行搜索。 本文内容就介绍到这里啦，更多内容可以前往UWA学堂进行阅读。