UndoRedo Service
Improve your editor tooling with easier support for undo & redo operations in Godot!
UndoRedo Service is an addon for Godot 4 that allows you to more easily take advantage of the editor's built-in UndoRedo system to save changes to editor game state right as they happen. Everything is managed through the UndoRedoService singleton that becomes active once the addon is installed.
UndoRedoService Features
- Provides safe access to the
EditorUndoRedoManagersingleton that won't crash your game on exported builds - Queue up operations to be executed with a streamlined approach as compared to the built-in
UndoRedoaction flow - Bundle do & undo property changes and method calls
- Merge UndoRedo actions together with
commit_merge_action, allowing you to treat multiple undo/redo actions as one - Safely apply queued operations without causing errors due to other unfinished
UndoRedoactions - Make & merge custom actions in response to native
UndoRedoactions
Examples: Before & After
1. Updating an Object's property
Native UndoRedovar undo_redo_manager := EditorInterface.get_editor_undo_redo()
undo_redo_manager.create_action("Edit Object property")
undo_redo_manager.add_do_property(my_object, &"my_property", new_value)
undo_redo_manager.add_undo_property(my_object, &"my_property", old_value)
undo_redo_manager.commit_action()
With UndoRedoServiceWARNING!
this example uses a direct
EditorUndoRedoManagerreference, but this would actually crash the exported version of the project at runtime if the script is loaded in.The safe way to access this natively is via
Engine.get_singleton(&"EditorInterface").get_editor_undo_redo(), which gets you a basic Object type with no autocompletion capabilites.
UndoRedoService.queue_do_undo_property(my_object, &"my_property", new_value, old_value)
UndoRedoService.commit_action("Edit Object Property")
or
UndoRedoService.queue_do_property(my_object, &"my_property", new_value)
UndoRedoService.queue_undo_property(my_object, &"my_property", old_value)
UndoRedoService.commit_action("Edit Object Property")
2. Calling an Object's method
Native UndoRedovar undo_redo_manager := EditorInterface.get_editor_undo_redo()
undo_redo_manager.create_action("Call Object method")
undo_redo_manager.add_do_method(my_object, &"my_method", new_arg_1, new_arg_2)
undo_redo_manager.add_undo_property(my_object, &"my_method", old_arg_1, old_arg_2)
undo_redo_manager.commit_action()
UndoRedoService.queue_do_undo_method(my_object, &"my_method", [new_arg_1, new_arg_2], [old_arg_1, old_arg_2])
UndoRedoService.commit_action("Call Object method")
or
UndoRedoService.queue_do_method(my_object, &"my_method", new_arg_1, new_arg_2)
UndoRedoService.queue_undo_method(my_object, &"my_method", old_arg_1, old_arg_2)
UndoRedoService.commit_action("Call Object method")
3. Adding & removing nodes
Native UndoRedofunc add_list_item() -> void:
var list_item := Label.new()
var undo_redo_manager := EditorInterface.get_editor_undo_redo()
undo_redo_manager.create_action("Add list item")
undo_redo_manager.add_do_method(list_items_v_box_container, &"add_child", list_item, true)
undo_redo_manager.add_do_method(list_item, &"set_owner", self)
undo_redo_manager.add_do_reference(list_item)
undo_redo_manager.add_undo_method(list_items_v_box_container, &"remove_child", list_item)
undo_redo_manager.commit_action()
func remove_list_item(list_item: Label) -> void:
var undo_redo_manager := EditorInterface.get_editor_undo_redo()
undo_redo_manager.create_action("Remove list item")
undo_redo_manager.add_do_method(list_items_v_box_container, &"remove_child", list_item)
undo_redo_manager.add_undo_method(list_items_v_box_container, &"add_child", list_item)
undo_redo_manager.add_undo_method(list_item, &"set_owner", self)
undo_redo_manager.add_undo_reference(list_item)
undo_redo_manager.commit_action()
func add_list_item() -> void:
var list_item := Label.new()
UndoRedoService.queue_do_method(list_items_v_box_container, &"add_child", list_item, true)
UndoRedoService.queue_do_method(list_item, &"set_owner", self)
UndoRedoService.queue_do_reference(list_item)
UndoRedoService.queue_undo_method(list_items_v_box_container, &"remove_child", list_item)
UndoRedoService.commit_action("Add list item")
func remove_list_item(list_item: Label) -> void:
UndoRedoService.queue_do_method(list_items_v_box_container, &"remove_child", list_item)
UndoRedoService.queue_undo_method(list_items_v_box_container, &"add_child", list_item)
UndoRedoService.queue_undo_method(list_item, &"set_owner", self)
UndoRedoService.queue_undo_reference(list_item)
UndoRedoService.commit_action("Remove list item")
4. Merging a new UndoRedo action into an Editor-created action
Native UndoRedo# Running code in response to an editor-invoked UndoRedo action,
# e.g. in response to an export variable changing in the Inspector.
var undo_redo_manager := EditorInterface.get_editor_undo_redo()
var undo_redo_action_name := "Randomize sprite color"
# Wait for an in-progress action to complete if there is one
if undo_redo_manager.is_committing_action():
# Find the current mid-commit action's name. We need to get the specific UndoRedo
# reference to do this.
var undo_redo := undo_redo_manager.get_history_undo_redo(
undo_redo_manager.get_object_history_id(self)
)
undo_redo_action_name = undo_redo.get_current_action_name()
await undo_redo_manager.history_changed
else:
return # There wasn't a mid-commit action when we expected one, so exit early
var new_color := Color(randf(), randf(), randf())
undo_redo_manager.create_action(undo_redo_action_name, UndoRedo.MERGE_ALL)
undo_redo_manager.add_do_property(example_sprite, &"modulate", new_color)
undo_redo_manager.add_undo_property(example_sprite, &"modulate", example_sprite.modulate)
undo_redo_manager.commit_action()
With UndoRedoServiceWARNING!
This native example doesn't account for a serious design flaw that can cause irrecoverable state errors when undoing/redoing an action with multiple actions merged into it. This comes down to an undo operation order limitation that, at least as of Godot 4.7, must be worked around via a caching method. See our UndoRedo Tooling Demo - Tool V7 for a deeper look into why this fails and how to (mostly) work around it.
# Running code in response to an editor-invoked UndoRedo action,
# e.g. in response to an export variable changing in the Inspector.
if not EditorUndoRedoHelper.is_valid_operation_context(is_undo_redo_reaction):
return
var new_color := Color(randf(), randf(), randf())
EditorUndoRedoHelper.queue_do_undo_property(example_sprite, &"modulate", new_color, example_sprite.modulate)
# `commit_merge_action()` takes care of the `await` step, action name check, and undo operation
# caching for us! We just need to pass in a fallback action name.
EditorUndoRedoHelper.commit_merge_action("Randomize sprite color")
Limitations
This addon is not a remake or redesign of the native UndoRedo system at large; it is more or less a streamlined wrapper over the existing EditorUndoRedoManager singleton, with a built-in undo operation caching mechanism that allows us to skip duplicate undo ops for merged actions when desired (to deal with the design flaw mentioned above).
This tool provides workarounds to facilitate some common UndoRedo needs, but ultimately it is limited to the UndoRedo API exposed from within Godot's internal library, and is unable to fix its core limitations. For example, it is impossible to know ahead of time with 100% certainty whether a newly created action will be able to merge in with a previous one.
The good news is that the native UndoRedo system is not actually that complicated; if you check out the Godot source code (C++) and look at undo_redo.cpp & editor_undo_redo_manager.cpp, you may find it relatively easy to modify these APIs to expose new functions and enable new behaviors! My hope is that over time, the UndoRedo system will improve enough that a tool like this one isn't necessary for more advanced use cases.
Changelog for version v1.0
No changelog provided for this version.