逻辑偏好

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

加载VS预加载

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

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

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

GDScript

C#

  1. # my_buildings.gd
  2. extends Node
  3. # Note how constant scripts/scenes have a different naming scheme than
  4. # their property variants.
  5. # This value is a constant, so it spawns when the Script object loads.
  6. # The script is preloading the value. The advantage here is that the editor
  7. # can offer autocompletion since it must be a static path.
  8. const BuildingScn = preload("res://building.tscn")
  9. # 1. The script preloads the value, so it will load as a dependency
  10. # of the 'my_buildings.gd' script file. But, because this is a
  11. # property rather than a constant, the object won't copy the preloaded
  12. # PackedScene resource into the property until the script instantiates
  13. # with .new().
  14. #
  15. # 2. The preloaded value is inaccessible from the Script object alone. As
  16. # such, preloading the value here actually does not benefit anyone.
  17. #
  18. # 3. Because the user exports the value, if this script stored on
  19. # a node in a scene file, the scene instantiation code will overwrite the
  20. # preloaded initial value anyway (wasting it). It's usually better to
  21. # provide null, empty, or otherwise invalid default values for exports.
  22. #
  23. # 4. It is when one instantiates this script on its own with .new() that
  24. # one will load "office.tscn" rather than the exported value.
  25. export(PackedScene) var a_building = preload("office.tscn")
  26. # Uh oh! This results in an error!
  27. # One must assign constant values to constants. Because `load` performs a
  28. # runtime lookup by its very nature, one cannot use it to initialize a
  29. # constant.
  30. const OfficeScn = load("res://office.tscn")
  31. # Successfully loads and only when one instantiates the script! Yay!
  32. var office_scn = load("res://office.tscn")
  1. using System;
  2. using Godot;
  3. // C# and other languages have no concept of "preloading".
  4. public class MyBuildings : Node
  5. {
  6. //This is a read-only field, it can only be assigned when it's declared or during a constructor.
  7. public readonly PackedScene Building = ResourceLoader.Load<PackedScene>("res://building.tscn");
  8. public PackedScene ABuilding;
  9. public override void _Ready()
  10. {
  11. // Can assign the value during initialization.
  12. ABuilding = GD.Load<PackedScene>("res://office.tscn");
  13. }
  14. }

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

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

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

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

    1. 如果 ‘导入’ 的类有可能发生变化, 那么它应该是一个属性, 使用 exportload 进行初始化(甚至可能以后才初始化).

    2. 如果脚本需要大量依赖关系, 而又不想消耗太多内存, 则可能希望在环境变化时, 在运行时中加载和卸载各种依赖关系. 如果将资源预加载为常量, 则卸载这些资源的唯一方法是卸载整个脚本. 如果改为加载属性, 则可以将它们设置为 null, 并完全删除对资源的所有引用(作为一个 Reference 扩展类型, 将导致资源从内存中删除自己).

大型关卡: 静态VS动态

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

答案很简单,”当性能需要的时候.” 与这两种选择有关的困境, 是一种古老的编程选择: 是否会优化内存而不是速度, 反之亦然?

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

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

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

因此, 最好的选择是…

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

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

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

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