Safely Renaming Exported Variables in Godot

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.