Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

导入插件

备注

本教程假设你已经知道如何制作通用插件. 如有疑问, 请参阅 制作插件 页面. 这也假设你熟悉Godot的导入系统.

前言

导入插件是一种特殊的编辑器工具, 它允许Godot导入自定义资源, 并将其作为一级资源对待. 编辑器本身捆绑了很多导入插件来处理常见的资源, 如PNG图片, Collada和glTF模型, Ogg Vorbis声音等等.

This tutorial shows how to create an import plugin to load a custom text file as a material resource. This text file will contain three numeric values separated by comma, which represents the three channels of a color, and the resulting color will be used as the albedo (main color) of the imported material. In this example it contains the pure blue color (zero red, zero green, and full blue):

0,0,255

配置

首先, 我们需要一个通用插件来处理导入插件的初始化和销毁. 让我们先添加 plugin.cfg 文件:

[plugin]

name="Silly Material Importer"
description="Imports a 3D Material from an external text file."
author="Yours Truly"
version="1.0"
script="material_import.gd"

然后我们需要 material_import.gd 文件来在需要时添加和删除导入插件:

# material_import.gd
@tool
extends EditorPlugin


var import_plugin


func _enter_tree():
    import_plugin = preload("import_plugin.gd").new()
    add_import_plugin(import_plugin)


func _exit_tree():
    remove_import_plugin(import_plugin)
    import_plugin = null

当这个插件被激活时, 它将创建一个新的导入插件实例(我们很快就会制作), 并使用 add_import_plugin() 方法将其加入编辑器. 我们在类成员 import_plugin' 中存储它的引用, 这样我们就可以在以后删除它时引用它. remove_import_plugin() 方法在插件停用时被调用, 以清理内存并让编辑器知道导入插件不再可用.

注意, 导入插件是一个引用类型, 所以它不需要明确地用 free() 函数从内存中释放. 当它超出范围时, 将被引擎自动释放.

EditorImportPlugin 类

这个展示的主角是 EditorImportPlugin 类. 它负责实现Godot需要知道如何处理文件时调用的方法.

让我们开始编写我们的插件, 一个方法:

# import_plugin.gd
@tool
extends EditorImportPlugin


func _get_importer_name():
    return "demos.sillymaterial"

The first method is the _get_importer_name(). This is a unique name for your plugin that is used by Godot to know which import was used in a certain file. When the files needs to be reimported, the editor will know which plugin to call.

func _get_visible_name():
    return "Silly Material"

The _get_visible_name() method is responsible for returning the name of the type it imports and it will be shown to the user in the Import dock.

你选择的名字应该可以接到“导入为”后面,例如“导入为 Silly Material”。你可以随心所欲地命名,但我们建议为你的插件起一个描述性的名字。

func _get_recognized_extensions():
    return ["mtxt"]

Godot's import system detects file types by their extension. In the _get_recognized_extensions() method you return an array of strings to represent each extension that this plugin can understand. If an extension is recognized by more than one plugin, the user can select which one to use when importing the files.

小技巧

许多插件可能会使用像 .json.txt 这样的常见扩展. 此外, 项目中可能存在仅作为游戏数据的文件, 不应导入. 导入时必须小心以验证数据. 永远不要指望文件格式正确.

func _get_save_extension():
    return "material"

导入的文件被保存在项目根部的 .import 文件夹中. 它们的扩展名应与你要导入的资源类型相匹配, 但由于Godot不能告诉你将使用什么(因为同一资源可能有多个有效的扩展名), 你需要声明将在导入时使用的内容.

由于我们正在导入材质, 因此我们将对此类资源类型使用特殊扩展. 如果要导入场景, 可以使用 scn . 通用资源可以使用 res 扩展名. 但是, 引擎不会以任何方式强制执行此操作.

func _get_resource_type():
    return "StandardMaterial3D"

导入的资源具有特定类型,编辑器可以据此知道它属于哪个属性槽。这样就能够将其从文件系统面板拖放到检查器的属性之中。

In our case it's a StandardMaterial3D, which can be applied to 3D objects.

备注

如果需要从同一扩展中导入不同类型, 则必须创建多个导入插件. 你可以在另一个文件上抽象导入代码, 以避免在这方面出现重复.

选项和预设

你的插件可以提供不同的选项, 以允许用户控制资源的导入方式. 如果一组选定的选项很常见, 你还可以创建不同的预设以使用户更容易. 下图显示了选项在编辑器中的显示方式:

../../../_images/import_plugin_options.png

由于可能有许多预设并且它们用数字标识, 因此使用枚举是一个很好的做法, 因此你可以使用名称来引用它们.

@tool
extends EditorImportPlugin


enum Presets { DEFAULT }


...

既然定义了枚举, 让我们继续看一下导入插件的方法:

func _get_preset_count():
    return Presets.size()

The _get_preset_count() method returns the amount of presets that this plugins defines. We only have one preset now, but we can make this method future-proof by returning the size of our Presets enumeration.

func _get_preset_name(preset_index):
    match preset_index:
        Presets.DEFAULT:
            return "Default"
        _:
            return "Unknown"

Here we have the _get_preset_name() method, which gives names to the presets as they will be presented to the user, so be sure to use short and clear names.

我们可以在这里使用 match 语句来使代码更加结构化. 这样, 将来很容易添加新的预设. 我们使用catch all模式来返回一些东西. 虽然Godot不会要求超出你定义的预设计数的预设, 但最好是安全起见.

如果你只有一个预设, 则可以直接返回其名称, 但如果你这样做, 则在添加更多预设时必须小心.

func _get_import_options(path, preset_index):
    match preset_index:
        Presets.DEFAULT:
            return [{
                       "name": "use_red_anyway",
                       "default_value": false
                    }]
        _:
            return []

This is the method which defines the available options. _get_import_options() returns an array of dictionaries, and each dictionary contains a few keys that are checked to customize the option as its shown to the user. The following table shows the possible keys:

类型

描述

name

字符串

选项的名称. 显示时, 下划线变为空格, 首字母大写.

default_value

任何类型

此预设的选项的默认值.

property_hint

枚举值

PropertyHint 中的一个值, 作为提示使用.

hint_string

字符串

属性的提示文本. 与你在GDScript中的 export 语句中添加相同.

usage

枚举值

PropertyUsageFlags 中的一个值来定义用途.

namedefault_value 键是 强制 , 其余是可选的.

Note that the _get_import_options method receives the preset number, so you can configure the options for each different preset (especially the default value). In this example we use the match statement, but if you have lots of options and the presets only change the value you may want to create the array of options first and then change it based on the preset.

警告

The _get_import_options method is called even if you don't define presets (by making _get_preset_count return zero). You have to return an array even it's empty, otherwise you can get errors.

func _get_option_visibility(path, option_name, options):
    return true

For the _get_option_visibility() method, we simply return true because all of our options (i.e. the single one we defined) are visible all the time.

如果只有在使用某个值设置了另一个选项时才需要使某个选项可见, 则可以在此方法中添加逻辑.

import 方法

The heavy part of the process, responsible for converting the files into resources, is covered by the _import() method. Our sample code is a bit long, so let's split in a few parts:

func _import(source_file, save_path, options, r_platform_variants, r_gen_files):
    var file = FileAccess.open(source_file, FileAccess.READ)
    if file == null:
        return FileAccess.get_open_error()

    var line = file.get_line()

The first part of our import method opens and reads the source file. We use the FileAccess class to do that, passing the source_file parameter which is provided by the editor.

如果打开文件时出错, 我们将其返回以让编辑器知道导入不成功.

var channels = line.split(",")
if channels.size() != 3:
    return ERR_PARSE_ERROR

var color
if options.use_red_anyway:
    color = Color8(255, 0, 0)
else:
    color = Color8(int(channels[0]), int(channels[1]), int(channels[2]))

此代码获取之前读取的文件行, 并将其拆分为以逗号分隔的片段. 如果有多于或少于三个值, 则认为该文件无效并报告错误.

然后它创建一个新的 Color 变量, 并根据输入文件设置其值. 如果启用了 use_red_anyway 选项, 那么它将颜色设置为纯红色.

var material = StandardMaterial3D.new()
material.albedo_color = color

This part makes a new StandardMaterial3D that is the imported resource. We create a new instance of it and then set its albedo color as the value we got before.

return ResourceSaver.save(material, "%s.%s" % [save_path, _get_save_extension()])

This is the last part and quite an important one, because here we save the made resource to the disk. The path of the saved file is generated and informed by the editor via the save_path parameter. Note that this comes without the extension, so we add it using string formatting. For this we call the _get_save_extension method that we defined earlier, so we can be sure that they won't get out of sync.

我们还返回 ResourceSaver.save() 方法的结果, 所以如果这一步有错误, 编辑器会知道.

平台变体和生成的文件

你可能已经注意到我们的插件忽略了 import 方法的两个参数。那些是返回参数(因此它们的名称以 r 开头),这意味着编辑器会在调用你的 import 方法之后读取它们。它们都是可以填充信息的数组。

r_platform_variants 参数用于需要根据目标平台导入不同的资源. 虽然被称为 平台 变体, 但它是基于 feature tags 的存在, 所以即使是同一个平台也可以有多个变体, 这取决于设置.

要导入平台变体, 需要在扩展名之前使用feature标记保存它, 然后将标记推送到 r_platform_variants 数组, 以便编辑可以知道你做了.

例如, 假设我们为移动平台保存一个不同的材质. 我们将需要做如下的事情:

r_platform_variants.push_back("mobile")
return ResourceSaver.save(mobile_material, "%s.%s.%s" % [save_path, "mobile", _get_save_extension()])

r_gen_files 参数用于在导入过程中生成并需要保留的额外文件. 编辑器将查看它以了解依赖关系并确保不会无意中删除额外文件.

这也是一个数组, 应该填充你保存的文件的完整路径. 例如, 让我们为下一个传递创建另一个材质并将其保存在不同的文件中:

var next_pass = StandardMaterial3D.new()
next_pass.albedo_color = color.inverted()
var next_pass_path = "%s.next_pass.%s" % [save_path, _get_save_extension()]

err = ResourceSaver.save(next_pass, next_pass_path)
if err != OK:
    return err
r_gen_files.push_back(next_pass_path)

试试这个插件

这是理论上的, 但是现在导入插件已经完成了, 让我们来测试一下. 确保你创建了示例文件(包含介绍部分中描述的内容)并将其另存为 test.mtxt . 然后在 "项目设置" 中激活插件.

如果一切顺利, 导入插件将添加到编辑器中并扫描文件系统, 使自定义资源显示在FileSystem基座上. 如果选择它并聚焦导入面板, 则可以看到选择该选项的唯一选项.

Create a MeshInstance3D node in the scene, and for its Mesh property set up a new SphereMesh. Unfold the Material section in the Inspector and then drag the file from the FileSystem dock to the material property. The object will update in the viewport with the blue color of the imported material.

../../../_images/import_plugin_trying.png

转到导入面板, 启用 "强制使用红色" 选项, 然后单击 "重新导入". 这将更新导入的材质, 并应该自动更新显示红色的视图.

就是这样! 你的第一个导入插件已经完成! 现在就发挥创造力,为自己心爱的格式制作插件吧。这对于以自定义格式编写数据然后在 Godot 中使用它就像它们是本机资源一样非常有用。这显示了导入系统如何强大和可扩展。