导入插件

备注

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

前言

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

本教程将向您展示如何创建一个简单的导入插件, 以将自定义文本文件作为材质资源加载. 此文本文件将包含由逗号分隔的三个数值, 它表示颜色的三个通道, 并且生成的颜色将用作导入材质的反射(主颜色). 在此示例中, 它将包含纯蓝色(红色0, 绿色0和满蓝色):

  1. 0,0,255

配置

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

  1. [plugin]
  2. name="Silly Material Importer"
  3. description="Imports a 3D Material from an external text file."
  4. author="Yours Truly"
  5. version="1.0"
  6. script="material_import.gd"

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

  1. # material_import.gd
  2. tool
  3. extends EditorPlugin
  4. var import_plugin
  5. func _enter_tree():
  6. import_plugin = preload("import_plugin.gd").new()
  7. add_import_plugin(import_plugin)
  8. func _exit_tree():
  9. remove_import_plugin(import_plugin)
  10. import_plugin = null

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

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

EditorImportPlugin 类

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

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

  1. # import_plugin.gd
  2. tool
  3. extends EditorImportPlugin
  4. func get_importer_name():
  5. return "demos.sillymaterial"

第一个方法是 get_importer_name(). 这是你的插件的唯一名称,Godot用它来知道在某个文件中使用了哪个导入. 当文件需要重新导入时, 编辑器会知道该调用哪个插件.

  1. func get_visible_name():
  2. return "Silly Material"

get_visible_name() 方法负责返回它导入的类型的名称, 它将在导入停靠区显示给用户.

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

  1. func get_recognized_extensions():
  2. return ["mtxt"]

Godot的导入系统通过扩展名检测文件类型. 在 get_recognized_extensions() 方法中, 返回一个字符串数组, 代表这个插件能理解的每个扩展名. 如果一个扩展名被一个以上的插件识别, 用户可以在导入文件时选择使用哪一个.

小技巧

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

  1. func get_save_extension():
  2. return "material"

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

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

  1. func get_resource_type():
  2. return "SpatialMaterial"

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

在我们的示例中, 它是 SpatialMaterial, 可以应用于3D对象.

备注

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

选项和预设

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

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

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

  1. tool
  2. extends EditorImportPlugin
  3. enum Presets { DEFAULT }
  4. ...

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

  1. func get_preset_count():
  2. return Presets.size()

get_preset_count() 方法返回这个插件定义的预置数量. 我们现在只有一个预设, 但我们可以通过返回我们的 Presets 枚举的大小来使该方法适应未来的需求.

  1. func get_preset_name(preset):
  2. match preset:
  3. Presets.DEFAULT:
  4. return "Default"
  5. _:
  6. return "Unknown"

这里我们有 get_preset_name() 方法, 它给出预设的名字, 因为它们将被展示给用户, 所以一定要使用简短而清晰的名字.

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

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

  1. func get_import_options(preset):
  2. match preset:
  3. Presets.DEFAULT:
  4. return [{
  5. "name": "use_red_anyway",
  6. "default_value": false
  7. }]
  8. _:
  9. return []

这是定义可用选项的方法. get_import_options() 返回一个字典数组, 每个字典包含一些可被检查的键, 以便在其显示给用户时定制选项. 下表显示了可能的键:

类型

描述

name

字符串

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

default_value

任何类型

此预设的选项的默认值.

property_hint

枚举值

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

hint_string

字符串

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

usage

枚举值

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

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

请注意, get_import_options 方法接收预设编号, 因此您可以为每个不同的预设(尤其是默认值)配置选项. 在这个示例中, 我们使用 match 语句, 但是如果您有很多选项并且预设只改变了您可能想要首先创建选项数组的值, 然后根据预设更改它.

警告

即使您没有定义预设(通过使 get_preset_count 返回零), 也会调用 get_import_options 方法. 您必须返回一个数组, 即使它是空的, 否则您可能会得到错误.

  1. func get_option_visibility(option, options):
  2. return true

对于 get_option_visibility() 方法, 我们只需返回 true , 因为我们所有的选项(即我们定义的单个选项)始终是可见的.

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

import 方法

负责将文件转换为资源的重要部分由 import() 方法涵盖. 我们的示例代码有点长, 所以让我们分成几个部分:

  1. func import(source_file, save_path, options, r_platform_variants, r_gen_files):
  2. var file = File.new()
  3. var err = file.open(source_file, File.READ)
  4. if err != OK:
  5. return err
  6. var line = file.get_line()
  7. file.close()

我们导入方法的第一部分是打开并读取源文件. 使用 File 类来做到这一点, 传递 source_file 参数, 该参数由编辑器提供.

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

  1. var channels = line.split(",")
  2. if channels.size() != 3:
  3. return ERR_PARSE_ERROR
  4. var color
  5. if options.use_red_anyway:
  6. color = Color8(255, 0, 0)
  7. else:
  8. color = Color8(int(channels[0]), int(channels[1]), int(channels[2]))

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

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

  1. var material = SpatialMaterial.new()
  2. material.albedo_color = color

这一部分制作了一个新的 SpatialMaterial , 这是导入的资源. 我们创建一个新的实例, 然后将其反射颜色设置为我们之前得到的值.

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

这是最后一个部分, 也是相当重要的部分, 因为在这里我们把制作好的资源保存到磁盘上. 保存文件的路径由编辑器通过 save_path 参数生成并告知. 注意, 这个参数 没有 扩展名, 所以我们用 string formatting 添加它. 为此, 我们调用前面定义的 get_save_extension 方法, 这样我们就可以确保它们不会丢失同步.

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

平台变体和生成的文件

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

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

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

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

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

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

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

  1. var next_pass = SpatialMaterial.new()
  2. next_pass.albedo_color = color.inverted()
  3. var next_pass_path = "%s.next_pass.%s" % [save_path, get_save_extension()]
  4. err = ResourceSaver.save(next_pass_path, next_pass)
  5. if err != OK:
  6. return err
  7. r_gen_files.push_back(next_pass_path)

试试这个插件

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

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

在场景中创建一个 MeshInstance 节点,为其 Mesh 属性设置一个新的 SphereMesh。在“检查器”中展开 Material 部分,然后将文件从“文件系统”面板拖动到材质属性。对象将在视区中使用导入材质的蓝色进行更新。

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

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

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