逻辑偏好
有没有想过应该用数据结构Y还是Z, 来处理问题X ?本文涵盖了与这些困境有关的各种主题.
先添加节点还是先修改属性?
运行时使用脚本初始化节点时,你可能需要对节点的名称、位置等属性进行修改。常见的纠结点在于,你应该什么时候去修改?
最佳实践是在节点加入场景树之前修改取值。部分属性的 setter 代码会更新其他对应的值,可能会比较慢!大多数情况下,这样的代码不会对游戏的性能产生影响,但对于程序式生成之类的重型使用场景,就可能让游戏卡成 PPT。
综上,最佳的做法就是先为节点设置初始值,然后再把它添加到场景树中。
加载 VS 预加载
在 GDScript 中,存在全局 preload 方法。它尽可能早地加载资源,以便提前进行“加载”操作,并避免在执行性能敏感的代码时加载资源。
其对应的 load 方法只有在执行 load 语句时才会加载资源。也就是说,它将立即加载资源。所以,在敏感进程中加载资源会造成速度减慢。 load()
函数是可以被 所有 脚本语言访问的 ResourceLoader.load(path) 的别名。
那么, 预加载和加载到底在什么时候发生, 又应该什么时候使用这两种方法呢?我们来看一个例子:
GDScriptC#
# 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 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")
using Godot;
// C# and other languages have no concept of "preloading".
public partial class MyBuildings : Node
{
//This is a read-only field, it can only be assigned when it's declared or during a constructor.
public readonly PackedScene Building = ResourceLoader.Load<PackedScene>("res://building.tscn");
public PackedScene ABuilding;
public override void _Ready()
{
// Can assign the value during initialization.
ABuilding = GD.Load<PackedScene>("res://Office.tscn");
}
}
预加载允许脚本在加载脚本时处理所有加载. 预加载是有用的, 但也有一些时候, 人们并不希望这样. 为了区分这些情况, 我们可以考虑以下几点:
如果无法确定何时可以加载脚本, 则预加载资源, 尤其是场景或脚本, 可能会导致进一步加载, 这是人们所不希望的. 这可能会导致无意中, 在原始脚本的加载操作之上的可变长度加载时间. 在原始脚本的加载操作之上, 这可能导致意外的, 可变长度的加载时间.
如果其他东西可以代替该值(例如场景导出的初始化), 则预加载该值没有任何意义. 如果打算总是自己创建脚本, 那么这一点并不是重要因素.
如果只希望“导入”另一个类资源(脚本或者场景),那么最好的解决方法就是使用预加载常量(Preloaded Constant)。不过也有例外的情况:
If the ‘imported’ class is liable to change, then it should be a property instead, initialized either using an
@export
or aload()
(and perhaps not even initialized until later).如果脚本需要大量依赖关系,又不想消耗太多内存,则可以在环境变化时动态地加载或卸载各种依赖关系。如果将资源预加载为常量,则卸载这些资源的唯一方法是卸载整个脚本。如果改为加载属性,则可以将它们设置为
null
并完全删除对资源的所有引用(扩展自 RefCounted 的类型会在指向其的所有引用均已消失时自动释放内存)。
大型关卡:静态 VS 动态
如果正在创建一个大型关卡, 哪种情况是最合适的?他们应该将关卡创建为一个静态空间吗?还是他们应该分阶段加载关卡, 并根据需要改变世界的内容?
答案很简单,“当性能需要的时候”。与这两种选择有关的困境是一种古老的编程选择:优化内存还是速度?
最简单的方法是使用静态关卡, 它可以一次加载所有内容. 但是, 这取决于项目, 这可能会消耗大量内存. 浪费用户的运行内存会导致程序运行缓慢, 或者计算机在同一时间尝试做的所有其他事情都会崩溃.
无论如何,应该将较大的场景分解为较小的场景(以利于资产重用)。然后,开发人员可以设计一个节点,该节点实时管理资源和节点的创建/加载和删除/卸载。具有大型多样环境或程序生成的元素的游戏,通常会实行这些策略,以避免浪费内存。
另一方面, 对动态系统进行编码更复杂, 即, 使用更多的编程逻辑, 这会导致出现错误和bug的机会. 如果不小心的话, 开发的系统, 会增加应用程序的技术成本.
因此, 最好的选择是…
在小型游戏中使用静态关卡.
在开发中型/大型游戏时, 如果有时间/资源, 可以去创建一个可以对节点和资源的管理进行编码的库或插件. 如果随着时间的流逝而改进, 以提高可用性和稳定性, 那么它可能会演变成跨项目的可靠工具.
为一款中/大型游戏编写动态逻辑代码, 因为你拥有编程技能, 但却没有时间或资源去完善代码(必须要完成游戏). 以后可能会进行重构, 将代码外包到插件中.
有关在运行时中, 可以交换场景的各种方式的示例, 请参见文档 手动更改场景 .