← ALL ARTICLES

编辑手势与撤销

全库目录221
本文目录10

拖动位置X时,面板和Gizmo连续显示2、3、9,撤销需要回到手势开始时的1。本文沿字段补丁、稳定身份与宿主事务说明一次编辑的预览、提交和补偿。跨文件批次的条件发布见批处理条件发布,对象与文件的持久共同决定见跨资源编辑恢复。

用户操作流程

以批量重命名为例,用户选择资产后,工具读取稳定ID与版本,生成旧名、新名、引用更新和冲突的变更预览。参数验证与正式执行都读取同一份计划;只读资产、名称冲突或缺失引用在修改前定位到具体ID。用户开始执行后看到按工作项统计的进度,取消在发布安全点生效,最终报告保存成功、跳过、冲突与恢复失败的位置。

交互只负责提供输入和展示这份状态。通过命令行传入相同资产ID、参数与规则版本,也会得到相同计划,界面中的隐式选择和当前视图不参与结果计算。撤销、发布和检查点由计划执行器统一处理,字段拖动沿本页手势事务推进,文件发布见批处理条件发布。

计划与编辑输入

界面与命令行把对象ID、参数、项目规则和工具版本交给同一个计划器。计划器读取冻结的对象快照,生成字段补丁及文件输出清单;执行器负责版本检查、事务和进度。引擎适配器执行对象操作,文件适配器负责暂存与发布。视图模型可以缓存显示状态,正式计划仍使用显式输入,因此窗口焦点和选择变化不会改变已经生成的计划。

某些DCC命令依赖当前选择、模式与活动视图。适配器执行这类命令前保存上下文,设置并验证所需上下文,结束时在finally恢复。直接数据接口则按稳定ID解析对象。显示名适合给用户识别,GUID、自有ID及生成代次才负责找回同一个对象。

序列化层把对象变成可验证的属性树:字段路径和类型定位数据,旧值与新值构成补丁,引用使用稳定ID保存。属性面板与Gizmo都经过这层读写。面板编辑位置与Gizmo拖动位置因此产生相同补丁,保存和撤销也会看到相同数据。预览使用运行时的坐标、材质和约束;不能实时复现的构建结果以暂存预览呈现,直到验证后才进入正式版本。

编辑事务与撤销

补丁保存一次字段变化,命令组织可重做的操作,事务决定哪些操作共同成为一个撤销项。手势的持续预览和最终命令因此分别记录。

命令、补丁与事务

一次补丁保存目标稳定ID、字段路径、旧值、新值与预期对象版本。旧值与新值采用可独立保存的序列化快照,不能让它们引用同一个随后继续修改的列表。Apply先确认对象和字段仍符合前置值,再写新值;Revert检查当前仍是本命令写入的值,再恢复旧值。对象被其他操作改变时,撤销提示冲突,避免用一份旧快照覆盖后来的编辑。

手势合并

鼠标按下时开始手势,读取每个目标字段的初值并保存对象版本。拖动帧只更新事务的最终值,同时通过同一序列化入口预览到模型;初值始终保持按下时的值。松开时验证最终值,把整份补丁作为一条命令推入撤销栈,并清空重做栈;最终值等于初值的字段从命令移除,整份为空便结束手势。按Esc则恢复所有初值,关闭预览并结束手势,不产生撤销记录。

例如位置X从1依次预览为2、3、9,命令保存old=1,new=9。撤销恢复1,重做恢复9;如果每帧把old覆盖成当前值,最终只会退到3。多选拖动同时保存A、B的初值,验证所有目标后才开始预览;更新某目标失败时恢复本次手势已经触及的全部字段。预览期间以手势ID独占这些字段,其他窗口的修改排队或结束当前手势后另建命令,因此取消不会覆盖一个并发写入。

text
Begin: capture stable IDs, versions and immutable old values
Update: validate target; replace new value; apply preview
Commit: validate all; drop unchanged patches; push one command
Cancel: restore old values in reverse order; discard gesture
Undo: verify final values; apply old values in reverse order
Redo: verify old values; apply new values in original order

这个轮廓要求所有值验证在发布命令前完成。对象版本会在每次实际编辑后递增,事务记录本次预览产生的版本;持有手势锁期间由自身写入导致的递增可以追踪,外来版本改变则中止并重新分析。命令的撤销和重做也更新版本,不能把旧版本号写回去伪装历史从未发生。编辑器宿主已有成熟撤销系统时,适配器把手势分组映射到宿主事务,并将会开启其他编辑事务的操作排到手势结束后,避免独立操作并入同一撤销项。

创建、删除与引用

创建命令在第一次Apply前分配稳定ID,并记录完整对象快照、父ID、兄弟顺序与引用关系。Undo删除同一个ID,Redo用同一个ID恢复;每次重做分配新ID会使其他对象的引用失效。删除命令反过来保存被删对象与受影响引用的快照,恢复时先建立全部对象身份,再接父子关系,最后恢复交叉引用。这样A引用B时,即使恢复顺序先遇到A,也不会把尚未恢复的B误判成永久丢失。

稳定ID在命令仍可能撤销或重做时保留,不分配给新对象。若宿主对象引用在销毁后失效,命令通过自有ID到当前宿主对象的映射重新解析,不能继续使用旧指针。删除一个父节点连带删除子节点时,快照涵盖整棵子树及外部引用处理规则;只保存父节点字段无法重建其子对象。

创建、引用重定向和删除组成一个事务时,重做按依赖顺序创建对象、设置引用、删除旧对象,撤销按反序恢复。恢复所需资产和类型在执行前验证;缺少类型时保留失败记录和快照,禁止把部分恢复显示成成功。对象身份和字段补丁因此使用同一执行器,事务才真正覆盖用户看见的整个操作。

Unity 与 Unreal 的撤销

Unity 6000.0中,Undo.RecordObject在修改前保存对象临时副本,帧末比较字段差异,将变化写入撤销记录。工具在每个实际修改帧先记录再赋值,手势适配器把这些帧归入同一宿主事务组;仅在松开时记录对象,会把已经发生的预览当成初值。父子关系变更、添加组件和销毁对象使用对应专用撤销接口,普通字段记录不能替代这些操作。Prefab实例修改还调用PrefabUtility.RecordPrefabInstancePropertyModifications保存实例覆盖,场景及资产保存沿各自宿主入口执行。

Unreal把一次手势映射为跨越该手势寿命的FScopedTransaction,创建时开始记录,正常结束时结束记录。游戏线程在第一次实际改动前对每个受影响、可事务化对象调用Modify(),把修改前序列化状态放入宿主撤销缓冲。成员数组在父对象中变化就记录父对象;组件自身字段变化还记录组件,单记Actor不会自动覆盖所有独立UObject。拖动期间保留同一事务,不能每帧局部创建和销毁一个作用域对象,否则仍形成多次撤销。

属性面板使用IPropertyHandle::SetValue时,句柄负责属性前后通知、事务和包修改,适配器沿该入口更新,避免再重复手工发相同通知。直接写字段的自定义Gizmo则在修改前记录对象并发PreEditChange,写入后发带字段信息和Interactive/ValueSet类型的PostEditChangeProperty;松开再通知本次持续修改结束。通知使依赖该属性的重建与视图刷新发生,撤销记录负责恢复值,两者各有用途。资产被构造脚本或重建逻辑替换后,重新按稳定ID解析目标,不能继续使用修改前缓存的组件指针。

Actor创建通过编辑器的放置/工厂入口,例如UEditorEngine::AddActor并使用可事务化对象标记;删除通过UEditorActorSubsystem::DestroyActor等编辑器入口,同时维护选择、关卡及受影响引用。层级和组件增删把所属容器、旧父节点和新父节点一起登记,和前面的对象快照保持同一事务边界。直接运行时Spawn/Destroy、只记录最后一个属性,不能替代编辑器维护的对象集合变化。

取消手势先按保存的初值恢复已预览字段,并走对应属性通知,再取消事务记录;外部文件按补偿清单处理。FScopedTransaction::Cancel()与恢复文件、撤销已执行远程操作属于不同动作,工具不依赖它提供跨资源数据库回滚。恢复失败保留诊断及快照。对象的包被标为Dirty只表示需要保存,不表示已经写入磁盘;Modify可标脏,必要的显式写入调用MarkPackageDirty,最后由保存入口执行权限、版本控制和文件发布检查。取消后也不能无条件把整包标成干净,包里其他字段可能有本手势之外的编辑。

2026-10-23核读的IPropertyHandle接口固定为UE5.6;FScopedTransaction、Modify与编辑器Actor入口引用Epic当前API页。作用域、修改前记录和保存职责按这份接口契约组织,接入工程锁定实际引擎版本后检查所用入口的签名及嵌套事务行为。

宿主适配验证同时检查对象通知、撤销分组与保存结果:一次拖动形成一个撤销项,取消恢复初值,重载包得到最后已保存值。跨文件操作的持久顺序见跨资源编辑恢复。

事务执行与补偿

一个重命名事务可把 A 改为临时名、B 改为 A、临时名改为 B,再更新引用。每一步先保存反向操作,只有成功执行的步骤才加入已完成列表。若第三步失败,按反序恢复前两步;撤销整个成功事务则反序执行全部步骤。

text
applied = []
try:
    for operation in plan:
        verify operation.precondition
        operation.apply()
        applied.append(operation)
except failure:
    failedCompensations = []
    for finished in reverse(applied):
        try: finished.revert()
        except recoveryFailure: failedCompensations.append(recoveryFailure)
    report failure, applied states and failedCompensations
    stop

这是进程内顺序执行的事务算法轮廓。每一步的前置条件检查都属于事务失败路径:后续操作的条件失效时,已经完成的操作仍需补偿。单步 apply 失败时必须保持原状,或由该步自行恢复部分修改后抛出异常。补偿步骤分别捕获错误,保证一次恢复失败后仍尝试其余反向操作。引擎撤销通常只覆盖登记的对象状态,外部文件和版本控制锁还需补偿或暂存发布。恢复也可能失败,因此记录原值、步骤状态与恢复结果,不能仅在异常处理中清空列表就宣称回滚完成。

本机验证:独立样例 中的 execute_plan 运行了三项事务检查,覆盖后续前置条件失败、某次补偿失败后继续恢复,以及成功路径不触发补偿。它验证纯内存回调顺序;外部文件恢复、引擎撤销、并发隔离与进程崩溃后的持久恢复需要各自的集成验证。

验证方法

分别验证手势1→2→3→9、取消预览、创建删除恢复及第三步失败。一次补偿失败后仍应继续其余反向操作;宿主中再验证单项撤销、属性通知、保存和重载。

实现与证据

机制链协议样例运行手势首末值、撤销重做与取消;补偿样例检查三种纯内存回调顺序。Unity和Unreal的对象撤销、包保存及通知尚未在目标宿主执行。

相关主题

参考资料