多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

UE多敌人FPS性能优化实战:从stat unit到动画预算分配的完整方案

UE多敌人FPS性能优化实战:从stat unit到动画预算分配的完整方案 做FPS项目的人几乎都会遇到同一个尴尬阶段单测一个敌人、几个敌人时帧数很漂亮一旦场景里铺开三四十个敌人UE FPS 性能优化的警报就响了——帧率从一百多直接掉到四十近战混战瞬间甚至能感觉到单帧顿卡。这套优化笔记是我自己在项目里反复调出来的实践方向重点讲多敌人场景下如何系统性定位瓶颈以及按收益排序去优化渲染、动画、AI和物理。如果你也在用虚幻引擎做多人战斗场景不管是小规模遭遇战还是大规模刷怪这篇文章的目标是让你拿到一套可以直接上手的排查路径和优化手段。内容会覆盖从工具使用到具体配置的完整链路如何用stat命令和Unreal Insights把瓶颈定位到线程级别、如何用LOD和剔除压低渲染成本、如何用动画预算分配器控制多角色的动画开销、如何让AI感知和寻路不再成为游戏线程的拖累最后配上实测前后的数据对比。无论你用的是UE5还是UE4大部分方案都能直接落地。1. 多敌人FPS的瓶颈分布先搞清楚卡在哪根神经上先抛个结论在多敌人FPS里感觉卡绝大多数不是单一原因而是渲染、动画、AI、物理四块开销在同一个帧里叠加。单看某一个模块时每个都不至于崩一旦四五十个敌人同时进场你的游戏线程、渲染线程和GPU可能同时接近满载这时候任何一次突发的资源加载、一次GC暂停、一个敌人的瞬时动画开销都会把帧时间顶到十几二十毫秒以上反映到玩家脸上就是开火瞬间掉帧。还有一个容易误判的点FPS框架下的帧率有平均帧和1% Low帧两个维度。平均帧看着有八十多帧但1% Low帧只有二十多帧玩家的实际感受就是卡但不至于完全玩不了——这种忽高忽低的抖动感在多人敌人混战里尤其明显。所以性能优化的目标不只是把平均帧拉高还要把最差的那1%帧时间压住让战斗过程保持稳定的输出节奏。1.1 三个线程GPU的常规判断套路UE引擎在编辑器中按下~键输入stat unit会看到一个经典的帧头统计它把一帧拆成几个主要区域Frame、Game、Draw、GPU。多看几行数字你就知道当前帧是被谁拖死的Game线程高游戏逻辑、AI、寻路、物理同步、动画更新最终都会落到游戏线程表现为持续偏高或频繁Hitch。Draw线程高渲染线程负责收集可见对象、生成绘制指令、驱动骨骼蒙皮任务、提交GPU状态Draw过高常伴随大量骨骼网格体或过多的静态网格实例。GPU高GPU一侧的负载主要来自着色器复杂度、overdraw、阴影和后处理比如大范围烟雾、多个实时阴影光源、全屏特效堆叠。Frame高但Game/Draw/GPU都不高这就很反常了多半是等待事件、网络同步、内存分配或GC导致的阻塞需要用Unreal Insights进一步查。实际跑法在敌人数量固定的一局里先用stat unit看整体水位再打开第二层统计。比如stat scenerendering看可绘制体数量、stat animation看骨骼动画更新耗时、stat ai看AI控制器与感知开销、stat navigation看导航路径查询和运行时导航生成的负载。一层层往下拆优化的方向就不会跑偏。1.2 多敌人场景的典型开销组成以我的经验一个四十名敌人的遭遇战场景典型的帧时间开销分布大概是游戏线程里AI感知和BehaviorTree决策占3到5毫秒动画更新占2到4毫秒物理与命中查询占1到2毫秒渲染线程因为骨骼蒙皮和绘制指令生成占2到4毫秒GPU端阴影与shader复杂度占2到5毫秒。这些都是经验区间的估计不同项目差异很大但有一个共性没有任何一块是绝对的大头往往是几块一起涨。所以对通用性能优化流程来说你要做的第一步永远是确认当前瓶颈而不是凭感觉去关阴影或删特效。否则你关了半天的阴影最后发现瓶颈在动画更新线程那就白折腾了。2. 用统计工具把感觉卡变成数据卡性能分析环节很多人会跳过去直接改代码我觉得这是最亏的。工具本身不复杂难的是养成先数据、后动手的习惯。下面这套流程是我每次优化多敌人场景都会走一遍的固定套路。2.1 stat unit 与 stat 类别的组合用法建议把常用命令整理成一个清单开发过程中随时调出stat unit stat scenerendering stat animation stat ai stat navigation stat rhi stat startfile stat stopfilestat startfile配合stat stopfile可以在本地生成一份trace文件之后用Unreal Insights打开能按线程维度看到每一帧里各个函数的真实耗时。相比纯粹看stat unittrace文件能定位到具体函数和调用链比如到底是AActor::Tick里的哪个任务耗时还是骨骼更新接口消耗大。正式优化前的几天我会开着stat startfile把战斗过程录几分钟然后回放查看比边玩边盯数字更容易发现细节。对于肉眼观察我习惯在战斗中切几个角度记录帧时间敌人在视野内少量、大量聚堆、面向或背向玩家、开火瞬间这些不同状态下瓶颈常常不一样。你可能发现大量敌人聚集时GPU阴影压力爆炸但在空旷地带时游戏线程反而最高。录数据时尽量把固定相机和自由录屏都做一遍。2.2 Unreal Insights的Trace链路与耗时采样UE的Unreal Insights在统计大量AI和骨骼更新时非常有用。操作方法不复杂控制台输入stat startfile进入战斗后输入stat stopfile在编辑器里用Unreal Insights打开生成的.utrace文件。此时可以重点查看GameThread Timeline找每一帧游戏线程里的最宽块确认AI、物理、动画的分布。RenderThread Timeline看骨骼网格体更新、Primitive组件收集的耗时。帧与帧之间的间隔如果有明显的无逻辑空洞可能是等待GPU或线程同步。在大量敌人场景里GameThread上的AIPerception、BTService、AnimInstance的耗时块会非常醒目。一旦你能在Timeline上看到这些块也就知道先优化谁了。2.3 帧曲线与瓶颈切换的判断方法一个常见误区是只盯Frame时间。比如你优化了AI后Frame从19ms降到12ms以为就够了再跑一场发现GPU从5ms涨到10ms帧又卡到16ms。这不是优化失效而是瓶颈从游戏线程转移到了GPU或渲染线程。性能优化是个搬迁过程把一边降下去另一边的水位就会显出来。所以我通常在优化每个环节后重新跑一次完整的统计记录Frame/Game/Draw/GPU四项的变化同时关注1% Low帧。用表格或Excel记录每次改动前后的数据比嘴上说感觉流畅了可靠得多。3. 渲染侧优化让看得见的敌人更便宜进入渲染部分。多敌人场景里渲染线程和GPU的开销主要来自每个敌人角色的骨骼网格体绘制、阴影绘制、材质与着色器复杂度、动态光源的影响范围以及远处一堆敌人依然在完整渲染。这里的优化原则很简单让屏幕上排不上号的敌人以最低的精度绘制。3.1 Mesh LOD与骨骼LOD的阶梯化设置静态网格体的LOD大家比较熟在Mesh属性里自动生成几档距离LOD即可。但敌人是骨骼网格体除了静态网格LOD还有骨骼网格体特有的LOD机制同一套模型可以按骨骼层拆成不同精度的LOD。例如全身完整骨骼的LOD0去掉手指骨骼、只保留主骨骼的LOD1再进一步去掉部分身体骨骼的LOD2。这样远处敌人不仅模型面数少骨骼更新和蒙皮计算的成本也随之下降而不是像传统LOD只降面数、骨骼计算还白费。设置时要注意编辑骨骼网格的LOD需要在Skeleton或SkeletalMesh的LODSettings里按骨骼名称对应的LOD分组把低优先级骨骼如手指、饰品放进低LOD组。这个操作有学习成本但收益非常直接特别是同时渲染几十个敌人时骨骼LOD带来的性能节省比单纯压顶点数明显得多。3.2 阴影与动态光消耗控制阴影往往是多敌人场景里最容易被忽视的GPU杀手。一个敌人角色在镜头前投射的动态阴影会成倍增加渲染开销阴影距离稍远就大量消耗。常用的手段对远处敌人关闭Cast Shadow或使用Shadow LOD例如距离玩家超过一定范围的敌人只对主光源投射阴影不参与次要灯光阴影。场景主光源用可移动光源时阴影地图分辨率不要整体拉太高用距离衰减让近处清晰、远处模糊。非必要的点光源/聚光灯如果会照亮多个敌人会强制它们重新参与阴影计算能烘焙的静态光源尽量烘焙动态光源数量控制在个位数。使用UE5的Virtual Shadow Maps时给每个动态网格设置合理的阴影距离和缓存策略避免每帧重建大量阴影图块。实战里我习惯打开Lit视图下的Shading Model或直接查看阴影渲染耗时然后一个光源一个光源地关掉看帧时间变化保留视觉上还能接受的那部分。3.3 剔除策略距离、遮挡与视锥的配合剔除是性价比最高的一类优化。首先保证视锥剔除默认开启然后布置Cull Distance Volume距离摄像机超过一定距离的物体直接不渲染适合敌人模型、杂物、装饰物。注意敌人这种会活动且可能与玩家互动的物体不能无脑远距离剔除可以先在距离阈值外禁用渲染但保留AI或者用淡出关闭渲染来避免突兀。遮挡剔除在多敌人场景中也很关键。室内巷道大量敌人堵门时如果引擎能正确识别前后遮挡可以省下很多Draw Call。默认的遮挡查询使用GPU Occlusion前提是必须有足够大的遮挡物例如墙体以及合理的可视化设置。如果发现敌人之间互挡导致绘制量暴增可以检查渲染设置里的遮挡查询阈值并利用阻挡体积让引擎理解哪些是遮挡主力。对于完全静止的装饰性网格柱子、路障、掩体用Hierarchical Instanced Static MeshHISM或Instanced Static MeshISM合并实例。这样可以大幅压减Draw Call把节省出来的渲染线程预算留给真正的敌人骨骼网格体。4. 动画与骨骼50个敌人同时跳舞的CPU代价多敌人场景的CPU瓶颈很多时候最先爆掉的是动画更新。每个骨骼网格体组件每帧都要做一次骨骼姿态计算包括读取动画资产、加权骨骼变换、生成最终蒙皮矩阵。你想象一下原本一个玩家角色做这件事不痛不痒当有40个敌人同时刷新时这个每人一份的开销瞬间就变得极其显眼。4.1 动画更新管线在何处吃掉CPU敌人动画从资源到屏幕大致经过这几个环节加载动画序列、动画蓝图更新状态机、姿势计算Locomotion、IK等、骨骼网格体Finalize、蒙皮提交。其中动画蓝图的状态机如果每帧都做大量蓝图节点计算开销会被放大几十倍。我见过一个项目里敌人动画蓝图为了做随机空闲效果每帧会调用多个随机数生成和计时器检测单个敌人多了之后Game线程直接顶上十几毫秒。所以优化动画时优先级大概是这样先关掉不必要的IK——Full Body IK或CCD IK非常贵多角色时能不用就不用或者只在近处少数敌人上启用再用Animation Budget Allocator统一管理多角色动画更新最后才考虑是否把动画蓝图重构成C AnimInstance。IK这玩意单人演示很惊艳但在几十个敌人同屏时基本属于性能黑洞该关就要关。4.2 Animation Budget Allocator 配置实践UE的Animation Budget Allocator是一个专门为多角色动画设计的分配器插件。开启方式Project Settings里搜索Animation Budget打开插件然后在项目设置里启用它。分配器会根据你设定的每帧动画预算动态决定哪些角色升级动画更新频率、哪些角色降级甚至跳过某些角色的动画更新只保留最后一帧姿势。我常用的配置思路先把Average Frame Rate设为目标帧率的一半左右例如目标60fps就设30到40给其他模块留余量。AnimQualityOption选择按LOD自动降级近处玩家前方的角色走完整动画远处的直接降到低频率更新。分配器支持把不同角色的Budget权重调高/调低让玩家关注的重要敌人保持流畅普通杂兵尽量节省。同时开启骨骼网格体上的Update Rate OptimizationURO选项让远处的敌人降低动画更新频率从60fps更新降到20甚至10fps更新。直观看去会有微小跳帧但在混战里非常不容易被察觉。实际踩过的坑如果不设置的角色在Animation Budget Allocator下被强制降级可能出现在战斗中被击倒后依然播放站立动画的诡异情况。解决办法是给关键状态死亡、受击停滞、处决动作预留更高的Significance或强制该角色升级回完整动画。所以这个功能适合大量杂兵少数Boss的结构不适合所有敌人都想要精细动作的设计。4.3 动画曲线与状态机的瘦身除分配器外动画序列和状态机本身也要瘦身。动画资产的压缩和Retarget其实影响加载与读取速度最重要的是剔除无用的动画曲线Curve。动画曲线会随着动画更新而计算如果每个敌人动画都携带大量用不到的自定义曲线CPU开销会凭空增加很多。建议在动画资产的曲线面板里删除项目中完全不会被AnimGraph引用的曲线。状态机方面多敌人场景尽量用简单的Locomotion状态机条件变量驱动减少嵌套状态机和大量Transition。UE5项目如果要频繁做复杂角色过渡可以直接用Inertialization功能——它用惯性融合来处理状态切换本质上通过混合上一个状态的姿势惯性来避免触发复杂Transition骨骼处理。默认在动画蓝图的状态机里启用后过渡开销会明显下降多敌人时差距会被放大。5. AI、导航与物理游戏线程的隐形重担很多人在渲染和动画上花了很多精力打开统计却发现游戏线程还是高居不下往下一看全是AI和导航的账单。这一节我把多敌人场景里最容易超预算的三个系统单独拉出来讲。5.1 敌人AI的高频横跳与感知冷却AIPerception是AI开销大户之一。默认视觉感知会定期执行视线检测每次检测都要做射线和碰撞查询。当几十个敌人同时存在如果感知更新间隔为0.1秒每一帧就有好几个甚至十几个感知检测在进行射线查询量快速上升。所以我把每个敌人的感知更新间隔调成0.3到0.5秒甚至根据难度设定在玩家进入战斗后才提高频率创建感知时常按可见性变化定时刷新而不是每帧持续检测。然后是BehaviorTree的Tick。BehaviorTree本身是低频驱动的但如果你在BTService里每帧去执行路径跟踪或更新黑板键仍然会累加。我的做法是把高频循环从BTService里拆出去比如移动速度更新、距离计算这种逻辑改成按距离LOD触发BT节点之间用Wait和更长的间隔来控制决策频率。这也呼应了前面说的Blueprint性能原则频繁的逻辑放在更底层的C里蓝图负责低频表达。远距离敌人还有一个很划算的策略进入休眠状态。当玩家距离某个敌人超过阈值时直接停止该敌人的BehaviorTree和Perception甚至把它的Tick关闭或拉大Tick间隔只保留一个简单动画或干脆暂停动画。玩家靠近时再唤醒。这相当于给AI加了个距离开关比任何局部优化都省。5.2 导航网格与路径查询的预算控制大量敌人同时做路径请求时stat navigation会显示明显的路径生成开销。常见问题包括动态障碍物导致网格频繁重建、导航体量大大范围多层关卡、大量Agent在同一区域同时执行MoveTo。处理手段限制同时进行计算寻路的AI数量对远处的敌人不做实时寻路只在被玩家感知到后才在回合外计算一次粗路径。避免频繁请求MoveTo很多AI因为每帧更新目的地而反复发起路径查询实际上只要终点变化不大一个Wait或定时重算即可。动态障碍影响如果敌人之间的碰撞只用于彼此避让尽量用轻量的避让逻辑代替实时Rebuild导航网格除非必要别让区域内高频变化物件影响导航网格。导航网格分块把导航网格按关卡区域拆分让AI只在所在区块内寻路不要跨区减少路径搜索空间。5.3 碰撞检测的按需拆解物理开销在大量敌人时主要来自两点每帧持续更新的碰撞查询以及敌人之间互相穿透导致的求解。命中检测如果每个子弹都做一个独立的射线子弹一多碰撞查询的数量就会指数级增加。多敌人FPS里更优的做法是把命中检测放在一个统一的批量查询函数中比如用MultiSphereTrace一次扫出一串潜在目标再逐个做有效性判断或者把子弹的命中查询延迟合并到低频更新。敌人自身的碰撞体尽量精简多数敌人用胶囊体做查询、球形做检测不需要精确的身体碰撞。想让子弹有打中装甲/肢体不同部位的效果时再针对关键敌人启用更细分的多碰撞体但普通杂兵保持简单。还要注意关闭不必要的Simulate Physics——很多敌人的武器、挂件如果开了物理模拟战斗时会被爆炸波及瞬间几十个动态物体在互相推撞物理引擎会花大量时间求解而这些效果玩家根本注意不到。6. 多目标实测以40个敌人场景的优化复盘理论讲完上一段实测数据。这个案例是我优化过的一个体验关卡室内巷战敌人总数40个左右长期保持25到35个同屏主角是手枪随机刷怪点。开发期就一堆掉帧编辑器和Packaged Build都有感知。基线配置是1080p、中低画质目标是想稳住60fps以上。6.1 基线数据采集方法先保持画面设置不变按之前说的方式录制三分钟的遭遇战trace每隔几秒在固定点位让敌人持续靠近。统计结果如表项目优化前基线说明Frame18-24ms平均约20msGame9-12msAI动画物理混合Draw3-5ms骨骼网格体与场景绘制指令GPU5-7ms阴影与着色器为主1% Low24fps战斗中频繁个位帧可以看到优化前的瓶颈明显在Game线程且1% Low很难看。按上面的顺序我先优化AI感知间隔、休眠、动画URO 动画预算分配器再做渲染侧削减最后处理物理查询。6.2 分阶段优化清单与结果第一阶段AI感知0.3秒间隔远距离敌人休眠移除BTService里每帧的路径重请求。Game线程从11ms降到7ms1% Low从24fps提升到32fps。第二阶段启用Animation Budget Allocator并开启URO把近战敌人动画更新频率降到30fps、远处降到15fps同时剔除动画资产无用曲线。Game线程再降到5ms动画更新产生的Blocks明显消失帧时间变得更稳定。第三阶段渲染侧下重手。把距离玩家较远以外的敌人强制LOD2并关闭动态阴影场景静态杂物改用HISM合并动态点光源保留两盏其余都改范围缩小或直接删除。Draw线程和GPU各降1-2ms。第四阶段物理与碰撞。敌人统一用胶囊体查询命中检测改用一次性范围扫掠而不是逐发子弹全屏射线关闭敌人武器小道具的物理模拟。Game线程进一步降到4ms以内物理Hitch消失。最终数据项目优化前优化后变化Frame18-24ms9-11ms将近减半Game9-12ms3-4ms大幅下降Draw3-5ms2-3ms渲染线程释放GPU5-7ms3-5ms阴影光源削减见效1% Low24fps42fps稳定性明显好转这个案例里的数据仅代表我的项目环境但优化路径是可以复制的。如果你的项目基线不同数据比例会有差异但 AI→动画→渲染→物理 这个优先序在多敌人场景里几乎不会错。6.3 值得注意的取舍与踩过的坑最后总结几个过程中实际踩到的坑。第一动画预算分配器会把一些看起来近但其实在优先区域外的敌人也降级导致玩家面前某个敌人卡成一帧一帧动。解决办法是使用分配器的Significance接口或者干脆在玩家周围一定半径内禁用分配器对特定角色的调度。第二远距离敌人休眠的判定不能只看距离还要参考玩家的朝向。如果一个敌人呆在玩家身后几米处但因为距离阈值太大没休眠反而不如干脆关闭渲染。更合理的方案是距离可见性双条件组合既不在镜头前、又不在感知范围内的敌人才休眠否则会出现身后偷袭的敌人完全消失的尴尬。第三命中检测的批量扫掠要注意碰撞通道的层级。如果所有敌人都只有一个胶囊体和同一个碰撞通道一次性扫掠确实很快但如果想区分爆头、肢体伤害就得额外维护一套组件命中映射这部分的代码成本和调试成本也要纳入预期。为了纯粹的帧数把玩法做糙那就本末倒置了。最后任何优化在多人联机项目里都要多做一步验证因为服务器和客户端逻辑分离客户端侧的动画和AI预算再低也不能影响服务器上的行为判定。我个人的习惯是每当一个大优化做完至少跑一遍服务器逻辑测试确认远距离休眠和动画降级没有破坏玩家的战斗反馈。多敌人FPS性能优化的本质是做预算管理把有限的帧时间优先分配给玩家正在关注的内容。先诊断瓶颈再动手从AI感知、动画预算、碰撞查询这些CPU大头切起再从LOD、阴影和剔除上抠GPU与渲染线程。我自己的项目走到现在最深的体会是性能优化不是一个一次性的任务而是要跟着关卡、玩法、美术迭代反复测量的长期活。希望这套流程能给你省下一些走弯路的时间。
返回列表