Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

邏輯偏好

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

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

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

最佳實踐是在節點加入場景樹之前修改取值。部分屬性的 setter 程式碼會更新其他對應的值,可能會比較慢!大多數情況下,這樣的程式碼不會對遊戲的性能產生影響,但對於程式式生成之類的重型使用場景,就可能讓遊戲卡成 PPT。

綜上,最佳的做法就是先為節點設定初始值,然後再把它新增到場景樹中。

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

    1. 若「匯入」的類別很有可能會更改的話,則應該使用屬性來代替,使用 exportload 來初始化 (或甚至不要一開始就初始化)。

    2. 若腳本有大量的相依性,則我們通常不會想消耗太多的記憶體,而會想在執行階段依據特定情況來載入或取消載入各種相依性。若我們將資源預加載進常數內,那麼要取消載入這些資源就只能把整個腳本都取消載入了。而若改用屬性來載入的話,那我們就能通過將屬性設為 null 並完整移除該屬性的所有參照來取消載入 (這是指,繼承了 Reference 的型別會在這麼做之後將自己從記憶體內刪除)。

大型關卡:靜態 vs. 動態

在建立大型關卡時,哪個情況才適用呢?要將關卡製作成一個獨立的靜態空間嗎?還是應該將關卡切成各種小部分並隨著需求來在世界內容中切換?

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

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

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

另一方面,要撰寫一套動態的系統是一件複雜的工作——會使用到更多的程式邏輯。而使用到更多邏輯,就代表產生錯誤與 Bug 的機率更高。一不小心就有可能因為開發了這個系統而增加了許多技術債。

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

  1. 在小型遊戲上使用靜態關卡。

  2. 如果有時間與資源來處理中大型遊戲的話,那麼可以製作一個函式庫或外掛程式來管理資源與節點。如果可以不斷地改進可用性與穩定性的話,那麼這套工具可以發展成在各個專案間可靠的工具。

  3. 如果有能力但沒有時間或資源來修改程式的話 (因為必須要最好遊戲),可以為中大型遊戲撰寫動態邏輯。之後可能有機會重構並將這部分的程式拆解為外掛。

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