資源

節點與資源

在這個教學之前,我們都專注於 Godot 中的 Node 類別,因為你會用節點來編寫行為,而大多數引擎功能也都依賴於此。但還有一種同樣重要的資料型別:Resource

節點 提供功能:例如繪製精靈(Sprite)、3D 模型、模擬物理、排列使用者介面等;資源 則是 資料容器,它們本身不會執行任何動作,而是由節點來使用資源中的資料。

Godot 從磁碟儲存或載入的任何內容都是資源。無論是場景(.tscn.scn 檔)、圖片、腳本……以下是一些 Resource 的例子:

當引擎從硬碟載入資源時,只會載入一次。如果該資源的實體已經存在於記憶體中,再次載入同一資源時都會回傳同一份拷貝。由於資源僅包含資料,因此不需要重複複製。

每個物件,無論是節點還是資源,都可以匯出屬性。屬性有許多型別,例如字串、整數、Vector2 等,而這些型別都可以成為資源。這代表節點和資源都可以將其他資源作為屬性來包含:

../../_images/nodes_resources.webp

外部資源與內建資源

資源有兩種保存方式:

  1. 外部,存放在場景之外,作為獨立檔案儲存於硬碟上。

  2. 內建,儲存在所屬的 .tscn.scn 場景檔案之內。

更具體來說,以下範例是在 Sprite2D 節點中指派了一個 Texture2D

../../_images/spriteprop.webp

點擊資源預覽圖可以查看該資源的屬性。

../../_images/resourcerobi.webp

Path 屬性會顯示資源的來源。在這個例子中,來源是一個名為 robi.png 的 PNG 圖片。當資源來自這類檔案時,屬於外部資源;如果將 Path 清空或置空,該資源就變成內建資源。

內建資源與外部資源的切換會在你儲存場景時發生。在上述例子中,如果你將 path 設為空字串並儲存,Godot 會把圖片嵌入到 .tscn 場景檔案內。

備註

即使你儲存的是內建資源,當多次實例化場景時,引擎也只會載入資源的一份拷貝。

從程式碼載入資源

要從程式碼中載入資源有兩種方式。第一種是隨時呼叫 load() 函式:

func _ready():
    # Godot loads the Resource when it reads this very line.
    var imported_resource = load("res://robi.png")
    $sprite.texture = imported_resource

你也可以使用 preload 預載資源。和 load 不同,這個函式會在編譯階段就從硬碟讀取檔案並載入。因此,preload 不能使用變數路徑,只能用常數字串。

func _ready():
    # Godot loads the resource at compile-time
    var imported_resource = preload("res://robi.png")
    get_node("sprite").texture = imported_resource

載入場景

場景也是一種資源,但有個地方要特別注意。儲存到硬碟上的場景是 PackedScene 型別的資源。也就是說,場景被包裝在一個 Resource 物件中。

若要取得場景的實體,必須使用 PackedScene.instantiate() 方法。

func _on_shoot():
        var bullet = preload("res://bullet.tscn").instantiate()
        add_child(bullet)

這個方法會依照場景階層建立節點、設定好屬性,然後回傳場景的根節點。你可以將它加入為任何其他節點的子節點。

這種做法有不少優點。由於 PackedScene.instantiate() 相當快速,你可以隨時建立新的敵人、子彈、特效等,而不用每次都從硬碟重新載入。請記得,圖片、網格等資源在所有場景實體之間都是共用的。

釋放資源

Resource 不再被使用時,會自動釋放自身。在大多數情況下,資源是被節點所持有的,因此當你釋放節點時,只要沒有其他節點正在使用該資源,引擎也會一併釋放它。

建立自訂資源

如同 Godot 中的任何物件,使用者也能為資源撰寫腳本。資源腳本繼承了在物件屬性與序列化文字或二進位資料(*.tres、*.res)間自由轉換的能力,也繼承自 RefCounted 型別的引用計數記憶體管理。

這相較於其他資料結構(如 JSON、CSV 或自訂 TXT 檔案)有許多明顯優勢。使用者只能將這些檔案匯入為 Dictionary 來解析。而 Resource 最大的不同點在於它繼承了 ObjectRefCounted、以及 Resource 的特性:

  • 可以定義常數,免去從其他資料欄位或物件取得常數的需求。

  • 可以定義方法,包括為屬性設定 setter/getter。這能將底層資料抽象化並封裝起來,即使資源腳本結構未來變動,使用該資源的遊戲內容不必跟著改動。

  • 可以定義訊號,讓資源在其資料變動時觸發對應的反應。

  • 可以明確定義屬性,讓使用者確信資料一定存在。

  • 資源的自動序列化與反序列化是 Godot Engine 的內建功能,使用者不需要自訂實作資源檔案資料的匯入/匯出。

  • 資源甚至可以遞迴序列化子資源,因此你可以設計更複雜的資料結構。

  • 使用者可以將資源儲存為適合版本控制的文字檔格式(*.tres)。遊戲匯出時,Godot 會將資源檔案序列化成二進位檔(*.res),提升速度與壓縮率。

  • Godot Engine 的屬性檢查器(Inspector)可以直接顯示與編輯資源檔案,因此使用者通常不需要自訂資料的視覺化或編輯邏輯。只要在檔案系統(FileSystem) Dock 中雙擊資源檔,或在屬性檢查器裡點擊資料夾圖示並於對話框中開啟檔案即可。

  • 除了基本 Resource 之外,也能擴充 其他 資源型別。

Godot 讓你可以在屬性檢查器中輕鬆建立自訂資源。

  1. 在屬性檢查器(Inspector)中建立一個 Resource 物件。只要腳本有繼承該型別,也能建立衍生自 Resource 的型別。

  2. 在屬性檢查器中將 script 屬性設為你的腳本。

此時檢查器會顯示你的資源腳本的自訂屬性。若編輯了這些值並儲存資源,檢查器也會將自訂屬性一併序列化!要在檢查器中儲存資源,請點擊檢查器頂端的儲存圖示,並選擇「儲存」或「另存新檔…」。

若腳本語言支援 腳本類別,這個流程會更簡潔。只要在腳本內定義類別名稱,該類別就會自動出現在屬性檢查器的建立對話框,並自動將你的腳本加到你建立的資源物件上。

讓我們看個例子。先建立一個 Resource 並命名為 bot_stats,它會以 bot_stats.tres 的全名出現在你的檔案列表中。沒有附加腳本的話沒什麼用,所以我們來加點資料和邏輯吧!附加名為 bot_stats.gd 的腳本(或新建一個腳本再拖曳進去)。

備註

若要讓新的資源類別出現在「建立資源」介面中,你需要在 GDScript 中提供類別名稱,或在 C# 中使用 [GlobalClass] 屬性。

class_name BotStats
extends Resource

@export var health: int
@export var sub_resource: Resource
@export var strings: PackedStringArray

# Make sure that every parameter has a default value.
# Otherwise, there will be problems with creating and editing
# your resource via the inspector.
func _init(p_health = 0, p_sub_resource = null, p_strings = []):
    health = p_health
    sub_resource = p_sub_resource
    strings = p_strings

現在,建立一個 CharacterBody3D,命名為 Bot,然後加上下列腳本:

extends CharacterBody3D

@export var stats: Resource

func _ready():
    # Uses an implicit, duck-typed interface for any 'health'-compatible resources.
    if stats:
        stats.health = 10
        print(stats.health)
        # Prints "10"

現在,選擇我們命名為 botCharacterBody3D 節點,然後將 bot_stats.tres 資源拖曳到屬性檢查器上。應該會印出 10!當然,這個做法可以延伸到更高階的功能,只要你理解 整個原理,資源系統的其他應用也就能迎刃而解。

備註

資源腳本很像 Unity 的 ScriptableObject。屬性檢查器本身就支援自訂資源。若有需要,用戶也可以設計自己基於 Control 的工具腳本,並與 EditorPlugin 搭配,為資料打造自訂的視覺化介面與編輯器。

Unreal Engine 的 DataTable 和 CurveTable 也能很容易用資源腳本重現。DataTable 就是將字串對應到自訂結構,類似於 Dictionary 將字串對應到一個自訂資源腳本。

# bot_stats_table.gd
extends Resource

const BotStats = preload("bot_stats.gd")

var data = {
    "GodotBot": BotStats.new(10), # Creates instance with 10 health.
    "DifferentBot": BotStats.new(20) # A different one with 20 health.
}

func _init():
    print(data)

除了直接在程式內寫死 Dictionary 值之外,你也可以:

  1. 從試算表匯入資料並產生鍵值對。

  2. 在編輯器內設計視覺化介面,並建立外掛程式,讓你在開啟這類資源時能將該視覺化介面加到屬性檢查器中。

CurveTable 也是類似概念,只是把鍵對應到一組浮點數陣列或 Curve / Curve2D 資源物件。

警告

請注意,資源檔(*.tres/*.res)會在檔案中存放它們所使用的腳本路徑。載入時,會依此路徑抓取並載入該腳本,作為其型別的擴充。因此,嘗試指定腳本的內部類別(亦即在 GDScript 中使用 class 關鍵字定義的內部類別)將無法運作。Godot 不會正確序列化腳本內部類別的自訂屬性。

在下方例子中,Godot 會嘗試載入 Node 腳本,發現它沒有繼承 Resource,因型別不相容而導致資源物件載入腳本失敗。

extends Node

class MyResource:
    extends Resource
    @export var value = 5

func _ready():
    var my_res = MyResource.new()

    # This will NOT serialize the 'value' property.
    ResourceSaver.save(my_res, "res://my_res.tres")