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.

逻辑偏好

有没有想过应该用数据结构Y还是Z, 来处理问题X ?本文涵盖了与这些困境有关的各种主题.

先添加节点还是先修改属性?

运行时使用脚本初始化节点时,你可能需要对节点的名称、位置等属性进行修改。常见的纠结点在于,你应该什么时候去修改?

最佳实践是在节点加入场景树之前修改取值。部分属性的 setter 代码会更新其他对应的值,可能会比较慢!大多数情况下,这样的代码不会对游戏的性能产生影响,但对于程序式生成之类的重型使用场景,就可能让游戏卡成 PPT。

综上,最佳的做法就是先为节点设置初始值,然后再把它添加到场景树中。

加载 VS 预加载

在 GDScript 中,存在全局 preload 方法。它尽可能早地加载资源,以便提前进行“加载”操作,并避免在执行性能敏感的代码时加载资源。

其对应的 load 方法只有在执行 load 语句时才会加载资源。也就是说,它将立即加载资源。所以,在敏感进程中加载资源会造成速度减慢。 load() 函数是可以被 所有 脚本语言访问的 ResourceLoader.load(path) 的别名。

那么, 预加载和加载到底在什么时候发生, 又应该什么时候使用这两种方法呢?我们来看一个例子:

# my_buildings.gd
extends Node

# Note how constant scripts/scenes have a different naming scheme than
# their property variants.

# This value is a constant, so it spawns when the Script object loads.
# The script is preloading the value. The advantage here is that the editor
# can offer autocompletion since it must be a static path.
const BuildingScn = preload("res://building.tscn")

# 1. The script preloads the value, so it will load as a dependency
#    of the 'my_buildings.gd' script file. But, because this is a
#    property rather than a constant, the object won't copy the preloaded
#    PackedScene resource into the property until the script instantiates
#    with .new().
#
# 2. The preloaded value is inaccessible from the Script object alone. As
#    such, preloading the value here actually does not benefit anyone.
#
# 3. Because the user exports the value, if this script stored on
#    a node in a scene file, the scene instantiation code will overwrite the
#    preloaded initial value anyway (wasting it). It's usually better to
#    provide null, empty, or otherwise invalid default values for exports.
#
# 4. It is when one instantiates this script on its own with .new() that
#    one will load "office.tscn" rather than the exported value.
export(PackedScene) var a_building = preload("office.tscn")

# Uh oh! This results in an error!
# One must assign constant values to constants. Because `load` performs a
# runtime lookup by its very nature, one cannot use it to initialize a
# constant.
const OfficeScn = load("res://office.tscn")

# Successfully loads and only when one instantiates the script! Yay!
var office_scn = load("res://office.tscn")

预加载允许脚本在加载脚本时处理所有加载. 预加载是有用的, 但也有一些时候, 人们并不希望这样. 为了区分这些情况, 我们可以考虑以下几点:

  1. 如果无法确定何时可以加载脚本, 则预加载资源, 尤其是场景或脚本, 可能会导致进一步加载, 这是人们所不希望的. 这可能会导致无意中, 在原始脚本的加载操作之上的可变长度加载时间. 在原始脚本的加载操作之上, 这可能导致意外的, 可变长度的加载时间.

  2. 如果其他东西可以代替该值(例如场景导出的初始化), 则预加载该值没有任何意义. 如果打算总是自己创建脚本, 那么这一点并不是重要因素.

  3. 如果只希望“导入”另一个类资源(脚本或者场景),那么最好的解决方法就是使用预加载常量(Preloaded Constant)。不过也有例外的情况:

    1. 如果已被导入的类有可能发生变化,那么它应该是一个使用 exportload 或之后进行初始化的属性。

    2. 如果脚本需要大量依赖关系,又不想消耗太多内存,则可以在环境变化时动态地加载或卸载各种依赖关系。如果将资源预加载为常量,则卸载这些资源的唯一方法是卸载整个脚本。如果改为加载属性,则可以将它们设置为 null 并完全删除对资源的所有引用(扩展自 RefCounted 的类型会在指向其的所有引用均已消失时自动释放内存)。

大型关卡:静态 VS 动态

如果正在创建一个大型关卡, 哪种情况是最合适的?他们应该将关卡创建为一个静态空间吗?还是他们应该分阶段加载关卡, 并根据需要改变世界的内容?

答案很简单,“当性能需要的时候”。与这两种选择有关的困境是一种古老的编程选择:优化内存还是速度?

最简单的方法是使用静态关卡, 它可以一次加载所有内容. 但是, 这取决于项目, 这可能会消耗大量内存. 浪费用户的运行内存会导致程序运行缓慢, 或者计算机在同一时间尝试做的所有其他事情都会崩溃.

无论如何,应该将较大的场景分解为较小的场景(以利于资产重用)。然后,开发人员可以设计一个节点,该节点实时管理资源和节点的创建/加载和删除/卸载。具有大型多样环境或程序生成的元素的游戏,通常会实行这些策略,以避免浪费内存。

另一方面, 对动态系统进行编码更复杂, 即, 使用更多的编程逻辑, 这会导致出现错误和bug的机会. 如果不小心的话, 开发的系统, 会增加应用程序的技术成本.

因此, 最好的选择是…

  1. 在小型游戏中使用静态关卡.

  2. 在开发中型/大型游戏时, 如果有时间/资源, 可以去创建一个可以对节点和资源的管理进行编码的库或插件. 如果随着时间的流逝而改进, 以提高可用性和稳定性, 那么它可能会演变成跨项目的可靠工具.

  3. 为一款中/大型游戏编写动态逻辑代码, 因为你拥有编程技能, 但却没有时间或资源去完善代码(必须要完成游戏). 以后可能会进行重构, 将代码外包到插件中.

有关在运行时中, 可以交换场景的各种方式的示例, 请参见文档 手动更改场景 .