Conditionally Export Properties to the Inspector in Godot
November 20, 2020

Suppose you are writing a plugin with several optional settings that can be changed through the Inspector (properties with the export keyword). Very often in this scenario there will be some settings thar relevant only if some other feature is enabled or disabled.

Unfortunately we can’t exactly “tell Godot” that we want to hide certain properties if a certain condition is true (or false). Yet, we can still expose properties to the Inspector based on conditions, only that we require a bit more work to do so.

There is just one caveat here. The properties that we will expose based on conditions will not have that nice arrow to revert the property to the default value. The one like in the image bellow:

Revert Property to Default Value

Nevertheless, the end result should be something similar to this:

Tutorial Result

Because this tutorial is relatively small and simple I don’t think it’s necessary to provide any kind of reference material on Github.

How

Before delving into a practical example, I want to talk about how we can expose properties based on certain conditions. The core of the functionality relies in overriding the _get_property_list() function, which is defined within the Object class. Since most (if not all) things that we do through scripts are in a way derived from that class, we will have this function available.

Anyway, this mentioned function is internally called by Godot and it should return an array containing a list of properties that we want to be displayed within the Inspector. One really nice thing here is that this list does not conflict with any variable that we do use the export keyword, meaning that we can mix those in the script. Just remember that there is no need to include exported variables within the array.

The returned array must be of dictionaries, each one representing a property. A property must be represented like this:

var property: Dictionary = {
	"name": "property_name",       # Required
	"type": TYPE_*,                # Required
	"hint": PROPERTY_HINT_*,       # Optional
	"hint_string": "some_hint",    # Optional
}

The name not only tells what will be shown within the Inspector tab, but also how the property will be identified during serialization.

The type is an integer indicating the property type. Any value from the Variant.Type enum can be used. The list can be found in the documentation. Also, if in doubt it’s possible to use typeof(some_type).

The hint is used to sort of tell Godot which widget we want to be created in order to edit the given property. It can be any value from the PropertyHint enum. A full list can be found in the already linked documentation page.

Finally the hint_string somewhat indicates how the widget will behave. The actual string depends a lot on the hint value. Generally speaking, checking the PropertyHint enum in the reference should give you some tips on what the string should be.

After we override said function and return an array with properties, those will indeed populate the Inspector tab. However, depending on how we setup things, if we try to change any of them nothing will happen. Indeed, we only have the names that will be shown within the editor and that will be used during (de)serialization. We still have to tell what value those are and how to set them. For that we have to override _get() and _set() functions. Godot will call those functions for each property within the array obtained from the _get_property_list() function.

The _get() function receives a string as argument, which is the name of the property. We must return the value of the given property. This is the value that will be serialized and shown within the editor (Inspector tab).

The _set() function receives two arguments. First the string with the name of the property. Then a variant containing the value that must be set. In here we have to return true if the property exists.

Now note something interesting. The _set() and _get() function can be used sort of to “bridge” the serialized/UI with the actual variables within our script. In other words, we can have internal variables with names that are completely different to those that we expose to the UI. In the practical example we will see exactly this!

Lastly, we want to modify the list whenever we change a condition related to the other properties. For that we have to call the property_list_changed_notify() function, which basically “tells” Godot something like this “look, the list of properties for this object has changed, could you please update it?”.

Two important things to note here:

  1. Properties that are part of the array will always come after any other property that is marked with export. This means that if we mix export properties with array list, we don’t have much control over the order in which they will appear.
  2. The script has to be marked as a tool script (just add tool keyword above the extends ...). Just remember to be careful with the code that is part of the _process() and/or _physics_process() because those will run while in the editor.

Incredibly, this is the entirety of what we need to know in order to conditionally expose properties to the Inspector tab!

In Practice

Now, for a practical example. Because what I will show here is somewhat silly, if you want to follow things I would recommend creating an empty project to test things out. That said, we only need the main scene with a script (empty one) attached to it. The base node type is irrelevant. We will not even run the project as what we want to see is basically editor only stuff.

Remember to add the tool keyword above the extends .... Then declare two boolean properties, which we will use to provide the conditions to show/hide the other properties:

# Provides condition for one set of properties
var some_feature1: bool = false

# Provides condition for another set of properties
var some_feature2: bool = true

The way those are declared will not put them within the Inspector tab. This is on purpose because we can somewhat group the “linked” properties by placing them in the correct order within the array that we will provide to the Godot editor.

That said, lets work on the _get_property_list() function. For the moment we will just expose two boolean properties that will be linked to those two variables. We will name the exposed properties as Awesome Feature 1 and Awesome Feature 2 (yay to the creativity here). As mentioned, both properties will be boolean, so we set type as TYPE_BOOL:

func _get_property_list() -> Array:
	var ret: Array = []
	
	ret.append({
		"name": "Awesome Feature 1",
		"type": TYPE_BOOL,
	})
	
	ret.append({
		"name": "Awesome Feature 2",
		"type": TYPE_BOOL,
	})
	
	return ret

Selecting the root node of the script should update the Inspector, which must then show something similar to this:

Exposed Properties 1

We have to override the _set() and _get() functions in order to allow interaction with the UI to actually update the variables we have in our script. So, we create them. The comments within the snippet bellow should help understand what is going on.

# The value argument must be a variant, which we can't explicitly tell through static typing.
# This function must return true if the property actually exists.
func _set(prop_name: String, val) -> bool:
	# Assume the property exists
	var retval: bool = true
	
	match prop_name:
		"Awesome Feature 1":
			some_feature1 = val
		
		"Awesome Feature 2":
			some_feature2 = val
		
		_:
			# If here, trying to set a property we are not manually dealing with.
			retval = false
	
	return retval

# This function must return a value, which is basically the one related to the property name.
# However it is a variant, which we can't define explicitly through static typing.
func _get(prop_name: String):
	var retval = null
	
	match prop_name:
		"Awesome Feature 1":
			return some_feature1
		
		"Awesome Feature 2":
			return some_feature2
	
	return retval

We still don’t have anything that is based on any condition. We are only exposing two variables that are shown within the Inspector with different names! Let’s add the conditional properties.

For the Awesome Feature 1, if it’s enabled we will just show a Texture property, otherwise nothing will be shown bellow it.

As for the Awesome Feature 2, if it’s enabled we will show a float property and if it’s disabled we will show an integer property.

The first thing we have to do is declare all of the extra properties that will internally hold the values:

# This will be shown only if some_feature1 is true
var some_texture: Texture = null

# This will be shown only if some_feature2 is true
var some_float: float = 0.0

# This will be shown only if some_feature2 is false
var some_int: int = 0

The next thing is the property list itself. We basically populate it based on the determined conditions:

func _get_property_list() -> Array:
	var ret: Array = []
	
	ret.append({
		"name": "Awesome Feature 1",
		"type": TYPE_BOOL,
	})
	
	# Conditionally show the texture property
	if (some_feature1):
		ret.append({
			"name": "Some Texture",
			"type": typeof(Resource),               # Texture is a Resource
			"hint": PROPERTY_HINT_RESOURCE_TYPE,    # We tell we want to edit a Resource
			"hint_string": "Texture",               # And explicitly a Texture
		})
	
	ret.append({
		"name": "Awesome Feature 2",
		"type": TYPE_BOOL,
	})
	
	# Conditionally show either float or integer
	if (some_feature2):
		ret.append({
			"name": "Some Float",
			"type": TYPE_REAL,
		})
	
	else:
		ret.append({
			"name": "Some Int",
			"type": TYPE_INT,
		})
	
	return ret

We have to update the _set() and _get() functions not only to allow us to interact and change the conditional properties. We also have to notify Godot that it must update the property list.

That said, within the match statement we have to call property_list_changed_notify(). But we will only do so whenever some_feature1 or some_feature2 is changed. Besides that we have to also incorporate the new three variables within the match of both _set() and _get():

func _set(prop_name: String, val) -> bool:
	# Assume the property exists
	var retval: bool = true
	
	match prop_name:
		"Awesome Feature 1":
			some_feature1 = val
			property_list_changed_notify()
		
		"Some Texture":
			some_texture = val
		
		"Awesome Feature 2":
			some_feature2 = val
			property_list_changed_notify()
		
		"Some Float":
			some_float = val
		
		"Some Int":
			some_int = val
		
		_:
			# If here, trying to set a property we are not manually dealing with.
			retval = false
	
	return retval


func _get(prop_name: String):
	var retval = null
	
	match prop_name:
		"Awesome Feature 1":
			return some_feature1
		
		"Some Texture":
			return some_texture
		
		"Awesome Feature 2":
			return some_feature2
		
		"Some Float":
			return some_float
		
		"Some Int":
			return some_int
	
	return retval

And that’s it! Changing the boolean properties should update the exposed values within the Inspector tab.

I would like to talk about those match statements. On very simple cases like the one shown in this tutorial it’s OK to have them. However, if at some point we have to expose lots of properties, maintaining those can become a nightmare.

One alternative method here is that we can create a Dictionary that will hold the values of the exposed variables, having as key the name of the property that we have assigned within the returned array. That way, we could have something similar to this:

func _set(prop_name: String, val) -> bool:
	exposed_properties[prop_name] = val
	return true

func _get(prop_name: String):
	return exposed_properties[prop_name]

Obviously that we have to incorporate proper checking but I believe this should be enough to provide the idea, right?

Finally, there is one last thing I would like to mention. We can categorize the exposed properties, which will be shown something like this:

Categorized

In order to do that, we have to prefix the name field of the exposed property, something like this:

ret.append({
   "name": "SomeCategory/categorized",
   "type": TYPE_INT
})

Obviously that in the match statements we also have to prefix the property name:

match prop_name:
	"SomeCategory/categorized":
		...

And that’s it! We are now conditionally exposing properties to the Inspector tab within the Godot Engine Editor!

Happy forging!