如何在编辑器中编辑和持久化可序列化资产Window?

How to edit and persist serializable Assets in the Editor Window?

我的项目中保存了一个资产,它代表一个可序列化的脚本化对象。 对象的代码很简单:

using UnityEngine;
using System.Collections;

public class TestScriptable : ScriptableObject {   
    public float gravity = .3f;
    public float plinkingDelay = .1f;
    public float storedExecutionDelay = .3f;    
}

在检查器中更改此对象的值没有问题,并且更改在退出 → 进入 Unity.

后仍然存在并继续存在

我正在尝试模仿 Editor Window 中的检查员行为。但是我在 Editor Window 中所做的任何更改虽然反映在 Inspector 中,但不会持续存在。 这是我在 Editor 文件夹中的两个脚本:

第一个(辅助)-这个脚本用按钮替换检查器字段(见上图),它调用我的自定义EditorWindow

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(TestScriptable))]
public class TestScriptableEditor : Editor {
  public override void OnInspectorGUI() {
    if (GUILayout.Button("Open TestScriptableEditor"))
      TestScriptableEditorWindow.Init();
  }
}

第二个(我的问题)- 脚本,我试图在其中更改我的资产值:

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;


public class TestScriptableEditorWindow : EditorWindow {
    public static TestScriptableEditorWindow testScriptableEditorWindow;
    private TestScriptable testScriptable;

    [MenuItem("Window/TestTaskIceCat/TestScriptableEditor")]
    public static void Init() {
        // initialize window, show it, set the properties
        testScriptableEditorWindow = GetWindow<TestScriptableEditorWindow>(false, "TestScriptableEditorWindow", true);
        testScriptableEditorWindow.Show();
        testScriptableEditorWindow.Populate();
    }

    // initialization of my troubled asset              
    void Populate() {
        Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets);        
        if (selection.Length > 0) {
            if (selection[0] == null)
                return;

            testScriptable = (TestScriptable)selection[0];
        }
    }

    public void OnGUI() {
        if (testScriptable == null) {
            /* certain actions if my asset is null */
            return;
        }

        // Here is my tries to change values
        testScriptable.gravity = EditorGUILayout.FloatField("Gravity:", testScriptable.gravity);
        testScriptable.plinkingDelay = EditorGUILayout.FloatField("Plinking Delay:", testScriptable.plinkingDelay);
        testScriptable.storedExecutionDelay = EditorGUILayout.FloatField("Stored Execution Delay:", testScriptable.storedExecutionDelay);
        // End of the region of change values
    }    

    void OnSelectionChange() { Populate(); Repaint(); }
    void OnEnable() { Populate(); }
    void OnFocus() { Populate(); }

}

我的问题是:我做错了什么?可能是什么问题呢?如何解决?我是否在编辑器 Window 中错误地加载了资产?或者是什么? 任何 help/ideas 将不胜感激。

好吧,一切都简单又复杂,同时又简单。

尽管检查器中的视觉变化 - 这并不意味着数据实际上已更改。看起来一切正常,但是......在我看来这是 Unity

的缺点

为了正确工作,你应该使用一些东西:

  • GUI.changed - returns 如果任何控件更改了输入数据的值,则为真。我们将用它来检测变化。
  • Undo.RecordObject - 记录在 RecordObject 函数之后对对象所做的任何更改。 Undo state 被记录下来,允许您使用 Undo 系统恢复更改。
  • EditorUtility.SetDirty!!!最重要的事情!!!)- 很快:将目标对象标记为 "dirty",因此需要保存。有关详细信息 - 单击 link.

现在,我们需要做的就是在OnGUI()方法的底部写一些代码;

if (GUI.changed) {
    // writing changes of the testScriptable into Undo
    Undo.RecordObject(testScriptable, "Test Scriptable Editor Modify"); 
    // mark the testScriptable object as "dirty" and save it
    EditorUtility.SetDirty(testScriptable); 
}

即你的代码将是这样的:

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;


public class TestScriptableEditorWindow : EditorWindow {
 public static TestScriptableEditorWindow testScriptableEditorWindow;
 private TestScriptable testScriptable;

 [MenuItem("Window/TestTaskIceCat/TestScriptableEditor")]
 public static void Init() {
  // initialize window, show it, set the properties
  testScriptableEditorWindow = GetWindow<TestScriptableEditorWindow>(false, "TestScriptableEditorWindow", true);
  testScriptableEditorWindow.Show();
  testScriptableEditorWindow.Populate();
 }

 // initialization of troubled asset 
 void Populate() {
  Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets);        
  if (selection.Length > 0) {
   if (selection[0] == null)
    return;

   testScriptable = (TestScriptable)selection[0];
  }
 }

 public void OnGUI() {
  if (testScriptable == null) {
   /* certain actions if my asset is null */
   return;
  }

  testScriptable.gravity = EditorGUILayout.FloatField("Gravity:", testScriptable.gravity);
  testScriptable.plinkingDelay = EditorGUILayout.FloatField("Plinking Delay:", testScriptable.plinkingDelay);
  testScriptable.storedExecutionDelay = EditorGUILayout.FloatField("Stored Execution Delay:", testScriptable.storedExecutionDelay);
  
  // Magic of the data saving
  if (GUI.changed) {
   // writing changes of the testScriptable into Undo
   Undo.RecordObject(testScriptable, "Test Scriptable Editor Modify");
   // mark the testScriptable object as "dirty" and save it
   EditorUtility.SetDirty(testScriptable); 
  }
 }    
  
 void OnSelectionChange() { Populate(); Repaint(); }
 void OnEnable() { Populate(); }
 void OnFocus() { Populate(); }
}

就是这样。简单易行。


现在是故事的复杂部分...

SetDirty - 当然不错。但此功能将在 Unity > 5.3 的版本中弃用。而且在某些版本中它将被删除。什么时候?我不知道。 除了使用 SetDirty,您还可以采用另一种方式:

您应该在两次调用之间执行的自定义编辑器或 EditorWindow 中的所有操作:

serializedObject.Update()

// Here is some of your code

serializedObject.ApplyModifiedProperties()

此代码包含:

最后四个跟SetDirty一样:他们会把修改后的对象(or/and场景)标记为"dirty"并为你创建Undo states

所以,知道这一点,我们可以得到这样的东西:

using UnityEngine;
using UnityEditor;

public class TestScriptableEditorWindow : EditorWindow {
    public static TestScriptableEditorWindow testScriptableEditorWindow;
    private TestScriptable testScriptable;
    // declaring our serializable object, that we are working on
    private SerializedObject serializedObj;

    [MenuItem("Window/TestTaskIceCat/TestScriptableEditor")]
    public static void Init() {
        testScriptableEditorWindow = GetWindow<TestScriptableEditorWindow>(false, "TestScriptableEditorWindow", true);
        testScriptableEditorWindow.Show();
        testScriptableEditorWindow.Populate();
    }

    // initialization of troubled asset
    void Populate() {
        Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets);
        if (selection.Length > 0) {
            if (selection[0] == null)
                return;

            testScriptable = (TestScriptable)selection[0];
            // initialization of the serializedObj, that we are working on
            serializedObj = new SerializedObject(testScriptable);
        }
    }

    // our manipulation
    public void OnGUI() {
        if (testScriptable == null) {
            /* certain actions if my asset is null */
            return;
        }

        // Starting our manipulation
        // We're doing this before property rendering           
        serializedObj.Update();
        // Gets the property of our asset and скуфеу a field with its value
        EditorGUILayout.PropertyField(serializedObj.FindProperty("gravity"), new GUIContent("Gravity"), true);
        EditorGUILayout.PropertyField(serializedObj.FindProperty("plinkingDelay"), new GUIContent("Plinking Delay"), true);
        EditorGUILayout.PropertyField(serializedObj.FindProperty("storedExecutionDelay"), new GUIContent("Stored Execution Delay"), true);
        // Apply changes
        serializedObj.ApplyModifiedProperties();
    }

    void OnSelectionChange() { Populate(); Repaint(); }
    void OnEnable() { Populate(); }
    void OnFocus() { Populate(); }
}

所以,这很简单,因为你应该只使用

Update → 动作 → ApplyModifiedProperties

但是它很复杂,因为你应该用一堆 属性 类 做很多工作:FindPropertyPropertyFieldSerializedProperty

但是当您了解它的工作原理时 - 它变得如此简单...