- 文档
- 教程
- 游戏开发
【免费下载链接】godot-docs
Godot Engine official documentation
导读
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_DISABLE | 0 | 不做合并,"do"/"undo" 操作各自独立成动作(默认值) |
MERGE_ENDS | 1 | 若与上一个动作同名则合并;只保留第一个动作的 "undo" 操作和最后一个动作的 "do" 操作。适合对单个值做连续变更的场景 |
MERGE_ALL | 2 | 若与上一个动作同名则合并(保留全部 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() | bool | UndoRedo 当前是否正在执行动作提交(即正在运行 "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: # 弹窗提示用户保存 passclear_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
相关推荐
Godot 编辑器插件撤销重做指南:深入解析 EditorUndoRedoManager
Godot 编辑器插件撤销重做指南:深入解析 EditorUndoRedoManager 导读 EditorUndoRedoManager 是 Godot 编辑
文档教程游戏开发Handsontable UndoRedo 插件完全指南:撤销与重做机制、栈生命周期与自定义可撤销操作
Handsontable UndoRedo 插件完全指南:撤销与重做机制、栈生命周期与自定义可撤销操作 Revert and restore your chan
前端UI组件为什么选择SVG2TikZ?5大优势助你轻松生成高质量LaTeX图形代码
为什么选择SVG2TikZ?5大优势助你轻松生成高质量LaTeX图形代码 SVG2TikZ是一款强大的Inkscape扩展工具,专为将SVG路径导出为TikZ/
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考