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")
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");
}
}
預加載可以讓腳本在載入腳本的時候處理所有的載入。預加載很好用,但有時我們不會想用預加載。為了搞清楚是指那些情況,我們可以想想下面這幾個問題:
如果我們無法判斷腳本什麼時候會載入,那麼,(特別是在場景或腳本中)預加載資源可能會進一步導致未預期的載入。這樣可能會導致除了原本腳本的載入操作意外,多出意外且動態長度的載入時間。
若可以使用其他東西來代替某個值 (特別是場景的匯出初始化),則預加載這個值就沒有意義。如果平常都是自行製作腳本的話,那麼這一點就並非重要因素。
如果只是想要「匯入」另一個類別資源 (腳本或場景),那麼使用預加載的常數應該是最好的方法。但是,有幾個例外情況下最好不要這麼做:
若「匯入」的類別很有可能會更改的話,則應該使用屬性來代替,使用
export
或load
來初始化 (或甚至不要一開始就初始化)。若腳本有大量的相依性,則我們通常不會想消耗太多的記憶體,而會想在執行階段依據特定情況來載入或取消載入各種相依性。若我們將資源預加載進常數內,那麼要取消載入這些資源就只能把整個腳本都取消載入了。而若改用屬性來載入的話,那我們就能通過將屬性設為
null
並完整移除該屬性的所有參照來取消載入 (這是指,繼承了 Reference 的型別會在這麼做之後將自己從記憶體內刪除)。
大型關卡:靜態 vs. 動態¶
在建立大型關卡時,哪個情況才適用呢?要將關卡製作成一個獨立的靜態空間嗎?還是應該將關卡切成各種小部分並隨著需求來在世界內容中切換?
這個嘛,用一句話來回答的話就是,「如果效能有需要的時候。」。其實這種兩難的情況跟一個古老的程式設計問題有關:要對記憶體最佳化還是對速度最佳化?
而最簡單的方法就是使用單一靜態關卡,並一次載入所有東西。但是,依據不同的專案,這樣可能會吃掉很多的記憶體。浪費使用者的 RAM 也會導致程式執行地很慢,或是當電腦嘗試同時做其他事情時讓遊戲當機。
無論如何,我們都應該將大型場景切分成許多小的場景 (來提升素材的可復用性)。開發人員可以設計一個節點,來即時管理資源與節點的建立/載入與刪除/取消載入。有大型且多樣化環境的遊戲,或是會連續產生多個元素的遊戲,通常會使用這種策略來減少浪費記憶體。
另一方面,要撰寫一套動態的系統是一件複雜的工作——會使用到更多的程式邏輯。而使用到更多邏輯,就代表產生錯誤與 Bug 的機率更高。一不小心就有可能因為開發了這個系統而增加了許多技術債。
因此,最好的做法應該是…
在小型遊戲上使用靜態關卡。
如果有時間與資源來處理中大型遊戲的話,那麼可以製作一個函式庫或外掛程式來管理資源與節點。如果可以不斷地改進可用性與穩定性的話,那麼這套工具可以發展成在各個專案間可靠的工具。
如果有能力但沒有時間或資源來修改程式的話 (因為必須要最好遊戲),可以為中大型遊戲撰寫動態邏輯。之後可能有機會重構並將這部分的程式拆解為外掛。
有關在執行時間切換場景的各種方法的範例,請參考 「手動更改場景」 說明文件。