-->
In GDScript we can export properties. Or, in other words, expose them. With this we can edit their values through specialized editors within the Inspector panel.
Depending on the needs we can create a script derived (extends) from Resource
meant to hold data, making those editable by exposing the relevant properties. In fact I have done something way more elaborate to hold databases, including some relational data (that is, tables that can reference other tables). This can be found in the Database Addon. Yet, sometimes all we want is an asset holding some data. No need for database tables or the likes.
As an example of this "simplified data", in a project I wanted the possibility to change the "skin" of the shown objects, which I called "theme" in the game itself. In order to "describe" each theme I created a resource script containing the required properties, like theme name, list of textures and so on. Then, for each theme, I had to create an instance of this resource.
When attempting to edit properties like that, there is a big limitation: We can't export custom resource types. Besides that, array editing is rather clunky. To solve that an editor plugin is required. And for this I bring the DataAsset plugin. Bellow an array of custom scripted resources is being rearranged:
Array Example
To start things it's first necessary to have a script derived from DatAsset
. This base class is provided with the addon. In this tutorial we will work on a sample_dasset.gd. One important aspect that must be taken in consideration is the fact that the script must be marked as a tool
one otherwise the plugin will fail to obtain certain required data:
tool
extends DataAsset
class_name SampleDataAsset
Lets add a few basic exported properties just so we get something to work with in the editor:
export var some_string: String = ""
export var some_float: float = 1.0
export(int, FLAGS, "physical", "poison", "fire", "cold", "lightning") var some_flags: int = 0
export var some_resource: Resource = null
export var some_texture: Texture = null
export var some_array: Array = []
Next we need a file resource, of the SampleDataAsset
type. To do this, right click the appropriate directory within the FileSystem panel then New Resource.... In the window that pops up, select SampleDataAsset
:
New Sample Data Asset
As soon as the new asset is created it should already open the new editor:
Asset Editor 1
As you can see, this asset editor is docked at the bottom part of the window, sharing space with the Output. If you scroll and/or expand the (DataAsset) editor panel you will probably notice a few things related to this specific script:
some_float
property? The usual export way of working with ranges in numerical types will not work with this plugin.some_resource
property will popup a huge list of possible resources that can be added here.some_array
property, telling that we must implement the get_property_info()
function.The base DataAsset
class contains a get_property_info()
function that will be called by the plugin when populating the window with the property editors. In our derived data asset we implement this function in order to provide the plugin with the information that it needs to properly fill the editor.
The mentioned function must return a Dictionary
. Each key in it should be the name of the corresponding property. The value associated with that key must be another Dictionary
, containing the relevant fields. I will exemplify the 3 aspects that I have highlighted. After that I will provide tables with the complete list of options that can be used, by property type.
So, lets begin with the some_float
. First lets define that we don't want values to be negative but we can set to any other positive value. For that we tell the plugin to use the range_min
option and set it to be 0.0
:
func get_property_info() -> Dictionary:
return {
"some_float": {
"range_min": 0.0,
},
}
Once the script is updated, reload the asset (by double clicking its file within the FileSystem panel). Now attempting to assign a value smaller than 0 will not work. At this point the value editor still shows the "spin buttons" to increase/decrease the value. If we also set a range_max
then those buttons will be removed and a slider will be shown, because then we obtain a well defined range. So, lets do it and set 1.0 to be the maximum value for this property:
func get_property_info() -> Dictionary:
return {
"some_float": {
"range_min": 0.0,
"range_max": 1.0,
},
}
And so, this specific property editor should now display a slider rather than the spin buttons:
Float Slider
Lets now move to the some_resource
property. In order to filter out the list of possible New instance of... options we have to specify the base type of the desired resource. This is done by providing a type
option within the property's dictionary. In it we must provide the path to the script that implements the resource. So, lets create a simple sample resource type to be used. Note that a class_name must be provided otherwise the plugin will fail to gather the required information. That said, a sample_resource:
extends Resource
class_name SampleScriptedResource
export var some_color: Color = Color(1, 1, 1, 1)
export var some_vec3: Vector3 = Vector3(0, 1, 0)
export(String, FILE, "*.png, *.jpg") var some_image_file: String = ""
Now, back to the sample_dasset.gd, we specify that we want the some_resource
property to be of the type defined in the sample_resource.gd file:
func get_property_info() -> Dictionary:
return {
"some_float": {
"range_min": 0.0,
"range_max": 1.0,
},
"some_resource": {
"type": "res://shared/scripts/sample_resource.gd",
},
}
Once the script is saved and the resource reloaded, the New instance of... popup menu should be filtered to show onlySampleScriptedResource
and any other that extends it:
New Scripted Resource Instance
Once the new instance is created, it should look like this:
Scripted Resource Editor
Finally we need to specify the type of entries that we would like to be added into the some_array
property. Again we use the type
option. But this time we can specify:
type
most of the TYPE_*
constants.To continue with the example we will simply specify the same sample_resource.gd mostly to see how things will look:
func get_property_info() -> Dictionary:
return {
"some_float": {
"range_min": 0.0,
"range_max": 1.0,
},
"some_resource": {
"type": "res://shared/scripts/sample_resource.gd",
},
"some_array": {
"type": "res://shared/scripts/sample_resource.gd",
},
}
Reloading the resource now should change to the proper array editor. There will be a big "Append" button. Clicking it will popup a list of resource types that are derived from the specified type, including the base one. Note that you can even append null
into the array when the desired type is a Resource
(which is the case for the example):
Append to Array
After appending a few instances of the SampleScriptedResource
and editing the some_color
property of each instance, the array editor looks something like the animation bellow. In it I'm also showcasing the fact that we can re-arrange array elements by simply dragging them:
Array Example
OK, that's it for the example! The tables bellow lists all options that can be used in the return value of the get_property_info()
function. Each table is for a given property type. Note that the default value will be used in case the option is not provided. And if there is no "default" listed then that "option" is actually required.
Name | Default | Description |
---|---|---|
label | "On" | A string determining the label that will be set within the checkbox. |
Name | Default | Description |
---|---|---|
range_min | 0 | Determines the minimum value that can be assigned to the property. |
range_max | 100 | Determines the maximum value that can be assigned to the property. |
step | 1 | The "delta" applied to the value when clicking the spin buttons or moving the slider. |
Name | Default | Description |
---|---|---|
range_min | 0.0 | Determines the minimum value that can be assigned to the property. |
range_max | 1.0 | Determines the maximum value that can be assigned to the property. |
step | 0.1 | The "delta" applied to the value when clicking the spin buttons or moving the slider. |
Name | Default | Description |
---|---|---|
type | - | The full path to a script defining a resource type. |
allow_base | true | If this is false then the popup menu with instance options will not display the specified base class, only derived ones. |
Name | Default | Description |
---|---|---|
type | - | Either an integer as a subset of TYPE_* , a string with the name of a core resource or the full path to the script defining a custom resource type. |
Before wrapping up this "tutorial" there are some minor things that I want to mention:
Texture
properties.DataAsset
is indeed a script, you can pretty much add any other desired code into it.The core of this plugin is a near empty class, the DataAsset
, which is implemented in the dasset.gd file. The only contents of this class is a function that returns an empty Dictionary
. The intention here is that classes are created and derived from DataAsset
. In this regard the plugin could have been designed to work with pretty much any scripted resource rather than only those that are derived from this base class.
The decision to make the plugin support only this class at its "root" is much more of consistency rather than anything else. Double clicking any non data asset resource will result in the default core behavior, that is, only displaying the exposed properties within the Inspector panel. And so, this plugin's editor is directly tied to the DataAsset
class. Usage then becomes more consistent.
As for the UI itself, everything is controlled at the edt_main.tscn scene and its attached script. This scene is instanced through the EditorPlugin
that is implemented in the daloader.gd script. This latter script is basically boilerplate plugin code so I wont explain it. Details on this can be found in the documentation.
Regarding the edt_main.gd, there are a few things to mention. The first thing is the _typeinfo
dictionary. In it each key is the integer "code" of a type, as a TYPE_*
. Associated with each of those keys is the appropriate property editor for that variable type. Well, the script for that editor, that is. The actual ideal location for this dictionary is within the resource editor, which is implemented in the ped_resource.gd script. The reason it's in the edt_main.gd instead is that it resulted in resource leak warning messages upon engine exit.
This plugin is meant to edit properties of resources of DataAsset
type. To that end, the edt_main.gd holds an instance (_rooteditor
) of the property editor created specifically for resources. It's implemented in the ped_resource.gd script. When this plugin is requested to edit a resource, it relays the incoming object into the _rooteditor
, which takes care of filling the UI with the appropriate property editors.
Perhaps the most interesting aspect of the resource property editor is in how it fills the UI. Whenever a resource is assigned into the editor, it will first check if it's of the expected type. This is necessary to allow drag & drop operations to occur. This verification is done in the _is_resource_allowed()
function. Within this verification the internal _iterate_properties()
is called. One of the arguments of this latter function is an array that must contain the list of properties of the resource type.
The property iteration function is called from the type verification because the way to obtain the list of properties differs when dealing with a core resource versus a scripted type. When dealing with a core resource we can use ClassDB.class_get_property_list(name_of_the_class)
. However scripted resources are not added into the class DB. Luckily the Script
class provides the get_script_property_list()
function, which returns an array with entries of the same format of the ClassDB
's function.
Each property generates a Dictionary
, that contains at the very least type
, name
and usage
. So, during iteration, the algorithm checks if the usage
entry contains the PROPERTY_USAGE_EDITOR
flag, which indicates that the property in question is meant to be exposed into the Inspector panel. In that case the plugin then verifies if the type is supported. And in case it is, it takes the appropriate editor from the incoming _typeinfo
dictionary (which was mentioned early).
Now notice the fact that the _iterate_properties()
contains one argument named dummy
. While filling the UI with the property editors, a dummy instance of the resource is created. The reason for this is to obtain the default values of the properties. The thing is, there isn't easy access (if at all) to this information from scripting. With this little hack it becomes possible to create the "revert" button for each property.
There is another detail within the property iteration function that is worth mentioning. There are a few properties that are irrelevant to this plugin. Those property names are placed in a Dictionary
named _IgnoreProperty
. The values of the entries are completely irrelevant because this is used as a Set rather than a map. So, during property iteration, if it's meant to be exposed, the very first verification is to check if the name exists within the _IgnoreProperty
"set". If so, just skip to the next property.
As any other property editor, the class implemented in ped_resource.gd extends ped_base.gd. This base class is meant primarily to provide the UI layout common to all property editors. This layout is a fixed width containing a label with the name of the property. After the label, a button, which should be shown only when the value of that property is not equal to the default. Finally, using all remaining width, an area meant to hold specific controls, which change for each property type. As an example, numerical properties are edited by the SpinSlider
. Then string properties are edited by a TextEdit
, which allows multiline input.
One last thing worth mentioning here is the class in ped_composite.gd. It serves as base to any property that contains multiple numerical components, like Vector2
, Vector3
and so on. This class provides means for derived ones to easily create components with any name. As an example, Color
properties have the components r, g, b and a, while a Vector3
property uses x, y and z. This can be seen within the create_component()
function.
That's it for this addon!