
Safely Renaming Exported Variables in Godot
Naming things is one of the two hard problems in Computer Science. When it comes to exported variables in Godot, it’s even harder because once I name a variable, I can’t easily rename it.
Why?
Let’s say I create this resource script, which specifies how many hitpoints an enemy has:
class_name EnemyData
extends Resource
@export var hp: int
Then, I create a bunch of enemy resources, each with a different number of hitpoints. Later, when I remember that using abbreviations for variable names isn’t a good practice, I decide to rename hp
to hitpoints
.
Now I have a problem. If I rename it directly, I will have to manually fix the number of hitpoints for each enemy resource because Godot will forget them.
Let’s consider my options:
- Rewrite the project in Unity and use its
FormerNameAttribute
. - Keep the name as is.
- Manually fix all instances.
- Hack together a script to fix the values.
I’ve tried writing a project in Unity before, and it didn’t end well. I’m also tired of manually fixing everything, and sometimes, I just want to use the new, better name. So I decided to do some hacking.
How hard can it be?
It should fall into the “monthly five-minute” category, so if I finish it in under five hours, it’ll be worth the time.
The Challenges
I need to load each resource file and assign the old value to the new variable.
However, I can’t assign the value in the _init
method because it’s not called when I load a resource file. So, I have to use a custom method.
Additionally:
- Resources can be embedded within other resources—either as variables, arrays, or even dictionaries—so, I have to update them recursively.
- Resources can also be embedded in scenes, meaning I have to process not only resource files (
*.tres
) but also scene files (*.tscn
). - To run a method on a resource embedded in a scene, the resource script must be a tool script (it needs the
@tool
annotation). - Note to self: Don’t forget to reload the project after changing a regular script to a tool script.
The Solution
I’m pretty sure there are some edge cases I haven’t accounted for, but so far, this approach works well for me.
Let’s take the example from the beginning and rename hp
to hitpoints
.
Step 1: Modify the Script
First, I need to:
- Add the
@tool
annotation. - Define the new
hitpoints
variable. - Add a
__migrate__
method to transfer the old value to the new variable.
Notice that the old hp
variable is still present:
@tool
class_name EnemyData
extends Resource
@export var hp: int
@export var hitpoints: int
func __migrate__():
hitpoints = hp
Step 2: Run the Migration Script
Next, I run the following editor script (Ctrl + Shift + X
). This script:
- Recursively walks through all resources and scenes.
- Calls the
__migrate__
method if it exists. - Saves the resource again.
@tool
extends EditorScript
const MIGRATE_METHOD_NAME := &"__migrate__"
func _run() -> void:
print("Migrating resources")
var resource_files := get_all_file_paths("res://", [".tres", ".tscn"])
for resource_file: String in resource_files:
var resource := ResourceLoader.load(resource_file)
migrate_resource(resource)
ResourceSaver.save(resource, resource_file)
print("Done")
func migrate_resource(variant: Variant) -> void:
if variant is not Resource:
return
var resource := variant as Resource
if resource.has_method(MIGRATE_METHOD_NAME):
prints("Migrating", resource.get_script().get_global_name())
resource.call(MIGRATE_METHOD_NAME)
for prop in resource.get_property_list():
var type := prop[&"type"] as int
var name := prop[&"name"] as StringName
match type:
Variant.Type.TYPE_OBJECT:
migrate_resource(resource[name])
Variant.Type.TYPE_ARRAY:
migrate_array(resource[name])
Variant.Type.TYPE_DICTIONARY:
migrate_dictionary(resource[name])
func migrate_array(array: Array) -> void:
for item in array:
if item is Resource:
migrate_resource(item)
func migrate_dictionary(dictionary: Dictionary) -> void:
for key in dictionary.keys():
var item = dictionary[key]
if item is Array:
migrate_array(item)
elif item is Dictionary:
migrate_dictionary(item)
else:
migrate_resource(item)
func get_all_file_paths(path: String, extensions: Array[String]) -> Array[String]:
var file_paths: Array[String] = []
var directory := DirAccess.open(path)
directory.list_dir_begin()
var file_name := directory.get_next()
while file_name != "":
var file_path := path + file_name if path.ends_with("/") else path + "/" + file_name
if directory.current_is_dir():
file_paths.append_array(get_all_file_paths(file_path, extensions))
else:
for extension in extensions:
if file_name.ends_with(extension):
file_paths.append(file_path)
file_name = directory.get_next()
return file_paths
Step 3: Clean Up
Once the migration is complete, I:
- Remove the old
hp
variable. - Remove the
__migrate__
method. - Remove the
@tool
annotation.
Final Thoughts
I like that the manual overhead of this approach is reasonably low and that there’s no leftover boilerplate code in the resource script once the migration is finished. However, this method probably won’t scale well for thousands of resources.
The script might evolve as I encounter new caveats, so I’ve put it on GitHub.