Import plugins

Introducción

An import plugin is a special type of editor tool that allows custom resources to be imported by Godot and be treated as first-class resources. The editor itself comes bundled with a lot of import plugins to handle the common resources like PNG images, Collada and glTF models, Ogg Vorbis sounds, and many more.

This tutorial will show you how to create a simple 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.

Nota

This tutorial assumes you already know how to make generic plugins. If in doubt, refer to the Creando plugins page. This also assumes you are acquainted with Godot’s import system.

The sample file to import contains only a line representing the pure blue color (zero red, zero green, and full blue):

0,0,255

Configuration

First we need a generic plugin that will handle the initialization and destruction of our import plugin. Let’s add the plugin.cfg file first:

[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"

Then we need the material_import.gd file to add and remove the import plugin when needed:

# 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

When this plugin is activated, it will create a new instance of the import plugin (which we’ll soon make) and add it to the editor using the add_import_plugin() method. We store a reference to it in a class member import_plugin so we can refer to it later when removing it. The remove_import_plugin() method is called when the plugin is deactivated to clean up the memory and let the editor know the import plugin isn’t available anymore.

Note that the import plugin is a reference type so it doesn’t need to be explicitly released from the memory with the free() function. It will be released automatically by the engine when it goes out of scope.

The EditorImportPlugin class

The main character of the show is the EditorImportPlugin class. It is responsible to implement the methods that are called by Godot when it needs to know how to deal with files.

Let’s begin to code our plugin, one method at time:

# 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 to 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"

El método get_visible_name() es responsable de informar el nombre del tipo que importa y se mostrará al usuario en el panel de Importación.

You should choose this name as a continuation to «Import as». Eg. «Import as Silly Material». Yes, this one is a bit silly, but you certainly can come up with a descriptive name for your plugin.

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.

Truco

Common extensions like .json and .txt might be used by many plugins. Also, there could be files in the project that are just data for the game and should not be imported. You have to be careful when importing to validate the data. Never expect the file to be well-formed.

func get_save_extension():
    return "material"

The imported files are saved in the .import folder at the project’s root. Their extension should match the type of resource you are importing, but since Godot can’t tell what you’ll use (because there might be multiple valid extensions for the same resource), you need to inform what will be the used in the import.

Since we’re importing a Material, we’ll use the special extension for such resource types. If you are importing a scene, you can use scn. Generic resources can use the res extension. However, this is not enforced in any way by the engine.

func get_resource_type():
    return "SpatialMaterial"

El recurso importado tiene un tipo específico, por lo que el editor puede saber a qué ranura de propiedades pertenece. Esto permite arrastrar y soltar desde el panel de Sistema de Archivos a una propiedad en el Inspector.

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

Nota

If you need to import different types from the same extension, you have to create multiple import plugins. You can abstract the import code on another file to avoid duplication in this regard.

Options and presets

Your plugin can provide different options to allow the user to control how the resource will be imported. If a set of selected options is common, you can also create different presets to make it easier for the user. The following image shows how the options will appear in the editor:

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

Since there might be many presets and they are identified with a number, it’s a good practice to use an enum so you can refer to them using names.

tool
extends EditorImportPlugin

enum Presets { PRESET_DEFAULT }

...

Now that the enum is defined, let’s keep looking at the methods of an import plugin:

func get_preset_count():
    return Presets.size()

El método get_preset_count() devuelve la cantidad de presets que definen estos plugins. Sólo tenemos un preset ahora, pero podemos hacer que este método esté preparado para el futuro devolviendo el tamaño de nuestra enumeración Presets.

func get_preset_name(preset):
    match preset:
        PRESET_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.

Aquí podemos usar la sentencia match para hacer el código más estructurado. De esta manera es más fácil añadir nuevos presets en el futuro. Usamos el patrón de catch all para devolver algo también. Aunque Godot no pedirá presets más allá del número de presets que definiste, siempre es mejor ser precavido.

Si sólo tiene un preset puede simplemente devolver su nombre directamente, pero si lo hace debe tener cuidado al añadir más presets.

func get_import_options(preset):
    match preset:
        PRESET_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:

Clave Tipo Descripción
name Cadena The name of the option. When showed, underscores become spaces and first letters are capitalized.
default_value Any El valor por defecto de la opción para este preset.
property_hint Enum value One of the PropertyHint values to use as hint.
hint_string Cadena The hint text of the property. The same as you’d add in the export statement in GDScript.
usage Enum value One of the PropertyUsageFlags values to define the usage.

The name and default_value keys are mandatory, the rest are optional.

Tenga en cuenta que el método get_import_options recibe el número de preset, por lo que puedes configurar las opciones para cada preset diferente (especialmente el valor por defecto). En este ejemplo usamos la sentencia match pero si tienes muchas opciones y los presets sólo cambian el valor, es posible que primero quieras crear un array de opciones y luego cambiarlo basado en el preset.

Advertencia

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(option, 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.

If you need to make certain option visible only if another is set with a certain value, you can add the logic in this method.

The import method

The heavy part of the process, responsible for the 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 = File.new()
    var err = file.open(source_file, File.READ)
    if err != OK:
        return err

    var line = file.get_line()

    file.close()

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

If there’s an error when opening the file, we return it to let the editor know that the import wasn’t successful.

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]))

This code takes the line of the file it read before and splits it in pieces that are separated by a comma. If there are more or less than the three values, it considers the file invalid and reports an error.

Then it creates a new Color variable and sets its values according to the input file. If the use_red_anyway option is enabled, then it sets the color as a pure red instead.

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

This part makes a new SpatialMaterial 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("%s.%s" % [save_path, get_save_extension()], material)

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.

También devolvemos el resultado del método ResourceSaver.save(), así que si hay un error en este paso, el editor lo sabrá.

Platform variants and generated files

You may have noticed that our plugin ignored two arguments of the import method. Those are return arguments (hence the r at the beginning of their name), which means that the editor will read from them after calling your import method. Both of them are arrays that you can fill with information.

The r_platform_variants argument is used if you need to import the resource differently depending on the target platform. While it’s called platform variants, it is based on the presence of feature tags, so even the same platform can have multiple variants depending on the setup.

To import a platform variant, you need to save it with the feature tag before the extension, and then push the tag to the r_platform_variants array so the editor can know that you did.

For an example, let’s say we save a different material for mobile platform. We would need to do something like the following:

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

The r_gen_files argument is meant for extra files that are generated during your import process and need to be kept. The editor will look at it to understand the dependencies and make sure the extra file is not inadvertently deleted.

This is also an array and should be filled with full paths of the files you save. As an example, let’s create another material for the next pass and save it in a different file:

var next_pass = SpatialMaterial.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_path, next_pass)
if err != OK:
    return err
r_gen_files.push_back(next_pass_path)

Trying the plugin

This has been theoretical, but now that the import plugin is done, let’s test it. Make sure you created the sample file (with the contents described in the introduction section) and save it as test.mtxt. Then activate the plugin in the Project Settings.

Si todo va bien, se agrega el plugin de importación al editor y se escanea el sistema de archivos, haciendo que el recurso personalizado aparezca en el panel de Sistema de Archivos. Si lo seleccionas y entras en el panel de Importación, podrás ver la única opción para seleccionar allí.

Crea un nodo MeshInstance en la escena y para su propiedad Mesh configura un nuevo SphereMesh. Despliega la sección Material en el Inspector y luego arrastra el archivo desde el panel de Sistema de Archivos hasta la propiedad material. El objeto se actualizará en la ventana de visualización con el color azul del material importado.

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

Ve al panel Importar, activa la opción «Usar rojo de todos modos» y haz clic en «Reimportar». Esto actualizará el material importado y debería actualizar automáticamente la vista mostrando el color rojo en su lugar.

And that’s it! Your first import plugin is done! Now get creative and make plugins for your own beloved formats. This can be quite useful to write your data in a custom format and then use it in Godot as if they were native resources. This shows how the import system is powerful and extendable.