【Unity】Undo/撤销

本文详细介绍了Unity编辑器中Undo机制的工作原理及使用方法。包括如何撤销创建对象、游戏体属性变化,以及针对UnityEngine.Object的撤销操作。还讲解了Undo的各种类型及其应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Ctrl + z这对按键组合应该为广大计算机使用者所熟知,即用来做撤销操作。在没有热键屏蔽的情况下(搜狗输入法,QQ等软件可能会抢占某些常用按键组合的优先使用权从而导致我们按下按钮发现没有反应),Unity也可以通过Ctrl+z的组合来进行很多撤销。

比如我们在场景中新建一个Cube,按下Ctrl+ z(或者菜单栏的Edit->Undo),新建的这个Cube就从场景中消失了。

Undo的机制

Unity中Undo使用的数据结构基于Stack,采用LIFO(Last In First Out)的策略,越晚进栈的越早被弹出。

这里写图片描述
这里写图片描述

撤销创建一个对象

创建一个新的空白场景,

创建一个用于Undo的对象

下面的代码是用于生成立方码。Example/Create Cube您可以生成由执行多维数据集。

using UnityEngine;
using UnityEditor;

public class Example
{
    [MenuItem("Example/Create Cube")]
    static void CreateCube ()
    {
        GameObject.CreatePrimitive (PrimitiveType.Cube);
    }
}

这里写图片描述

发现Ctrl+z 无法撤销我们的创建操作。由于我们单纯地用了代码来创建这么一个Cube,记录操作的栈并没有记录之前的状态,所以这个操作无法撤销。

针对创建新GameObject的操作,我们使用Undo.RegisterCreatedObjectUndo来记录之前的状态。

让我们重新创建一个新的场景(为了避免使用之前的Undo栈)

using UnityEngine;
using UnityEditor;

public class Example
{
    [MenuItem("Example/Create Cube")]
    static void CreateCube ()
    {
        var cube = GameObject.CreatePrimitive (PrimitiveType.Cube);
        Undo.RegisterCreatedObjectUndo (cube, "Create Cube");
    }
}

我们使用上面的代码来创建新的Cube,这次使用Undo.RegisterCreatedObjectUndo (cube, "Create Cube");记录了状态,所以可以撤销创建新Cube的操作了。

撤消游戏体属性的变化

下面给出一段旋转GameObject的代码,Example/Random Rotate可以让物体旋转一个随机的角度

using UnityEngine;
using UnityEditor;

public class Example
{
    [MenuItem("Example/Random Rotate")]
    static void RandomRotate ()
    {
        var transform = Selection.activeTransform;

        if (transform) {
            transform.rotation = Random.rotation;
        }
    }
}

这里写图片描述

上面的代码里没有Undo相关的代码,所以并不能撤销随机角度旋转的操作。
这里我们使用Undo.RecordObject来记录Object的属性。

using UnityEngine;
using UnityEditor;

public class Example
{
    [MenuItem("Example/Random Rotate")]
    static void RandomRotate ()
    {
        var transform = Selection.activeTransform;

        if (transform) {
            Undo.RecordObject (transform, "Rotate " + transform.name);
            transform.rotation = Random.rotation;
        }
    }
}

旋转之后Ctrl+z,bingo!Cube的rotation重置了!

撤销UnityEngine.Object

下面来讲讲继承自UnityEngine.Object对象的撤销(继承自UnityEngine.Object的对象都是可序列化的)

经常Undo的对象包括下面三种:

  • 游戏对象
  • Component(也包括MonoBehaviour)
  • ScriptableObject

当要Undo用System.Serializable修饰的属性的时候,如下操作,Example/Change PlayerInfo后撤销就回到之前的状态了。

[System.Serializable]
public class PlayerInfo
{
    public string name;
    public int hp;
}

示例Player

using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField]
    PlayerInfo info;
}
using UnityEngine;
using UnityEditor;

public class Example
{
    [MenuItem("Example/Change PlayerInfo")]
    static void ChangePlayerInfo ()
    {
        var player = Selection.activeGameObject.GetComponent<Player> ();

        if (player) {
            Undo.RecordObject (player, "Change PlayerInfo");
            player.info = new PlayerInfo{
                name = "New PlayerName",
                hp = Random.Range(0,10)
            };
        }
    }
}

Undo的类型

  • Undo.RecordObject(s)记录Object的状态
  • Undo.AddComponent 要添加Component的时候使用,可以撤销掉新加的Component
  • Undo.RegisterCreatedObjectUndo 新建Object的时候使用,可以撤销新建的物体
  • Undo.DestroyObjectImmediate 要删除Object的时候使用,可以撤销删除操作
  • Undo.SetTransformParent 修改Transform的Parent的时候使用,可以恢复层次结构

Revert

Revert是和Record相反的操作,相当于Ctrl+z

这里写图片描述

Undo中有Group的概念,通过Undo.IncrementCurrentGroup()来增大groupID,一个group可以包含多个记录并可以同时操作。

  • Undo.RevertAllInCurrentGroup() 撤回当前groupID的所有操作
  • Undo.IncrementCurrentGroup() 把记录的groupId加1
  • Undo.RevertAllDownToGroup(int groupID) 回退到groupID的状态
  • Undo.CollapseUndoOperations (int groupID) 把groupID组中的记录折叠,一次Ctrl+z即可回退该组所有记录
<think>我们正在处理Unity编辑器扩展开发中的撤销操作问题。用户想知道如何正确使用`Undo.RecordObject`与`EditorGUILayout.ObjectField`配合以支持撤销操作。 关键点: 1. 在Unity编辑器脚本中,当我们修改一个对象的属性时,为了支持撤销Undo)操作,我们需要使用`Undo.RecordObject`来记录对象的状态。 2. `EditorGUILayout.ObjectField`用于在编辑器窗口中绘制一个对象字段,允许用户分配或更改一个UnityEngine.Object类型的引用。 3. 当我们使用`EditorGUILayout.ObjectField`来修改一个对象的引用字段时,我们需要在修改前记录撤销操作。 步骤: - 首先,使用`EditorGUI.BeginChangeCheck()`来开始检测GUI的变化。 - 然后,绘制`ObjectField`并获取其返回值。 - 使用`EditorGUI.EndChangeCheck()`来判断值是否发生了变化。 - 如果发生了变化,则调用`Undo.RecordObject`来记录对象的状态,然后再进行赋值。 注意:`Undo.RecordObject`需要在修改对象属性之前调用,这样Unity才能记录对象的状态。 示例场景: 假设我们有一个自定义的编辑器窗口,其中有一个`MonoBehaviour`脚本的实例(称为`targetObject`),它有一个`GameObject`类型的字段`targetField`。 我们将展示如何在这个编辑器窗口中绘制一个对象字段,并支持撤销操作。 代码示例:</think># Unity Editor 扩展开发:使用 Undo.RecordObject 与 EditorGUILayout.ObjectField 实现撤销操作 在 Unity 编辑器扩展开发中,正确实现撤销操作是创建专业工具的关键。`Undo.RecordObject` 与 `EditorGUILayout.ObjectField` 的配合使用可以确保对象引用的变更支持撤销/重做功能。以下是详细实现方法: ## 核心实现原理 撤销操作的核心流程: 1. **开始变更检测**:使用 `EditorGUI.BeginChangeCheck()` 2. **绘制对象字段**:使用 `EditorGUILayout.ObjectField()` 3. **检测变更**:使用 `EditorGUI.EndChangeCheck()` 4. **记录撤销状态**:使用 `Undo.RecordObject()` 5. **应用变更**:更新对象引用 ## 完整示例代码 ```csharp using UnityEditor; using UnityEngine; [CustomEditor(typeof(ExampleComponent))] public class ExampleComponentEditor : Editor { private SerializedObject serializedTarget; private SerializedProperty targetObjectProperty; private void OnEnable() { serializedTarget = new SerializedObject(target); targetObjectProperty = serializedTarget.FindProperty("targetObject"); } public override void OnInspectorGUI() { serializedTarget.Update(); // 开始检测GUI变化 EditorGUI.BeginChangeCheck(); // 绘制对象字段 Object newObject = EditorGUILayout.ObjectField( "目标对象", targetObjectProperty.objectReferenceValue, typeof(GameObject), true ); // 检测到字段变化时执行撤销记录 if (EditorGUI.EndChangeCheck()) { // 记录撤销操作 Undo.RecordObject(target, "更改目标对象"); // 应用新值 targetObjectProperty.objectReferenceValue = newObject; } serializedTarget.ApplyModifiedProperties(); } } // 示例组件 public class ExampleComponent : MonoBehaviour { public GameObject targetObject; } ``` ## 关键点解析 1. **序列化对象处理** - 使用 `SerializedObject` 和 `SerializedProperty` 确保正确处理对象引用 - 通过 `Update()` 和 `ApplyModifiedProperties()` 管理状态同步[^1] 2. **变更检测机制** ```csharp EditorGUI.BeginChangeCheck(); // ... 绘制UI ... if (EditorGUI.EndChangeCheck()) { // 处理变更 } ``` 这种模式确保只在值实际变化时执行撤销操作[^2] 3. **撤销记录** ```csharp Undo.RecordObject(target, "更改目标对象"); ``` - `target` 是要记录状态的对象 - 第二个参数是撤销操作在历史记录中显示的名称 4. **多对象撤销支持** 处理多个对象的撤销操作: ```csharp Undo.RecordObjects(new Object[] { obj1, obj2 }, "批量修改对象"); ``` ## 高级应用:自定义撤销组 对于复杂操作,可以创建撤销组: ```csharp Undo.IncrementCurrentGroup(); // 开始新撤销Undo.RecordObject(obj1, "修改对象1"); obj1.transform.position = new Vector3(1, 2, 3); Undo.RecordObject(obj2, "修改对象2"); obj2.name = "New Name"; Undo.CollapseUndoOperations(Undo.GetCurrentGroup()); // 合并为一个撤销步骤 ``` ## 常见问题解决 1. **撤销后值未更新**: - 确保在 `OnInspectorGUI` 中调用 `serializedObject.Update()` - 使用 `EditorUtility.SetDirty(target)` 标记对象为脏 2. **Prefab 实例修改**: 处理 Prefab 实例时使用: ```csharp PrefabUtility.RecordPrefabInstancePropertyModifications(target); ``` 3. **性能优化**: - 避免在每帧调用的方法中频繁记录撤销 - 对于批量操作使用 `Undo.RecordObjects` 替代多次调用 ## 最佳实践建议 1. 始终为撤销操作提供有意义的描述文本 2. 复杂操作使用撤销组减少历史记录条目 3. 修改 Prefab 实例时使用 `PrefabUtility` API 4. 测试撤销功能在不同场景下的表现(Play 模式/编辑模式)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值