自訂資源格式載入器

前言

ResourceFormatLoader 是用來載入檔案資產的工廠介面。資源是主要的容器,當再次於相同檔案路徑呼叫載入時,會參照到先前已載入的 Resource。理論上,載入的資源必須是無狀態的。

本指南假設讀者已經了解如何建立 C++ 模組與 Godot 資料型別。若不熟悉,請參考 以 C++ 自訂模組 此指南

參考資料

用途說明?

  • 新增多種檔案格式的支援

  • 音訊格式

  • 影片格式

  • 機器學習模型

不適用於哪些?

  • 點陣圖

應使用 ImageFormatLoader 來載入圖片。

參考資料

建立 ResourceFormatLoader

每個檔案格式都包含一個資料容器以及一個 ResourceFormatLoader

ResourceFormatLoader 是回傳所有必要後設資料以支援 Godot 新副檔名的類別。該類別必須回傳格式名稱與副檔名字串。

此外,ResourceFormatLoader 必須實作 load 函式,將檔案路徑轉換為資源。載入資源時,load 必須負責讀取並處理資料序列化。

resource_loader_json.h
#pragma once

#include "core/io/resource_loader.h"

class ResourceFormatLoaderJson : public ResourceFormatLoader {
    GDCLASS(ResourceFormatLoaderJson, ResourceFormatLoader);
public:
    virtual RES load(const String &p_path, const String &p_original_path, Error *r_error = NULL);
    virtual void get_recognized_extensions(List<String> *r_extensions) const;
    virtual bool handles_type(const String &p_type) const;
    virtual String get_resource_type(const String &p_path) const;
};
resource_loader_json.cpp
#include "resource_loader_json.h"

#include "resource_json.h"

RES ResourceFormatLoaderJson::load(const String &p_path, const String &p_original_path, Error *r_error) {
Ref<JsonResource> json = memnew(JsonResource);
    if (r_error) {
        *r_error = OK;
    }
    Error err = json->load_file(p_path);
    return json;
}

void ResourceFormatLoaderJson::get_recognized_extensions(List<String> *r_extensions) const {
    if (!r_extensions->find("json")) {
        r_extensions->push_back("json");
    }
}

String ResourceFormatLoaderJson::get_resource_type(const String &p_path) const {
    return "Resource";
}

bool ResourceFormatLoaderJson::handles_type(const String &p_type) const {
    return ClassDB::is_parent_class(p_type, "Resource");
}

建立 ResourceFormatSaver

若希望能夠編輯並儲存資源,可實作一個 ResourceFormatSaver

resource_saver_json.h
#pragma once

#include "core/io/resource_saver.h"

class ResourceFormatSaverJson : public ResourceFormatSaver {
    GDCLASS(ResourceFormatSaverJson, ResourceFormatSaver);
public:
    virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0);
    virtual bool recognize(const RES &p_resource) const;
    virtual void get_recognized_extensions(const RES &p_resource, List<String> *r_extensions) const;
};
resource_saver_json.cpp
#include "resource_saver_json.h"

#include "resource_json.h"
#include "scene/resources/resource_format_text.h"

Error ResourceFormatSaverJson::save(const String &p_path, const RES &p_resource, uint32_t p_flags) {
    Ref<JsonResource> json = memnew(JsonResource);
    Error error = json->save_file(p_path, p_resource);
    return error;
}

bool ResourceFormatSaverJson::recognize(const RES &p_resource) const {
    return Object::cast_to<JsonResource>(*p_resource) != NULL;
}

void ResourceFormatSaverJson::get_recognized_extensions(const RES &p_resource, List<String> *r_extensions) const {
    if (Object::cast_to<JsonResource>(*p_resource)) {
        r_extensions->push_back("json");
    }
}

建立自訂資料型別

Godot 的 核心型別 或管理的資源中可能沒有適當的替代品。這時需註冊新的資料型別,讓 Godot 能夠辨識如機器學習模型等額外的二進位格式。

以下為建立自訂資料型別的範例:

resource_json.h
#pragma once

#include "core/io/json.h"
#include "core/variant_parser.h"

class JsonResource : public Resource {
    GDCLASS(JsonResource, Resource);

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("set_dict", "dict"), &JsonResource::set_dict);
        ClassDB::bind_method(D_METHOD("get_dict"), &JsonResource::get_dict);

        ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "content"), "set_dict", "get_dict");
    }

private:
    Dictionary content;

public:
    Error load_file(const String &p_path);
    Error save_file(const String &p_path, const RES &p_resource);

    void set_dict(const Dictionary &p_dict);
    Dictionary get_dict();
};
resource_json.cpp
#include "resource_json.h"

Error JsonResource::load_file(const String &p_path) {
    Error error;
    FileAccess *file = FileAccess::open(p_path, FileAccess::READ, &error);
    if (error != OK) {
        if (file) {
            file->close();
        }
        return error;
    }

    String json_string = String("");
    while (!file->eof_reached()) {
        json_string += file->get_line();
    }
    file->close();

    String error_string;
    int error_line;
    JSON json;
    Variant result;
    error = json.parse(json_string, result, error_string, error_line);
    if (error != OK) {
        file->close();
        return error;
    }

    content = Dictionary(result);
    return OK;
}

Error JsonResource::save_file(const String &p_path, const RES &p_resource) {
    Error error;
    FileAccess *file = FileAccess::open(p_path, FileAccess::WRITE, &error);
    if (error != OK) {
        if (file) {
            file->close();
        }
        return error;
    }

    Ref<JsonResource> json_ref = p_resource.get_ref_ptr();
    JSON json;

    file->store_string(json.print(json_ref->get_dict(), "    "));
    file->close();
    return OK;
}

void JsonResource::set_dict(const Dictionary &p_dict) {
    content = p_dict;
}

Dictionary JsonResource::get_dict() {
    return content;
}

注意事項

有些函式庫可能未定義像是 IO 處理等常用操作,因此必須將 Godot 的呼叫進行轉換。

例如,以下程式碼示範如何將 FileAccess 呼叫轉換為 std::istream

#include "core/io/file_access.h"

#include <istream>
#include <streambuf>

class GodotFileInStreamBuf : public std::streambuf {

public:
    GodotFileInStreamBuf(FileAccess *fa) {
        _file = fa;
    }
    int underflow() {
        if (_file->eof_reached()) {
            return EOF;
        } else {
            size_t pos = _file->get_position();
            uint8_t ret = _file->get_8();
            _file->seek(pos); // Required since get_8() advances the read head.
            return ret;
        }
    }
    int uflow() {
        return _file->eof_reached() ? EOF : _file->get_8();
    }

private:
    FileAccess *_file;
};

參考資料

註冊新檔案格式

Godot 會利用 ResourceLoader 處理器註冊 ResourceFormatLoader,當 load 被呼叫時,該處理器會自動選擇適合的載入器。

register_types.h
void register_json_types();
void unregister_json_types();
register_types.cpp
#include "register_types.h"

#include "core/class_db.h"
#include "resource_loader_json.h"
#include "resource_saver_json.h"
#include "resource_json.h"

static Ref<ResourceFormatLoaderJson> json_loader;
static Ref<ResourceFormatSaverJson> json_saver;

void register_json_types() {
    ClassDB::register_class<JsonResource>();

    json_loader.instantiate();
    ResourceLoader::add_resource_format_loader(json_loader);

    json_saver.instantiate();
    ResourceSaver::add_resource_format_saver(json_saver);
}

void unregister_json_types() {
    ResourceLoader::remove_resource_format_loader(json_loader);
    json_loader.unref();

    ResourceSaver::remove_resource_format_saver(json_saver);
    json_saver.unref();
}

參考資料

在 GDScript 載入

建立一個名為 demo.json 的檔案,填入以下內容,並放在專案根目錄下:

{
  "savefilename": "demo.json",
  "demo": [
    "welcome",
    "to",
    "godot",
    "resource",
    "loaders"
  ]
}

然後將下列腳本附加到任意節點:

extends Node

@onready var json_resource = load("res://demo.json")

func _ready():
    print(json_resource.get_dict())