Undo/Redo系统必须用Command对象封装操作,深拷贝关键状态、禁止依赖外部指针或引擎API、用vector+索引管理栈、支持合并、绕过GC与资源加载逻辑。

撤销系统必须用 Command 对象封装操作
直接修改游戏对象状态再靠变量回滚,99% 会出错。Undo/Redo 的核心是把每次可逆操作(比如移动一个敌人、修改触发器范围)打包成一个 Command 实例,它得自己知道怎么执行(execute())、怎么反向执行(undo()),而且不能依赖外部临时状态。
常见错误是让 Command 持有原始指针或引用,结果关卡对象被删除后,undo() 访问野指针;或者在 execute() 里直接改了对象字段,却没在 undo() 里存够快照——比如只记了旧坐标,忘了旧旋转或激活状态。
- 每个
Command构造时,必须深拷贝或序列化关键字段(位置、布尔开关、字符串 ID 等),别偷懒用std::shared_ptr包着原始对象然后指望它“自动同步” - 避免在
Command里调用引擎 API(如GetWorld()->SpawnActor()),这类调用应由编辑器统一调度,Command只管数据变更 - 给
Command加个virtual ~Command() = default;,防止多态析构出问题
撤销栈和重做栈要用 std::vector 而不是 stack
std::stack 只能访问顶部,但重做需要从栈底取命令;更麻烦的是,用户撤销到中间某步后新操作,必须清空其后的所有重做项——这需要随机访问和截断能力。std::vector 是唯一合理选择。
典型误用是两个独立 std::stack,导致重做栈无法感知“撤销中途插入新操作”这个事件,结果重做列表残留已失效的命令。
立即学习“C++免费学习笔记(深入)”;
- 用
std::vector<:unique_ptr>> undo_stack;</:unique_ptr>和std::vector<:unique_ptr>> redo_stack;</:unique_ptr> - 维护一个整数索引
current_index,指向当前有效状态在undo_stack中的位置(初始为 -1),所有操作都基于该索引增删 - 执行新命令时:
undo_stack.erase(undo_stack.begin() + current_index + 1, undo_stack.end());,再 push_back,然后current_index++,redo_stack.clear()
Command 必须支持合并(Merge)避免操作爆炸
拖拽一个物体时,鼠标每帧移动都会产生一个 MoveCommand,几十帧下来就是几十个命令——用户点一次 Ctrl+Z 却只挪回 1 像素,体验极差。必须在连续同类操作间自动合并。
不合并的后果不只是栈膨胀:重做时逐帧重放,可能因浮点误差或时间步长差异导致最终位置偏移;更严重的是,某些操作(如批量删除)一旦拆成单个命令,就丧失原子性。
- 在
Command基类加虚函数virtual bool CanMerge(const Command& other) const和virtual void Merge(const Command& other) - 例如
MoveCommand合并条件:目标 ID 相同、且上一条也是MoveCommand;合并动作是更新final_position字段,丢弃中间位置 - 编辑器在 push 新命令前,先检查
undo_stack末尾是否可合并,能则调用Merge(),否则才 push
撤销系统要绕过 GC 和资源卸载逻辑
UE 项目里,如果 Command 删除了一个 Actor,undo() 试图 SpawnActor,但该 UClass 已被卸载,就会 crash;或者 C++ 编辑器里用 new 分配对象,undo() 重建时忘了调用构造函数,成员变量是垃圾值。
根本矛盾在于:撤销不是“重新走一遍流程”,而是“回到某个确定的数据快照”。任何依赖运行时环境(资源加载、GC 标记、引擎 Tick)的操作都不可靠。
- 禁止在
Command里调用UObject::StaticClass()、LoadObject、FindObject等可能触发加载或 GC 的函数 - 所有需持久化的数据(如 Actor 的
FTransform、组件列表、自定义属性 struct)必须在execute()前就序列化进Command成员变量 - 如果必须恢复资源引用(比如贴图路径),存储
FString或FSoftObjectPath,undo()时不立即加载,等编辑器空闲帧再异步补全
真正难的不是写四个函数,而是每次新增编辑功能时,都得想清楚:这个操作的数据边界在哪?哪些字段变、哪些不该变?合并策略会不会漏掉关键状态?这些没法靠框架自动推导。



















