資源

節點與資源

在這個教學之前,我們都專注於 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. Create a new Resource object in the Inspector. This can even be a type that derives Resource, so long as your script is extending that type.

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

The Inspector will now display your Resource script's custom properties. If one edits those values and saves the resource, the Inspector serializes the custom properties too! To save a resource from the Inspector, click the save icon at the top of the Inspector, and select "Save" or "Save As...".

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

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

備註

To make the new resource class appear in the Create Resource GUI you need to provide a class name for GDScript, or use the [GlobalClass] attribute in C#.

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