逻辑偏好

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

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

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

It is the best practice to change values on a node before adding it to the scene tree. Some properties' setters have code to update other corresponding values, and that code can be slow! For most cases, this code has no impact on your game's performance, but in heavy use cases such as procedural generation, it can bring your game to a crawl.

综上,最佳的做法就是先为节点设置初始值,然后再把它添加到场景树中。有值在被加入场景树之前不能被设置的例外情况,比如设置世界坐标的时候。

加载 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 provide any benefit.
#
# 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. Instantiating the script on its own with .new() triggers
#    `load("office.tscn")`, ignoring any value set through the export.
@export var a_building : PackedScene = 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. If one has the time/resources on a medium/large game, create a library or plugin that can manage nodes and resources with code. If refined over time so as to improve usability and stability, then it could evolve into a reliable tool across projects.

  3. 对于一款中/大型游戏,可采用动态逻辑,因为你拥有编程技能,却没有时间或资源去完善代码(毕竟要完成游戏),后续可能进行重构,将代码外移到插件中。

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