☰
Godot 4 UndoRedo 完全指南:为节点与编辑器插件实现可靠的撤销/重做系统
2026/10/14 17:17:00 网站建设 项目流程
  • 文档
  • 教程
  • 游戏开发

【免费下载链接】godot-docs

Godot Engine official documentation

项目地址:https://gitcode.com/GitHub_Trending/go/godot-docs
点击查看免费下载

导读

UndoRedo 是 Godot 引擎内置的高层撤销/重做基础设施:它以"动作(action)"为单位注册方法调用与属性变更,提交后即可通过undo()/redo()统一回滚或重放。本文以官方类参考 classes/class_undoredo.rst 为主线,覆盖从基本用法、操作注册顺序、对象生命周期管理(add_do_reference/add_undo_reference)、动作合并(MergeMode)到版本号跟踪的完整 API,并进一步结合仓库内 EditorUndoRedoManager 文档 讲解如何在编辑器中与全局撤销历史集成。读完本文,你将能在自己的游戏工具链或 EditorPlugin 中编写可撤销、可合并、可追踪版本的健壮操作流程。

核心机制:以"动作"为单位记录 do / undo 操作

UndoRedo 的工作方式是在"动作"内部注册方法调用和属性变更。你可以创建一个动作,然后用函数调用与属性变更分别描述"如何执行(do)"与"如何撤销(undo)",最后提交该动作。动作提交时,所有do_*方法会立即执行;调用undo()时执行undo_*方法;再调用redo()时,do_*方法会再次执行。这样一个动作就同时携带了正向执行与反向回滚的完整信息,实现"可逆操作"。

从类继承关系看,UndoRedo直接继承自Object(见 class_undoredo.rst),属于引擎核心对象而非节点,因此它可以独立创建、独立管理,不依赖场景树。

在使用任何add_do_*/add_undo_*方法之前,必须先调用create_action()创建一个动作;注册完所有操作后,必须调用commit_action()提交。如果某个动作不需要注册方法,可以省略add_do_method()/add_undo_method();同样地,属性注册也可省略。同一个动作中允许注册多个方法或属性,UndoRedo 会将它们按注册顺序依次执行。

基本用法:从零搭建一个可撤销的"移动节点"操作

下面是最基础也最典型的动作注册流程:把节点移动到新位置,同时把旧位置登记为撤销值。

GDScript 版本:

var undo_redo = UndoRedo.new() func do_something(): pass # Put your code here. func undo_something(): pass # Put here the code that reverts what's done by "do_something()". func _on_my_button_pressed(): var node = get_node("MyNode2D") undo_redo.create_action("Move the node") undo_redo.add_do_method(do_something) undo_redo.add_undo_method(undo_something) undo_redo.add_do_property(node, "position", Vector2(100, 100)) undo_redo.add_undo_property(node, "position", node.position) undo_redo.commit_action()

C# 版本:

private UndoRedo _undoRedo; public override void _Ready() { _undoRedo = new UndoRedo(); } public void DoSomething() { // Put your code here. } public void UndoSomething() { // Put here the code that reverts what's done by "DoSomething()". } private void OnMyButtonPressed() { var node = GetNode<Node2D>("MyNode2D"); _undoRedo.CreateAction("Move the node"); _undoRedo.AddDoMethod(new Callable(this, MethodName.DoSomething)); _undoRedo.AddUndoMethod(new Callable(this, MethodName.UndoSomething)); _undoRedo.AddDoProperty(node, "position", new Vector2(100, 100)); _undoRedo.AddUndoProperty(node, "position", node.Position); _undoRedo.CommitAction(); }

注意示例中的关键点:

  • add_do_property(node, "position", Vector2(100, 100))声明提交动作时把节点 position 设为新值;
  • add_undo_property(node, "position", node.position)在提交之前先读取当前位置,将其保存为撤销值,这样撤销时能精确还原旧位置;
  • 方法参数使用Callable(C# 中为new Callable(this, MethodName.XXX)),属性参数则使用object + StringName 属性名 + Variant 值。

操作顺序与分组原则:do 归 do、undo 归 undo

当多个相互依赖的方法/属性需要注册时,需要特别注意顺序问题:默认情况下,undo 操作按注册的先后顺序依次执行——最先注册的 undo 操作会最先被撤销。因此,与其把 "do 操作" 和它对应的 "undo 操作" 成对穿插注册,不如把所有的 do 操作归在一侧、所有的 undo 操作归在另一侧,如下所示:

GDScript:

undo_redo.create_action("Add object") # DO undo_redo.add_do_method(_create_object) undo_redo.add_do_method(_add_object_to_singleton) # UNDO undo_redo.add_undo_method(_remove_object_from_singleton) undo_redo.add_undo_method(_destroy_that_object) undo_redo.commit_action()

C#:

_undo_redo.CreateAction("Add object"); // DO _undo_redo.AddDoMethod(new Callable(this, MethodName.CreateObject)); _undo_redo.AddDoMethod(new Callable(this, MethodName.AddObjectToSingleton)); // UNDO _undo_redo.AddUndoMethod(new Callable(this, MethodName.RemoveObjectFromSingleton)); _undo_redo.AddUndoMethod(new Callable(this, MethodName.DestroyThatObject)); _undo_redo.CommitAction();

这种组织方式确保撤销时的执行顺序与创建时的依赖关系一致(先移除单例引用,再销毁对象,而不是反过来)。create_action()的backward_undo_ops参数可以反转这一默认行为——当它为true时,undo 操作会按后进先出的顺序执行(详情见下文方法详解)。

对象生命周期管理:add_do_reference 与 add_undo_reference

当动作涉及创建或删除节点时,撤销历史的清理会面临一个难题:如果用户在一个动作中"添加了节点",随后清空了历史(例如切换场景),这个被添加的节点将永远无法再被撤销,成了泄漏在场景树中的孤儿。add_do_reference()与add_undo_reference()正是为解决此类生命周期问题而设计。

add_do_reference(object)注册一个"由 do 动作创建、undo 动作移除"的对象引用。当 do 历史被删除时,UndoRedo 会处理该对象:若对象是RefCounted,则取消其引用(unreference);否则直接释放(freed)。文档特别强调:不要对资源(Resource)使用此方法。

添加节点的典型用法("Add node" 动作):

var node = Node2D.new() undo_redo.create_action("Add node") undo_redo.add_do_method(add_child.bind(node)) undo_redo.add_do_reference(node) undo_redo.add_undo_method(remove_child.bind(node)) undo_redo.commit_action()

对应地,add_undo_reference(object)注册"由 undo 动作创建(或恢复)、do 动作移除"的对象引用,在 undo 历史被删除时做同样的清理。删除节点的典型用法("Remove node" 动作):

var node = $Node2D undo_redo.create_action("Remove node") undo_redo.add_do_method(remove_child.bind(node)) undo_redo.add_undo_method(add_child.bind(node)) undo_redo.add_undo_reference(node) undo_redo.commit_action()

两段示例都借助Callable.bind()把节点作为参数预绑定到add_child/remove_child上,形成完整的"添加 → 引用 → 移除"或"移除 → 恢复 → 引用"闭环。

限制历史深度:max_steps 属性

max_steps(默认值0)控制 undo/redo 历史中最多能保存的步数。当已保存步数超过该上限时,更早的步骤会从历史中被移除,再也无法通过undo()回溯到。值为0或负数表示不限制。

undo_redo.max_steps = 100 # 只保留最近 100 步

在长时间运行的编辑型工具中,合理设置max_steps可以有效控制内存占用(尤其是配合add_do_reference/add_undo_reference时,限制步数意味着更早创建的对象引用也会随历史清理被释放)。

动作合并策略:MergeMode 枚举

连续执行相似操作(如拖动滑块、连续缩放)时,如果每一步都成为独立动作,撤销体验会很糟糕。create_action()的merge_mode参数控制动作之间的合并策略,对应UndoRedo.MergeMode枚举:

常量值行为
MERGE_DISABLE0不做合并,"do"/"undo" 操作各自独立成动作(默认值)
MERGE_ENDS1若与上一个动作同名则合并;只保留第一个动作的 "undo" 操作和最后一个动作的 "do" 操作。适合对单个值做连续变更的场景
MERGE_ALL2若与上一个动作同名则合并(保留全部 do 与 undo 操作)

典型场景是属性连续变化:例如用户在检查器中连续拖动position.x,每次提交都使用create_action("Move", UndoRedo.MERGE_ENDS),最终历史中只会留下一条动作——撤销一次即可回到拖动前的状态。

与MERGE_ENDS配合的是start_force_keep_in_merge_ends()/end_force_keep_in_merge_ends()方法对:被start_force_keep_in_merge_ends()标记之后注册的下一个 "do" 与 "undo" 操作,即使动作最终被MERGE_ENDS合并,也会被保留处理;调用end_force_keep_in_merge_ends()恢复常规行为。这在合并时仍需执行关键副作用(如释放中间资源)的场景中非常有用。

完整方法参考:create / commit / undo / redo 主流程

create_action(name, merge_mode = 0, backward_undo_ops = false)

创建一个新动作。在此之后调用所有add_do_method()/add_undo_method()/add_do_property()/add_undo_property()注册操作,最后用commit_action()提交。merge_mode决定动作如何与上一个同名动作合并(见上文枚举表);backward_undo_ops决定 undo 操作的执行顺序:为false时 undo 操作按注册顺序执行(先注册先撤销),为true时反转顺序。

commit_action(execute = true)

提交动作。execute默认为true,即调用本方法时立即执行所有 "do" 方法并设置所有 "do" 属性。若传入false,则只登记动作而不立即执行,适用于"先登记、稍后由别的流程触发执行"的场景。

undo() 与 redo()

undo()撤销最近一个动作,redo()重做最近一个被撤销的动作。两者均在无可执行动作时返回false,可据此禁用 UI 按钮:

undo_button.disabled = not undo_redo.has_undo() redo_button.disabled = not undo_redo.has_redo()

信号 version_changed

每当undo()或redo()被调用时,UndoRedo 会发出version_changed信号。注意:文档明确指出该信号只在 undo/redo 时触发,与提交新动作(version 自动递增)并不等同,需要刷新历史 UI 时应同时监听提交路径。

常用查询方法一览

方法返回说明
get_current_action()int当前动作的索引
get_current_action_name()String当前动作名称,等价于get_action_name(get_current_action())
get_action_name(id)String按索引获取动作名称
get_history_count()int历史中的动作数量
has_undo()bool是否有可撤销动作
has_redo()bool是否有可重做动作
is_committing_action()boolUndoRedo 当前是否正在执行动作提交(即正在运行 "do" 方法或设置属性)
get_version()int版本号(见下节)
clear_history(increase_version = true)void清空历史及关联引用;increase_version为false时版本号不递增

版本号机制:用 get_version() 检测"是否有未保存改动"

每次提交一个新动作,UndoRedo 的版本号(version)会自动递增。get_version()返回该版本号,主要用于**对比"已保存时的版本"与"当前版本"**是否一致,从而判断是否有未保存的改动:

var saved_version := undo_redo.get_version() # ...用户执行了若干操作... func _notification(what): if what == NOTIFICATION_WM_CLOSE_REQUEST: if undo_redo.get_version() != saved_version: # 弹窗提示用户保存 pass

clear_history()默认也会递增版本号(可通过increase_version = false禁止),保证清空历史后文档状态与版本号仍然一致。

与编辑器集成:EditorUndoRedoManager

在编辑器中开发插件时,不要直接使用独立的UndoRedo实例,而应改用 EditorUndoRedoManager。它是编辑器为每个已打开场景维护的 UndoRedo 管理器:每个场景拥有独立的撤销历史,与场景无关的操作(如 ProjectSettings 修改、外部资源编辑)则进入全局历史。它通过 EditorPlugin.get_undo_redo() 获取,API 与 UndoRedo 基本一致,主要区别在于用object + method name代替Callable注册方法。

@tool extends EditorPlugin func _on_my_tool_button_pressed(): var undo_redo := get_undo_redo() var node := get_node("MyNode2D") undo_redo.create_action("Move the node") undo_redo.add_do_property(node, "position", Vector2(100, 100)) undo_redo.add_undo_property(node, "position", node.position) undo_redo.commit_action()

管理器会根据动作中第一个操作的对象自动推断目标历史:对象是Node则归属当前编辑场景;是内置资源则归属其路径对应场景;是外部资源或其他类型则进入全局历史。推断可能出错时,可在create_action()中传入custom_context对象强制指定归属。此外还需注意:

  • clear_history(id, increase_version = true)可针对单个场景、全局历史(GLOBAL_HISTORY = 0)或全部历史(INVALID_HISTORY = -99)清空;
  • mark_unsaved = false(4.5 新增参数)可让动作不把历史标记为未保存,适用于选区变化等自动保存的设置;
  • force_fixed_history()强制下一次操作使用动作自身历史而非按对象推断,仅用于嵌套资源尚无路径等极端情况;
  • 在@tool脚本或导出工具按钮中,可用EditorInterface.get_editor_undo_redo()获取管理器(见 class_@gdscript.rst 的@export_tool_button示例);注意导出工程中不存在EditorInterface,需改用Engine.get_singleton(&"EditorInterface")并省略静态类型。

EditorNode3DGizmo 的_commit_handle()/_commit_subgizmos()回调也遵循同一模式:通常以当前手柄值作为 "do"、restore参数作为 "undo" 创建动作;cancel为true时则直接设置 restore 值、不创建动作(见 class_editornode3dgizmo.rst)。

版本演进备注

create_action()的backward_undo_ops参数在 Godot 4.1 引入(见 upgrading_to_godot_4.1.rst),GDScript 与 C# 均兼容;EditorUndoRedoManager.create_action()的mark_unsaved参数在 Godot 4.5 引入。旧版本迁移时可通过源码兼容层平滑过渡,无需改动调用方。

结语:UndoRedo 的适用边界

UndoRedo 是"命令模式"的引擎级实现:以动作为最小粒度,同时登记执行与回滚逻辑,天然适合编辑器工具、关卡搭建、绘画程序等需要可逆操作的应用。设计动作时牢记两条原则——do 操作与 undo 操作分组注册、创建/删除对象时配套add_do_reference/add_undo_reference——即可写出撤销行为完全符合预期的工具链;需要与编辑器全局历史联动时,改用 EditorUndoRedoManager 即可无缝接入场景级撤销体系。

  • 文档
  • 教程
  • 游戏开发

【免费下载链接】godot-docs

Godot Engine official documentation

项目地址:https://gitcode.com/GitHub_Trending/go/godot-docs
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询