Godot游戏引擎v4.3 CPU优化指南
Godot游戏引擎v4.3 CPU优化指南
在游戏开发中,性能优化是一个永恒的话题。本文将详细介绍如何在Godot游戏引擎v4.3中进行CPU优化,包括使用性能剖析器、手动计时函数等工具来测量和优化CPU性能。同时,文章还将讨论缓存、语言选择、线程使用、场景树管理以及物理优化等关键主题。
测量性能
要优化程序性能,首先需要了解程序中的瓶颈所在。瓶颈是程序中最慢的部分,限制了整体的执行速度。通过专注于这些瓶颈,我们可以集中精力优化那些能带来最大性能提升的区域,而不是花费大量时间优化只能带来微小改进的函数。
对于CPU来说,找出瓶颈的最简单方法就是使用性能剖析器。
CPU分析器
剖析器与你的程序一起运行,并进行时间测量,以计算出每个功能所花费的时间比例。Godot集成开发环境有一个方便的内置剖析器。它不会在每次启动项目时运行:必须手动启动和停止。这是因为,与大多数剖析器一样,记录这些时序测量会大大减慢你的项目速度。
剖析后,你可以回看一帧的结果。
这是一个演示项目的简介结果。我们可以看到物理、音频等内置流程的消耗,也可以在底部看到自己脚本功能的消耗。等待各种内置服务器的时间可能不会被计算在剖析器中。这是一个已知的错误。当一个项目运行缓慢时,你经常会看到一个明显的功能或流程比其他功能或流程花费更多的时间。这是你的主要瓶颈,你通常可以通过优化这个领域来提高速度。
有关使用Godot内置分析器的更多信息,请参阅:调试器面板。
外部分析器
虽然Godot IDE剖析器非常方便有用,但有时你需要更强大的功能,以及对Godot引擎源代码本身进行剖析的能力。你可以使用若干个第三方C++分析器来实现。例子结果来自Callgrind,这是Valgrind的一部分。从左边开始,Callgrind正在列出函数及其子函数内的时间百分比(Inclusive),函数本身(不包括子函数)内的时间百分比(Self),函数被调用的次数,函数名称以及文件或模块。
在这个例子中,我们可以看到几乎所有的时间都花在了Main::iteration()函数上。这是Godot源代码中的主函数,它被反复调用。它导致帧被绘制,物理模拟被更新,节点和脚本被更新。很大一部分时间花在了渲染画布的函数上(66%),因为这个例子使用的是2D基准测试。在下面,我们可以看到将近50%的时间花在了Godot代码之外的libglapi和i965_dri(图形驱动)。这告诉我们,很大一部分CPU时间被花在了图形驱动上。
这其实是一个很好的例子,因为在理想的世界里,只有很小一部分时间会花在图形驱动上。这说明存在一个问题,即在图形API中进行了太多的交流和工作。这种特殊的剖析导致了2D批处理的发展,通过减少这方面的瓶颈,大大加快了2D渲染的速度。
手动计时函数
另一个方便的技术,特别是在当你使用分析器确定了瓶颈后,就是手动为功能或被测区域计时。具体细节因语言而异,但在GDScript中,你可以做如下操作:
var time_start = Time.get_ticks_usec()
# Your function you want to time
update_enemies()
var time_end = Time.get_ticks_usec()
print("update_enemies() took %d microseconds" % (time_end - time_start))
当手动为函数计时时,通常最好是多次(1000次或更多次)运行该函数,而不是只运行一次(除非是非常慢的函数)。这样做的原因是,定时器的精度往往有限。此外,CPU会以一种无序的方式调度进程。因此,一系列运行的平均值比单次测量更准确。
当你尝试优化功能时,一定要反复对它们进行剖析或计时。这将为你提供关键的反馈,说明优化是否有效(或无效)。
缓存
CPU缓存是另外一个需要特别注意的东西,特别是在比较一个函数的两个不同版本的时序结果时。其结果可能高度依赖于数据是否在CPU缓存中。CPU不会直接从系统RAM中加载数据,尽管它与CPU缓存相比非常巨大(几千兆字节而不是几兆字节)。这因为系统RAM的访问速度非常慢。相反,CPU从一个较小、较快的内存库中加载数据,称为cache。从缓存中加载数据的速度非常快,但每次你试图加载一个没有存储在缓存中的内存地址时,缓存必须前往主内存并缓慢地加载一些数据。这种延迟会导致CPU长时间闲置,被称为“cache miss”。
这意味着,第一次运行一个函数时,由于数据不在CPU缓存中,它可能运行得很慢。第二次和以后的时间,可能运行得更快,因为数据在缓存中。由于这个原因,在计时时一定要使用平均数,并且要注意缓存的影响。
了解缓存对于CPU优化也是至关重要的。如果你有一个算法(例程),从主内存随机分布的区域加载小数据位,这可能会导致大量的缓存失误,很多时候,CPU会在附近等待数据,而不是做别的工作。相反,如果你能使你的数据访问本地化,或者更好的是以线性方式访问内存(像一个连续的列表),那么缓存将以最佳方式工作,CPU将能够尽可能快地工作。
Godot通常会为你处理这些低级细节。例如,服务器API已经确保了渲染和物理等事情的数据是针对缓存优化的。但是,当你编写GDExtensions时,一定要特别注意缓存的影响。
语言
Godot支持多种不同的语言,值得注意的是,其中有一些折衷。有些语言是以速度为代价而设计的,便于使用,而另一些语言速度更快,但更难使用。
无论你选择哪种脚本语言,内置的引擎函数都以同样的速度运行。如果你的项目在自己的代码中进行了大量的计算,可以考虑将这些计算转移到更快的语言中。
GDScript
GDScript被设计成易于使用和迭代的语言,是制作多种类型游戏的理想选择。然而,在这种语言中,易用性被认为比性能更重要。如果你需要进行繁重的计算,可以考虑将你的一些项目转移到其他语言中。
C#
C#在Godot中很受欢迎,并且得到了第一类支持。它提供了速度和易用性之间的良好平衡。但是要注意可能发生的垃圾回收暂停和泄漏,这些可能在游戏过程中发生。一个常见的解决垃圾回收问题的方法是使用对象池,但这超出了本指南的范围。
其他语言
第三方提供了对其他语言的支持,包括Rust。
C++
Godot是用C++编写的。使用C++通常会产生最快的代码。然而,在实际层面上,部署到不同平台的最终用户机器上是最困难的。使用C++的选项包括GDExtensions和自定义模块。
线程
在进行大量的计算时,考虑使用线程,这些计算可以相互并行运行。现代CPU有多个核心,每个核心能做的工作量有限。通过将工作分散在多个线程上,你可以进一步向CPU的峰值效率迈进。
线程的缺点是你必须极其小心。因为每个CPU核心独立运行,它们可能会同时尝试访问相同的内存。一个线程可能正在读取一个变量,而另一个线程正在写入:这被称为竞态条件。在使用线程之前,确保你理解这些危险以及如何防止这些竞态条件。线程会使调试变得更加困难。
有关线程的更多信息,请参见使用多线程。
SceneTree
虽然节点是一个非常强大、涉及面广泛的概念,但请注意:每个节点都是有代价的。内置函数,如_process()和_physics_process()会在节点树上遍历每个节点进行调用。当你有非常多的节点时,这种内务管理就会降低性能。(节点的数量取决于目标平台,可能从数千到数万不等,请确保在开发过程中评测所有目标平台上的性能)。
在Godot渲染器中,每个节点都是单独处理的。因此,减少节点的数量、让每个节点多做一些工作,可以获得更好的性能。
SceneTree比较奇怪的一点是:你有时可以通过从SceneTree中删除节点,而非暂停或隐藏节点这种方式来获得更好的性能,不一定要删除一个从场景树中分离出来的的节点。例如,你可以保留一个节点的引用,使用Node.remove_child(node)将该节点从场景树中分离出来,然后使用Node.add_child(node)将其重新加回场景树。对于在游戏中添加和删除区域,这一点十分有用。
你可以通过使用服务器API来完全避免使用SceneTree。更多信息请参见利用服务器进行优化。
物理
在某些情况下,物理终会成为一个瓶颈,尤其是在复杂的世界和大量物理对象的情况下更是如此。
以下是一些加速物理的技巧:
- 尝试使用渲染简单的几何图形来处理碰撞形状,虽然在通常情况下对终端用户来说这一点并不明显,但可以大大提高性能。
- 尝试禁用不在视野中/在当前区域之外的物理物体的物理效果,在视野中/在当前区域之内时则给这些物理对象启用物理效果(例如,你允许每个区域有8个怪物,并允许重新启用这些怪物的物理效果)。
物理的另一个关键方面是物理刻速率。在一些游戏中,你可以大大降低物理刻率,比如说,你可以不用每秒更新物理60次,而只需每秒更新30次甚至20次。这样可以大大降低CPU的负载。
改变物理刻速率的缺点是,当物理更新速率与每秒渲染的帧数不匹配时,可能会出现抖动。另外,降低物理刻速率会增加输入延迟。建议在大多数以玩家实时移动为特色的游戏中,坚持使用默认的物理刻速率(60Hz)。
解决抖动的方法是使用固定时间步长插值以匹配物理,该技术涉及到平滑多个帧的渲染位置和旋转等操作。你可以自己实现,或者使用第三方插件。从性能上来说,与运行物理刻比,插值是一个非常低成本、高性能提升的操作,速度快了好几个数量级,在减少抖动的同时也带来了部分显著的性能提升。