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.

邏輯偏好

可曾想過問題 X 要用策略 Y 還是 Z 來解決?本篇文章將探討關於這類難題的各種主題。

先新增節點還是先修改屬性?

運作時使用腳本初始化節點時,你可能需要對節點的名稱、位置等屬性進行修改。常見的糾結點在於,你應該什麼時候去修改?

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.

基於這些原因,通常最佳實務是在將節點加入場景樹之前設定其初始值。但也存在一些例外情況,某些值在加入場景樹之前 無法 設定,例如設定全域位置。

載入 (Load) vs. 預載入 (Preload)

在 GDScript 中,有一個全域的 preload 方法。通過這個方法可以儘量將「載入」操作提早執行,並避免在效能敏感的程式執行時載入資源。

相對地,load 方法則只會在碰到 load 陳述式時才載入資源。也就是說,load 方法只會就地載入資源,而當在敏感的處理過程中使用 load 就會讓程式變慢。 load 方法同時也是 ResourceLoader.load(path) 的別名,這個方法可以在 所有 腳本語言中存取。

那麼,Load 與 Preload 實際上到底差在哪裡呢?什麼時候又該用什麼呢?請看看這個範例:

# 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")

Preloading allows the script to handle all the loading the moment one loads the script. Preloading is useful, but there are also times when one doesn't wish to use it. Here are a few considerations when determining which to use:

  1. If one cannot determine when the script might load, then preloading a resource (especially a scene or script) could result in additional loads one does not expect. This could lead to unintentional, variable-length load times on top of the original script's load operations.

  2. 若可以使用其他東西來代替某個值 (特別是場景的匯出初始化),則預加載這個值就沒有意義。如果平常都是自行製作腳本的話,那麼這一點就並非重要因素。

  3. 如果只是想要「匯入」另一個類別資源 (腳本或場景),那麼使用預加載的常數應該是最好的方法。但是,有幾個例外情況下最好不要這麼做:

    1. 如果這個「被匯入」的類別可能會變動,那麼它應該是一個屬性,並透過 @exportload() 進行初始化(甚至可能延後初始化)。

    2. If the script requires a great many dependencies, and one does not wish to consume so much memory, then one may wish to load and unload various dependencies at runtime as circumstances change. If one preloads resources into constants, then the only way to unload these resources would be to unload the entire script. If they are instead loaded as properties, then one can set these properties to null and remove all references to the resource (which, as a RefCounted-extending type, will cause the resources to delete themselves from memory).

大型關卡:靜態 vs. 動態

If one is creating a large level, which circumstances are most appropriate? Is it better to create the level as one static space? Or is it better to load the level in pieces and shift the world's content as needed?

這個嘛,用一句話來回答的話就是,「如果效能有需要的時候。」。其實這種兩難的情況跟一個古老的程式設計問題有關:要對記憶體最佳化還是對速度最佳化?

而最簡單的方法就是使用單一靜態關卡,並一次載入所有東西。但是,依據不同的專案,這樣可能會吃掉很多的記憶體。浪費使用者的 RAM 也會導致程式執行地很慢,或是當電腦嘗試同時做其他事情時讓遊戲當機。

無論如何,我們都應該將大型場景切分成許多小的場景 (來提升素材的可復用性)。開發人員可以設計一個節點,來即時管理資源與節點的建立/載入與刪除/取消載入。有大型且多樣化環境的遊戲,或是會連續產生多個元素的遊戲,通常會使用這種策略來減少浪費記憶體。

On the flip side, coding a dynamic system is more complex; it uses more programmed logic which results in opportunities for errors and bugs. If one isn't careful, they can develop a system that bloats the technical debt of the application.

因此,最好的做法應該是…

  1. Use static levels for smaller games.

  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. Use dynamic logic for a medium/large game because one has the coding skills, but not the time or resources to refine the code (game's gotta get done). Could potentially refactor later to outsource the code into a plugin.

有關在執行時間切換場景的各種方法的範例,請參考 「手動更改場景」 說明文件。