資源

節點與資源

直到這個教學以前,我們都專注到 Godot 中的 Node 類別,因為節點是編寫行為以及大多數引擎功能所需要的東西。但還有另一種一樣重要的資料型別: Resource

節點 提供功能:繪製 Sprite、3D 模型、模擬物理、排列使用者界面…等。 資源 則是一個 資料容器 。資源本身並不會做任何事,反而是節點會使用放入了資料的資源。

任何 Godot 從硬碟中保存或讀取的都是資源。無論是場景( .tscn.scn 檔)、圖片、腳本…等。這裡有一些 Resource 的例子: TextureScriptMeshAnimationAudioStreamFontTranslation

當引擎從硬碟載入資源時 只會載入一次 。若記憶體內已經有該資源的拷貝,則再嘗試載入同樣的資源每次都會回傳相同的拷貝。因為資源至包含資料,所以不需要進行複製。

所有的物件,無論是節點或屬性,都能匯出屬性。屬性有許多種類,如 String、Integer、Vector2…等,這些所有的型別都可以轉換為資源。這表示節點與資源都還可以包含資源作為屬性:

../../_images/nodes_resources.png

外部 vs 內建

要保存資源有兩種方法:

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

  2. 內建 ,將資源保存在所附加的 .tscn 或是 .scn 檔內。

更具體來講,假設在 Sprite 節點中有一個 Texture

../../_images/spriteprop.png

點擊資源預覽可以讓我們檢視並編輯資源的屬性。

../../_images/resourcerobi.png

Path 屬性告訴我們資源來自哪裡。在這個例子中是來自檔名為 robi.png 的 PNG 圖片。當資源像這樣來自檔案時,即為外部資源。若將 Path 清空或 Path 為空,則變成內建資源。

內建與外部資源的切換會在保存場景時發生。在上方的例子中,若將 path "res://robi.png" 清空並保存,則 Godot 會將圖片保存在 .tscn 場景檔案裡。

備註

就算保存內建資源,當實體化場景多次,引擎也還是只會載入一次該資源的副本。

在程式碼中載入資源

要從程式碼中載入資源有兩種方法。第一種方法是使用 load() 函式,可以在任何時候使用:

func _ready():
        var res = load("res://robi.png") # Godot loads the Resource when it reads the line.
        get_node("sprite").texture = res
public override void _Ready()
{
    var texture = (Texture)GD.Load("res://robi.png"); // Godot loads the Resource when it reads the line.
    var sprite = GetNode<Sprite>("sprite");
    sprite.Texture = texture;
}

另外也可以 preload (預載)資源。與 load 不同的是,這個函式會從硬碟中讀取檔案並在編譯時期載入。因此,這種方法無法使用保存在變數內的路徑而必須使用常數字串。

func _ready():
        var res = preload("res://robi.png") # Godot loads the resource at compile-time
        get_node("sprite").texture = res
// 'preload()' is unavailable in C Sharp.

載入場景

場景也是資源,但有一點需要注意。保存在硬碟上的場景是 PackedScene 型別的資源。這種場景是被打包在資源內的。

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

func _on_shoot():
        var bullet = preload("res://bullet.tscn").instance()
        add_child(bullet)
private PackedScene _bulletScene = (PackedScene)GD.Load("res://bullet.tscn");

public void OnShoot()
{
    Node bullet = _bulletScene.Instance();
    AddChild(bullet);
}

這個方法會在場景結構中建立節點並對節點進行組態設定,最後回傳場景的根節點。接著便能將其新增為其他節點的子節點。

這種做法有幾種優點。由於 PackedScene.instance() 很快,所以可以建立新的敵人、子彈、效果…等而不需每次都從硬碟上載入。要記得,跟之前提到的一樣,圖片、網格…等都是場景間共通的。

釋放資源

當不需要再使用一個 Resource 時,資源會自動釋放自己。由於在大多數情況下,資源都包含在節點中,所以當釋放節點後引擎就會自動釋放所有沒有任何節點用到的資源。

建立自己的資源

就像 Godot 中的任何物件一樣,使用者也可以自己編寫資源。資源腳本繼承了能夠在物件屬性、序列化文字、與二進位資料 (*.tres、*.res) 間自由轉換的能力。資源也從 Reference 型別上繼承了引用計數的記憶體管理。

這比起其他替代類型的資料結構如 JSON、CSV、或是自定 TXT 檔多了許多直接的優點。使用者只能將這些資源匯入為 Dictionary (JSON) 或是匯入為 File 來解析。Resource 與這些型別不同的是它繼承了 ObjectReference 、與 Resource 的功能:

  • 可以定義常數,所以不需要仰賴其他資料欄位或是物件。

  • 可以定義方法,包含為屬性設定 setter 與 getter 方法。這樣以來即可抽象化與封裝基礎資料。若 Resource 腳本的結構需要修改時便不需要跟著改動遊戲。

  • 可以定義訊號,故 Resource 可以在其所管理的資料發生更改時觸發反應。

  • 可以定義屬性,這樣一來使用者便能 100% 確認資料存在。

  • Resource 的自動序列化與復原序列化 (Serialization/Deserialization) 是 Godot Engine 的內建功能。使用者毋須自己實作資源檔案資料的匯入/匯出。

  • 資源也可以遞歸序列化子資源,這表示使用者可以設計更複雜的資料結構。

  • 使用者能將資源保存為對版本控制友善的文字檔 (*.tres) 。在匯出遊戲後,Godot 會將資源檔案序列化為二進位檔 (*.res) 來提高速度與壓縮率。

  • Godot Engine 的屬性面板可開箱即用地對資源檔案算繪與編輯。因此,使用者通常不需要自己實作視覺化或編輯資料的自定邏輯。可以從在檔案系統 Dock 中對資源檔案點兩下,或是在屬性面板中點擊資料夾圖示並在對話框中打開檔案來進行。

  • 資源也可以擴充除了基本 Resource 型別以外的 其他 資源型別。

在 Godot 中可以很輕鬆地在屬性面板中建立自定資源。

  1. 在屬性面板中建立一個純資源。只要腳本是繼承自該類型,也可以建立衍生自資源的型別。

  2. 在屬性面板中將 script 屬性設為你的腳本。

接著屬性面板中便會顯示資源腳本的自定屬性。若有人編輯了屬性值並保存資源,則屬性面板也會序列化自定屬性。要從屬性面板中保存資源,點擊屬性面板的工具選單(右上角),並選擇「保存」或「另存新檔」。

若腳本所使用的語言支援 腳本類別 ,則這個過程還可以再簡化。只需要定義腳本的名稱,就會將類別新增到屬性面板的建立對話框中,如此一來會自動將腳本新增至建立的資源物件中。

來看看一些例子。

# bot_stats.gd
extends Resource
export(int) var health
export(Resource) var sub_resource
export(Array, String) var strings

# 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

# bot.gd
extends KinematicBody

export(Resource) var stats

func _ready():
    # Uses an implicit, duck-typed interface for any 'health'-compatible resources.
    if stats:
        print(stats.health) # Prints '10'.
// BotStats.cs
using System;
using Godot;

namespace ExampleProject {
    public class BotStats : Resource
    {
        [Export]
        public int Health { get; set; }

        [Export]
        public Resource SubResource { get; set; }

        [Export]
        public String[] Strings { get; set; }

        // Make sure that every parameter has a default value.
        // Otherwise, there will be problems with creating and editing
        // your resource via the inspector.
        public BotStats(int health = 0, Resource subResource = null, String[] strings = null)
        {
            Health = health;
            SubResource = subResource;
            Strings = strings ?? new String[0];
        }
    }
}

// Bot.cs
using System;
using Godot;

namespace ExampleProject {
    public class Bot : KinematicBody
    {
        [Export]
        public Resource Stats;

        public override void _Ready()
        {
            if (Stats != null && Stats is BotStats botStats) {
                GD.Print(botStats.Health); // Prints '10'.
            }
        }
    }
}

備註

資源腳本與 Unity 的 ScriptableObject 類似。屬性面板提供了對自定資源的內建資源。但若有需要,使用者也可以自己設計基於 Control 的工具腳本,並與 EditorPlugin 結合使用來為資源的資料建立自定的視覺界面與編輯器。

Unreal Engine 4 的 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)
using System;
using Godot;

public class BotStatsTable : Resource
{
    private Godot.Dictionary<String, BotStats> _stats = new Godot.Dictionary<String, BotStats>();

    public BotStatsTable()
    {
        _stats["GodotBot"] = new BotStats(10); // Creates instance with 10 health.
        _stats["DifferentBot"] = new BotStats(20); // A different one with 20 health.
        GD.Print(_stats);
    }
}

除了直接在行內寫上字典值,也可以…

  1. 從試算表中匯入整張表的值並產生索引鍵/值組,或…

  2. 在編輯器中設計視覺界面並製作一個簡單的插件來在開啟這類資源型別的時候顯示在屬性面板中。

CurveTable 是同樣的東西,不同的地方是 CurveTable 是映射到一組以浮點數為值的陣列或 Curve/Curve2D 資源物件。

警告

請注意,資源檔案 (*.tres/*.res) 會檔案中保存資源所使用的腳本路徑。載入後,資源會抓取並以擴充其型別的方式載入腳本。這表示將無法指派子類別,如腳本的內部類別(如在 GDScript 內使用 class 關鍵字)。Godot 不會正確地序列化腳本內子類別的自定屬性。

下方的例子裡,Godot 會載入 Node 腳本,並確認其是否繼承了 Resource ,然後判斷在 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("res://my_res.tres", my_res)
using System;
using Godot;

public class MyNode : Node
{
    public class MyResource : Resource
    {
        [Export]
        public int Value { get; set; } = 5;
    }

    public override void _Ready()
    {
        var res = new MyResource();

        // This will NOT serialize the 'Value' property.
        ResourceSaver.Save("res://MyRes.tres", res);
    }
}