
1. 多敌人场景为什么是 UE FPS 的性能分水岭做 UE 的 FPS 项目单人靶场跑 120 帧不算本事真正把帧数打下来的是多敌人同屏这个场景。我做过几个 PvE 向的射击项目也帮朋友排查过不少性能问题几乎每一次帧率崩盘都发生在敌人数量从 3 个涨到 15 个的那一瞬间。原因不复杂单个敌人 Actor 本身不贵贵的是它背后挂载的一整条链路——骨骼网格体的动画求值、动画蓝图的逻辑、AI 行为树的 Tick、碰撞检测、网络同步、以及渲染线程要为每个角色提交的 Draw Call 和蒙皮计算。这些东西在单敌人时被引擎的并行能力轻松吃掉一旦数量上去主线程、渲染线程、GPU 三条线同时吃紧帧时间就从 8ms 一路飙到 30ms 以上。这篇内容我想聊的是一套可复现的排查与优化流程不是罗列一堆引擎参数。核心思路是先用工具定位瓶颈到底卡在哪条线程再针对性地砍开销而不是盲目地关阴影、降画质。适合已经能跑通一个基础 FPS Demo、但一遇到多敌人就掉帧的开发者也适合想系统梳理 UE 性能方法论的中级同学。文中涉及的工具以 Unreal Insights、Stat 命令、RenderDoc 为主都是引擎自带或免费的工具不需要额外采购。我先把结论摆前面多敌人场景的优化八成的时间应该花在动画和渲染线程上主线程的 Gameplay 逻辑反而是最容易优化的部分。很多人一上来就去优化 AI 逻辑方向就偏了。2. 先定位再动手多敌人场景的瓶颈判定方法2.1 用 Stat 命令快速锁定问题线程优化最忌讳的就是我觉得这里慢。UE 自带了一套非常成熟的 Stat 命令体系多敌人场景我通常按这个顺序看stat unit看 Frame、Game、Draw、GPU 四个数字。Frame 是总帧时间Game 是游戏线程Draw 是渲染线程GPU 是显卡。哪个数字最接近 Frame瓶颈就在哪。stat game如果 Game 线程高进一步看是 Tick、动画还是物理。stat scenerendering如果 Draw 线程高看 Draw Call 数量、可见静态网格体、动态阴影等。stat anim专门看动画开销多敌人场景这个命令必开。stat physics看物理和碰撞的开销。我实测过一个典型场景8 个敌人同屏stat unit显示 Frame 22ms、Game 9ms、Draw 18ms、GPU 14ms。这里 Draw 线程 18ms 明显是主瓶颈Game 线程虽然也不低但不是首要矛盾。如果这时候去优化 AI 逻辑帧率几乎不会变因为渲染线程才是卡脖子的那根。注意stat unit里的 Draw 指的是渲染线程Render Thread的 CPU 耗时不是 GPU 耗时。很多人把 Draw 和 GPU 搞混导致优化方向完全跑偏。Draw 高说明 CPU 提交渲染命令太慢GPU 高才是显卡算不过来。2.2 Unreal Insights 抓帧看时间线Stat 命令只能告诉你哪条线程慢但慢在哪个函数、哪个 Actor得靠 Unreal Insights。用法很简单启动时加-tracecpu,frame,bookmark参数或者在编辑器里用Trace.Start和Trace.Stop命令抓一段。抓下来之后重点看几个轨道Game Thread 轨道展开看World Tick下面每个 Actor 的 Tick 耗时。多敌人场景经常能看到某个敌人的动画蓝图 Tick 或者某个自定义组件 Tick 异常突出。Render Thread 轨道看InitViews、Shadow Depths、Base Pass这几段。多敌人时Shadow Depths经常是重灾区因为每个角色都要投动态阴影。Anim 轨道如果开了动画 Insights能看到每个骨骼网格体的动画求值耗时包括蓝图线程和 Worker 线程的分配。我踩过的一个坑早期项目里每个敌人身上挂了一个自定义的感知组件每帧做一次球形检测找玩家。8 个敌人就是 8 次OverlapMultiByChannelGame 线程直接多出 3ms。Insights 里一眼就能看到这个函数在时间线上排成一排改成一个管理器统一做一次检测就解决了。2.3 建立可对比的基准场景优化前一定要有个固定的测试场景否则你根本不知道改动是变好了还是变差了。我的做法是固定地图、固定敌人出生点、固定玩家视角。用控制台命令t.MaxFPS 0解锁帧率避免被垂直同步干扰。记录一组基准数据stat unit的四个数字、Draw Call 数量stat scenerendering里的 Mesh Draw Calls、可见角色数。每次改动后回到同一位置、同一视角重新记录。这个基准场景的价值在于它把感觉变流畅了这种主观判断变成了可量化的数字。我见过太多人改了一堆参数结果帧率没变甚至更差就是因为没有基准。3. 动画系统多敌人场景最大的隐形开销3.1 骨骼网格体与动画蓝图的成本构成一个敌人角色在渲染上看起来只是一个人但在 CPU 侧它是一整套流水线动画蓝图求值计算骨骼姿势→ 蒙皮把骨骼变换应用到顶点→ 提交渲染。多敌人场景里动画求值通常跑在 Worker 线程上但动画蓝图的逻辑部分比如状态机、蓝图节点还是要在 Game 线程或专门的动画线程上跑。动画蓝图的成本主要来自三块状态机求值每个状态机每帧都要判断转移条件条件越复杂越贵。蓝图节点Event Blueprint Update Animation里写的逻辑每帧每个角色都跑一遍。骨骼节点比如 IK、Look At、物理动画这些是逐骨骼计算的骨骼越多越贵。我做过一个测试一个标准人形角色约 70 根骨骼动画蓝图里有一个三状态的状态机加几个混合节点单个角色的动画求值大约 0.3ms。8 个角色就是 2.4ms如果动画蓝图写得复杂一点轻松突破 5ms。3.2 用动画共享和 LOD 砍掉大部分开销多敌人场景最有效的动画优化手段是动画共享Animation Sharing。原理很简单如果 10 个敌人都在播放跑步这个动画且它们的相位差不多那完全可以只求值一次把结果共享给所有角色。UE 提供了Anim Sharing插件配置好之后能把同类型角色的动画求值成本降低 60% 以上。配置步骤大致是启用Animation Sharing插件。创建一个Anim Sharing Setup数据资产定义哪些骨骼网格体可以共享。在角色蓝图里挂上Anim Sharing Manager组件注册到共享系统。调整共享的容差参数比如允许相位差在 0.1 秒内的角色共享同一份动画。注意动画共享对每个角色动作完全独立的场景效果有限比如敌人各自做不同的攻击动作时就没法共享。它最适合大量敌人做相似移动巡逻、追击的场景。如果你的敌人 AI 行为差异很大优先考虑动画 LOD。动画 LOD是另一个必做的优化。UE 的骨骼网格体支持按距离降低动画更新频率近距离0-15 米每帧更新全骨骼。中距离15-40 米每 2 帧更新一次可以关掉 IK 和物理动画。远距离40 米以上每 4 帧甚至更低频率更新只保留最基础的状态机。在骨骼网格体资产的LOD Settings里可以配置Update Rate Optimization把Force Update Rate和Evaluate Skeleton按 LOD 分级设置。我实测下来8 个敌人里通常只有 2-3 个在近距离其余都在中远距离动画开销能直接砍掉一半。3.3 动画蓝图的 Debug 与瘦身技巧动画蓝图写起来方便但很容易变成性能黑洞。我排查动画问题的习惯是用stat anim看Anim Evaluate和Anim Update的耗时。在动画蓝图里用Anim Node的调试功能看每个节点的耗时。重点检查Event Blueprint Update Animation里有没有做重活比如每帧做射线检测、每帧查表。一个常见的坑有人在动画蓝图里每帧调用Get Player Character然后算距离这个操作本身不贵但乘以 20 个敌人就不一样了。更好的做法是在角色蓝图里算好距离存到一个变量里动画蓝图直接读。还有一个技巧是用动画层接口Anim Layer Interface替代动画蓝图里的分支。很多项目在动画蓝图里用一堆Blend Poses by Bool来切换上半身动作节点多了之后求值成本很高。改成动画层接口让不同逻辑层独立求值再合并结构更清晰性能也更好。4. 渲染线程与 Draw Call多敌人的第二战场4.1 多敌人场景的 Draw Call 从哪来每个骨骼网格体角色至少贡献 1 个 Draw Call不透明部分如果开了描边、透明材质、武器挂件可能变成 3-5 个。20 个敌人就是 60-100 个 Draw Call再加上场景本身的静态网格体Draw Call 数量很容易突破 500。在移动端或者中低端 PC 上Draw Call 超过 300 就开始明显吃 CPU 了。渲染线程的开销不只是 Draw Call 数量还有动态阴影每个角色都要渲染一遍阴影贴图这是双倍甚至三倍的几何提交。可见性剔除每个角色都要做视锥剔除和遮挡剔除计算。材质切换不同角色用不同材质会打断合批。我实测过一个场景关闭所有敌人的动态阴影后Draw 线程从 18ms 降到 11ms帧率从 45 涨到 65。阴影的代价在多敌人场景里被严重低估了。4.2 阴影、剔除与合批的取舍针对多敌人场景渲染优化我通常按这个优先级来动态阴影降级把敌人的阴影从动态改成静态或者干脆关掉用一张假的接触阴影Contact Shadow贴图代替。如果必须保留动态阴影把阴影距离调近比如只让 10 米内的敌人投阴影。合并材质让所有敌人共用一套材质通过材质参数比如颜色区分。这样引擎有机会做实例化渲染减少材质切换。调整剔除距离在角色蓝图里设置Cull Distance Volume超过一定距离的敌人直接不渲染。FPS 游戏里玩家通常只关注视野内的敌人远处的可以降级。使用 Nanite 或 Impostor如果项目支持 Nanite静态部分可以用敌人这种动态角色更适合用 Impostor公告板做远景替代。注意关阴影和降剔除距离是视觉换性能的手段一定要和美术、策划确认可接受度。我见过一个项目为了性能把敌人阴影全关了结果玩家反馈敌人像贴纸一样飘在地上最后又加回来了。所以这类改动要留好开关方便随时调整。4.3 用 RenderDoc 看 GPU 侧的真实开销CPU 侧的 Draw 线程优化完之后如果 GPU 还是高就得用 RenderDoc 抓一帧看 GPU 在干什么。重点看Base Pass不透明几何的渲染耗时。Shadow Depths阴影贴图渲染耗时。Lighting光照计算耗时多敌人场景如果有大量动态光源会很贵。RenderDoc 的用法是在 UE 里按F11或者用RenderDoc插件抓帧然后在 RenderDoc 里逐个 Pass 看耗时。我遇到过一个案例GPU 高是因为每个敌人身上挂了一个点光源做眼睛发光效果20 个敌人就是 20 个动态光源光照 Pass 直接爆炸。改成用自发光材质Emissive就解决了零光照开销。5. Gameplay 与网络同步容易被忽视的细节5.1 Actor Tick 的精细化管理多敌人场景里每个敌人 Actor 的 Tick 都是一笔开销。UE 默认所有 Actor 都开 Tick但很多敌人的逻辑根本不需要每帧跑。我的做法是按需开 Tick敌人在待机状态时关掉 Tick进入战斗状态再打开。用SetActorTickEnabled控制。降低 Tick 频率用SetActorTickInterval把远处敌人的 Tick 间隔从 0.016 秒改成 0.1 秒。用定时器替代 TickAI 的感知、寻路这类逻辑不需要每帧跑用SetTimer每 0.2 秒跑一次就够了。我做过对比20 个敌人全部每帧 TickGame 线程约 6ms改成按状态和距离分级 Tick 后降到 2.5ms。这个优化几乎不影响游戏体验因为玩家根本感知不到远处敌人的逻辑更新频率。5.2 网络同步的带宽与 CPU 成本如果是联机 FPS多敌人场景还要考虑网络同步。每个敌人 Actor 的移动、动画状态都要同步20 个敌人的同步开销很可观。优化手段包括降低同步频率用Net Update Frequency控制远处敌人可以降到 5Hz。只同步必要属性用Replicated标记时想清楚哪些属性真的需要同步别把整个结构体都同步。用相关性Relevancy剔除玩家看不到的敌人不同步UE 的Net Cull Distance Squared可以配置。这里有个经验动画状态的同步比位置同步更贵。如果敌人动画是纯客户端根据速度推算的就不需要同步动画状态只同步位置和速度即可。这需要动画蓝图支持从速度反推动画的逻辑做起来有点绕但省下的带宽很值。5.3 用对象池管理敌人生命周期频繁地 Spawn 和 Destroy 敌人会造成 GC 压力和内存碎片。多敌人场景比如一波一波刷怪一定要用对象池预创建一批敌人 Actor设为隐藏和禁用。需要时从池里取一个重置状态并显示。敌人死亡后不 Destroy而是隐藏并放回池里。对象池的收益在刷怪频繁的场景特别明显。我做过一个波次防守的 Demo用对象池后 GC 卡顿从每波一次降到几乎没有帧时间的 1% low 提升了 30% 以上。6. 常见问题排查速查表与避坑经验6.1 多敌人性能问题速查表现象可能原因排查命令解决方向Game 线程高Actor Tick 过多、AI 逻辑重stat game分级 Tick、定时器替代Draw 线程高Draw Call 多、动态阴影stat scenerendering关阴影、合材质、剔除GPU 高光照复杂、材质贵RenderDoc减光源、简化材质动画卡顿动画蓝图复杂、无 LODstat anim动画共享、动画 LOD帧时间抖动GC、Spawn 频繁stat memory对象池、减少分配1% low 帧低突发计算、同步阻塞Insights分摊计算、异步加载6.2 我踩过的几个典型坑坑一盲目开 Lumen 和 Nanite。这两个功能在静态场景里效果很好但多敌人场景下动态角色的 Lumen 贡献很小开销却很大。我建议 FPS 项目在多敌人场景里关掉 Lumen 的动态部分用传统光照或者烘焙光照。坑二动画蓝图的 Tick 顺序。动画蓝图的求值默认在 Game 线程之后、渲染之前如果 Game 线程已经很满动画求值会被挤压导致动画看起来卡顿。解决办法是把动画求值尽量放到 Worker 线程用Parallel Anim Evaluation选项。坑三碰撞通道设置不当。敌人的碰撞体如果和所有通道都检测物理开销会翻倍。我通常给敌人单独设一个碰撞通道只和必要的通道检测比如子弹、玩家。坑四材质里的每帧计算。有些材质里写了Time节点做动态效果每个敌人每帧都要算一遍。如果效果不明显改成静态或者用顶点动画替代。6.3 优化后的验收标准优化做完怎么判断达标了我的标准是平均帧率目标平台的中端配置上稳定 60 帧以上。1% low 帧不低于平均帧率的 70%这个指标比平均帧率更能反映流畅度。帧时间稳定性用 Insights 看帧时间曲线不能有规律的尖峰。Draw Call 数量PC 端控制在 2000 以内移动端控制在 500 以内。最后分享一个我个人的习惯每次优化改动都单独提交一次写清楚改了什么、预期收益多少、实测收益多少。这样如果某个改动引入了新问题回滚起来很方便也能积累自己的优化经验库。多敌人场景的性能优化没有银弹靠的就是一次次定位、改动、验证的循环把每一块开销都压到合理范围。