Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

性能分析器

你在 Godot 里运行游戏,随心所欲地玩上一会儿。过程很有趣,游戏的功能也越来越完善,你感觉离正式发布已经不远了。

但是,当你打开技能树时,它会因为代码中的问题而停止运行。看着技能树像幻灯片一样滚动是不可接受的。出了什么问题?是定位技能树元素、UI还是渲染?

你可以尝试优化一切,反复运行游戏,但你可以更聪明地处理这件事,缩小可能性。进入Godot的分析器。

性能分析器概述

打开调试器面板并点击分析器选项卡即可打开性能分析器。

../../../_images/profiler.png

Godot 的性能分析器并不会自动运行,因为性能分析也有很大的性能开销。性能分析会持续不断地观测游戏中发生的所有事情并将其汇报给调试器,因此默认是关闭的。

要开始进行性能分析,请先运行你的游戏,然后把焦点切回编辑器。点击 Profiler(性能分析器) 选项卡左上角的 Start(开始) 按钮即可。你也可以勾选 Autostart(自动开始) ,这样下次运行项目时,性能分析器就会自动启动并记录数据。需要注意的是, Autostart 复选框的勾选状态在关闭并重新打开编辑器后不会被保留(也就是说每次重启编辑器都需要重新勾选)。

备注

性能分析器目前不支持 C# 脚本。对 C# 脚本进行性能分析可以使用 JetBrains Rider 和 JetBrains dotTrace,安装 Godot 支持插件即可。

你随时都可以点击清除按钮清空数据。测量数据的类型可以通过测量下来菜单修改。测量面板和图表会相应地发生变化。

测量数据

分析器的界面分为两个部分。左侧是功能列表,右侧是性能图表。

主要的测量内容有帧时间、物理帧、空闲时间、物理时间。

  • 帧时间指的是 Godot 为完整的一幅图像执行的逻辑所消耗的时间,包括物理和渲染。

  • 物理帧指的是 Godot 在物理更新之间所花费的时间。理想状态下,帧时间是你所选择的值:默认为 16.66 毫秒,对应 60 FPS。你可以用它作为其他相关信息的参考值。

  • 空闲时间指的是 Godot 更新物理之外的逻辑所花费的时间,比如 _process 里的代码和设置为在 Idle 时更新的计时器和相机。

  • 物理时间指的是 Godot 更新物理任务所花费的时间,比如 _physics_process 和设置为在 Physics 时更新的内置节点。

备注

帧时间包含渲染时间。如果你发现游戏里突然出现了迷之卡顿,而物理和脚本的运行速度都还挺快的,那么这种卡顿就可能是出现粒子特效或视觉特效造成的!

默认情况下,Godot 会勾选(显示)‘帧时间(Frame Time)’和‘物理时间(Physics Time)’。这能让你大致了解每一帧的耗时,以及它与你设定的目标物理帧率(Physics FPS)之间的关系。你可以通过点击左侧的复选框来随时开启或关闭某些功能。顺着列表往下滑,你还会看到其他功能模块,比如 2D 物理、3D 物理和音频等,最后会来到‘脚本(Script)’部分,你写的代码也会在这里显示出来。

“如果你点击图表,左侧就会切换显示对应那一帧的详细信息。此外,在右上角还有一个帧数计数器,你可以通过它更精细地手动调整,来查看任意特定帧的数据。

测量范围和测量窗口

你可以通过 Measure (测量指标)下拉菜单来切换你想查看的数据类型。默认情况下,它显示的是 Frame Time(帧时间),也就是完成一帧所需要的总耗时(单位为毫秒)。这里的 Average Time(平均时间)指的是,当某个函数被调用超过一次时,它每次执行所花费的平均时长。举个例子,如果一个函数总共运行了 5 次,累计耗时 0.05 毫秒,那么显示的平均时间就是 0.01 毫秒。

如果精确的毫秒数对你来说没那么重要,而你想更直观地看到某项任务在整个帧里占用了多少时间比例,那就使用百分比测量模式。其中,Frame %(帧百分比)是相对于总帧时间(Frame Time)的占比,而 Physics %(物理百分比)则是相对于物理时间(Physics Time)的占比。

最后一个选项是时间的统计范围。 Inclusive (包含时间)测量的是一个函数执行所花费的总时间,其中 包含 了它内部调用的所有嵌套函数所耗费的时间。举个例子:

../../../_images/split_curve.png

get_neighbors, find_nearest_neighbormove_subject 这三个函数都显示花费了大量时间。如果你只看这些,很容易被误导,误以为这三个函数本身的运行速度都很慢。

但如果切换成 Self(自身) 模式,Godot 在统计函数体所花费的时间时,就不会把该函数内部调用的其他函数所消耗的时间计算在内。

../../../_images/self_curve.png

你可以看到, get_neighborsmove_subject 的耗时占比大幅下降,变得没那么‘重要’了。这实际上意味着, get_neighborsmove_subject 的大部分时间其实都是在等待它们内部调用的其他函数执行完毕,而 find_nearest_neighbor 才是 真正 运行缓慢的那个。

使用分析器调试速度慢的代码

用性能分析器找出运行缓慢的代码,归根结底就是:运行你的游戏,然后盯着不断绘制的性能图表。一旦发现帧时间(frame time)出现了难以接受的峰值(也就是突然卡顿了一下),你就可以直接点击图表,让游戏暂停,并把帧数(Frame #)精准定位到那个峰值开始的地方。有时候,你可能需要在不同的帧和不同的函数之间来回跳转查看,才能最终找到问题的根源。

在‘脚本函数(Script functions)’列表下,勾选一些函数的复选框,看看哪些函数比较耗时。这些耗时的函数,就是你需要重点检查和优化的对象。

以微秒为单位手动测量

如果函数比较复杂,那么找出需要优化的部分可能会很困难。是数学的问题,还是获取进行数学运算所需数据的方式的问题?是 for 循环吗?还是 if 语句?

你可以让程序运行一些临时函数,手动计算时刻,来缩小测量范围。有两个属于 Time 类对象的函数,分别是 get_ticks_msecget_ticks_usec 。第一个会以毫秒为单位(每秒为1,000毫秒)测量,而第二个则以微秒为单位(每秒为1,000,000微秒)测量。

两者都会以各自的时间量返回游戏引擎启动以来经过的时间。

如果使用微秒的开始和结束包裹一段代码,那两者的差则是运行这段代码花费的时间。

# Measuring the time it takes for worker_function() to run
var start = Time.get_ticks_usec()
worker_function()
var end = Time.get_ticks_usec()
var worker_time = (end-start)/1000000.0

# Measuring the time spent running a calculation over each element of an array
start = Time.get_ticks_usec()
for calc in calculations:
    result = pow(2, calc.power) * calc.product
end = Time.get_ticks_usec()
var loop_time = (end-start)/1000000.0

print("Worker time: %s\nLoop time: %s" % [worker_time, loop_time])

当你成为一名更有经验的程序员后,这种(时刻依赖性能分析器的)技巧就没那么必要了。你会开始凭直觉知道,一个运行中的程序,哪些部分容易变慢。明白‘循环和分支可能会导致性能下降’,靠的是经验;而这些经验,正是通过不断的性能测量和钻研探索积累而来的。

不过,有了性能分析器(profiler)和 ticks 函数,你应该就已经具备了足够的基础,可以开始着手找出代码中哪些部分需要优化了。