SuperAPI AI 工具集

game-thread-performance

编程与开发

查看项目

更新于 11-06 16:19

技能介绍

用途

游戏线程是许多虚幻项目的核心瓶颈:它负责 Actor 的 Tick、蓝图执行、输入、物理回调以及 UI 更新。如果游戏线程超出每帧预算,即使 GPU 空闲,帧也会被延迟。本技能用于解释游戏线程时间如何驱动帧率,并给出优化方法。

何时使用

  • 帧率感觉偏低,但 GPU 看起来未被充分利用。
  • stat unit 或 stat game 显示 Game 线程主导了帧时间。
  • Tick 繁重的 Actor、蓝图热路径或频繁的 Tick() 工作导致卡顿。
  • 需要快速建立“游戏线程时间如何换算为 FPS”的心智模型。

心智模型:帧预算是固定的

单帧的硬上限为 1000 / TargetFPS 毫秒,因此游戏线程每多花一毫秒,都会真实占用帧时间:

目标 FPS 每帧预算
30 FPS 33.33 ms
60 FPS 16.66 ms
120 FPS 8.33 ms
240 FPS 4.16 ms
360 FPS 2.77 ms

经验法则:

  • 如果 Game 线程占用了大部分帧预算,说明游戏是 CPU 受限的。
  • 如果 GPU 或 Render Thread 占主导,则问题与渲染或填充率相关。

什么在消耗游戏线程预算

游戏线程上常见的重负载路径:

  • Actor 与组件的 Tick()
  • 每帧执行的蓝图逻辑
  • 热路径中过多的 GetWorld()、GetOwner()、Cast<> 或 FindComponentByClass()
  • 重复的分配、字符串构建和 UObject 频繁创建销毁
  • 过于频繁的物理 / 碰撞 / 导航更新
  • Widget 更新与 Slate 失效
  • 每帧大量定时器或委托广播

结果不仅是 FPS 降低,还会带来帧时间方差增大,即使平均 FPS 看起来尚可,也会感觉为卡顿、输入延迟或游戏不流畅。

快速排查流程

  1. 先运行 stat unit。
    • Game > Render / GPU?说明游戏线程是限制因素。
    • Render / GPU > Game?问题可能出在绘制调用、过度绘制或后处理。
  2. 运行 stat game,查看哪些系统占用了最多的游戏线程时间。
  3. 如果热点在你的代码中,用 DECLARE_CYCLE_STAT、SCOPE_CYCLE_COUNTER 或 TRACE_CPUPROFILER_EVENT_SCOPE 进行插桩。
  4. 每次修复后重新测试;优化真正的热点,而不是显而易见的那一个。

优化手段

1. 减少每帧工作

  • 在 Actor 不需要时禁用 Tick:
PrimaryActorTick.bCanEverTick = false
  • 仅在对象实际处于活动状态时启用 Tick。
  • 对不频繁的逻辑使用定时器或事件代替 Tick()。
  • 一次性缓存引用,而不是每帧重新计算。
  • 避免在 Tick 中进行昂贵的字符串格式化、分配或 NewObject 频繁创建。

2. 将昂贵工作移出游戏线程

  • 用 AsyncTask、TaskGraph 或后台作业把重 CPU 工作放到工作线程。
  • 最终的 UObject / Actor 变更只保留在游戏线程上。
  • 使用队列或原子操作将数据从工作线程传回游戏线程。

3. 优化蓝图与 C++ 热路径

  • 将每帧热路径从蓝图迁移到 C++。
  • 用更少的批量操作替代大量微小操作。
  • 避免在 Tick() 中重复调用 GetPawn()、GetComponentByClass()、Cast<> 或 FindField<>。

4. 降低物理、AI 与 Widget 开销

  • 限制执行昂贵移动、碰撞或模拟更新的 Actor 数量。
  • 批量更新 UI,而不是每帧使 Widget 失效。
  • 减少游戏过程中重复的 AI / EQS / 导航网格工作。

5. 调整高帧率目标的预期

在 120 FPS 下,完整帧预算只有 8.33 ms;在 240 FPS 下只有 4.16 ms。如果在 120 FPS 下游戏线程就消耗了其中 5 ms,那么在渲染开始前就已经花掉了大部分预算。这就是高刷新率目标格外苛刻的原因。

示例:安全的游戏线程减负

void AMyActor::Tick(float DeltaSeconds)
{
    QUICK_SCOPE_CYCLE_COUNTER(STAT_MyActor_Tick);

    // Cache references once in BeginPlay / constructor when possible.
    // Avoid expensive work here if it can run less often.
    if (!bReady) return;

    // Do only the minimal per-frame logic that must stay on the game thread.
}

如果工作繁重,将其移到工作线程任务,并在游戏线程上应用结果:

AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WeakThis = TWeakObjectPtr<AMyActor>(this)]()
{
    // Heavy CPU work here.
    // Do NOT touch UObject / Actor state from this thread.
});

注意事项

  • 不要仅因帧率低就假定 GPU 是瓶颈。先检查 stat unit 和 stat game。
  • 高 FPS 目标并非没有代价——240 FPS 和 360 FPS 留给游戏线程的时间非常少,因此热路径清理更重要,而不是更不重要。
  • 把错误的东西移出线程可能引入 bug。只移动真正 CPU 受限且线程安全的工作;将 Actor / UObject 变更编组回游戏线程。
  • 在玩法项目中,蓝图繁重的 Tick 通常是最容易见效的优化点。

参考资料与来源

引擎源码(UE 5.8):

  • Engine/Source/Runtime/Core/Public/Stats/Stats.h — 统计与周期计数器辅助工具。
  • Engine/Source/Runtime/Engine/Private/LevelTick.cpp — 引擎中的每帧 Tick 流程。
  • Engine/Source/Runtime/Engine/Private/TimerManager.cpp — 基于定时器的延迟路径。

官方文档(UE 5.8):

  • Unreal Insights — https://dev.epicgames.com/documentation/unreal-engine/unreal-insights-in-unreal-engine
  • Trace — https://dev.epicgames.com/documentation/unreal-engine/trace-in-unreal-engine-5
  • Stat Commands — https://dev.epicgames.com/documentation/unreal-engine/stat-commands-in-unreal-engine
  • Testing and Optimizing — https://dev.epicgames.com/documentation/unreal-engine/testing-and-optimizing-your-content

相关技能:profiling-and-optimization、timers-and-async、debugging-techniques。