C++ 游戏关卡编辑器怎么手写撤销与重做系统(Undo/Redo Command模式)

轻晨姑娘_2139

轻晨姑娘_2139

2026-10-21

826人浏览

原创

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

c++ 游戏关卡编辑器怎么手写撤销与重做系统(undo/redo command模式)

撤销系统必须用 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,导致重做栈无法感知“撤销中途插入新操作”这个事件,结果重做列表残留已失效的命令。

  • 用 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 Guardian
Command Guardian

用于在执行删除、覆盖、移动、部署、重写 Git 历史、修改权限等命令前的安全检查。

下载

不合并的后果不只是栈膨胀:重做时逐帧重放,可能因浮点误差或时间步长差异导致最终位置偏移;更严重的是,某些操作(如批量删除)一旦拆成单个命令,就丧失原子性。

  • 在 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() 时不立即加载,等编辑器空闲帧再异步补全

真正难的不是写四个函数,而是每次新增编辑功能时,都得想清楚:这个操作的数据边界在哪?哪些字段变、哪些不该变?合并策略会不会漏掉关键状态?这些没法靠框架自动推导。

C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!

相关专题

更多
c++和c语言的区别有哪些
c++和c语言的区别有哪些

c++和c语言的区别:1、面向对象编程(OOP)支持不同;2、新增特性不同;3、标准库不同;4、编译方式不同;5、命名空间不同等等。想了解更多c++的相关内容,可以阅读本专题下面的文章。

2024.03.14

2248

9

c++和python学习顺序推荐
c++和python学习顺序推荐

一般建议先学习C++,再学习Python,因为这样可以逐步从较为底层的编程语言向更高级的语言过渡。想了解更多python的相关内容,可以阅读本专题下面的文章。

2024.03.14

1019

6

python和c++学习性价比分析
python和c++学习性价比分析

Python易于学习,广泛应用于Web开发、数据科学和人工智能等领域,但性能较低。C语言性能高,适用于对性能要求较高的场景,如游戏开发和系统编程,但学习曲线陡峭,错误处理复杂。想了解更多python的相关内容,可以阅读本专题下面的文章。

2024.03.14

447

5

c语言和c++一样吗
c语言和c++一样吗

c语言和c++是两种不同的编程语言,虽然有相似之处,但存在显著差异。c语言专注于过程式编程和系统级开发,以简洁、高效著称。c++作为c语言的超集,引入了面向对象编程,增强了代码组织和管理能力,但学习曲线也更陡峭。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.03.14

327

5

c语言和c++先学哪个好
c语言和c++先学哪个好

初学者选择学习c语言还是c++语言,需要根据个人学习目标、背景以及编程兴趣和预期应用方向来决定。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.03.14

406

5

c语言和c++的区别和联系
c语言和c++的区别和联系

c语言和c++是计算机科学领域应用广泛的编程语言。虽然它们有着相似的基础,但它们在语言类型、语法功能和内存管理方面存在着显著差异。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.03.14

580

5

c++软件中文更改教程
c++软件中文更改教程

对于 ide,可通过打开设置,找到语言设置,选择中文,并保存更改。对于非 ide 应用程序,可查找设置或选项,选择语言设置,更改为中文,并保存更改。想了解更多c++的相关内容,可以阅读本专题下面的文章。

2024.03.21

1409

9

python和java和c++学习性价比分析
python和java和c++学习性价比分析

Python以其易学性、丰富的库和活跃的社区而著称,适合数据科学、人工智能和Web开发。Java以其跨平台性、企业级应用开发和Android应用开发而闻名。C++以其底层控制能力、高效性能和游戏开发而著称。选择哪种语言取决于个人兴趣、职业方向和特定需求。想了解更多python和java和c++的相关内容,可以阅读本专题下面的文章。

2024.03.22

1217

7

c++和c语言学习顺序推荐
c++和c语言学习顺序推荐

对于初学者,建议先学习C语言,掌握编程基础后再转入C++,便于理解面向对象编程概念。有编程经验者可直接学习C++,快速接触高级编程技术。想了解更多c++和c语言的相关内容,可以阅读本专题下面的文章。

2024.03.25

1325

9

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C++ 手册教程
C++ 手册教程

共41课时 | 78.4万人学习

C++ Core Guidelines
C++ Core Guidelines

共0课时 | 0人学习

C++ Reference
C++ Reference

共0课时 | 0人学习