Страницы

Поиск по вопросам

воскресенье, 12 января 2020 г.

Как сохранять данные asset'ов при их редактировании в EditorWindow?

#c_sharp #unity3d


У меня в проекте есть asset, который из себя представляет serializable scriptable object.

Его код очень простой:

using UnityEngine;
using System.Collections;

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


Мне не составляет труда изменить значения у этого ассета через инспектор. Эти изменения
сохраняются. И после перезагрузки Unity все значения остаются, как и нужно.



Но вот в моем кастомном окне Editor Window такое не получается. Хоть и все изменения,
сделанные в окне, отображаются и в инспекторе, но, тем не менее, после перезагрузки
Unity можно увидеть, что данные остались те, 
которые были до изменения мною. Т.е. те, которые были еще при первой загрузке приложения.
Ничего не сохранилось :-(

вот два скрипта которые я применяю для папки 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() {
        // инициализируем окно, отображаем его и устанавливаем настройки
        testScriptableEditorWindow = GetWindow(false,
"TestScriptableEditorWindow", true);
        testScriptableEditorWindow.Show();
        testScriptableEditorWindow.Populate();
    }

    // здесь происходит инициализация моего ассета
    // с которым буду проводить манипуляции
    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) {
            /* здесь манипуляции в случае если мой ассет 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);
        // конец региона с изменениями значений
    }    

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

}




Собственно вопросы: В чем может быть проблема? Как её решить?  Может я как-то не
так загружаю ассет? Или что?
    


Ответы

Ответ 1



На самом деле все просто и сложно и еще раз просто одновременно. Не смотря на то, что даже при изменении данных в окне вы видите их изменение в инспекторе - не значит, что они реально изменяются. Выглядит как будто все работает, но.....Лично я бы это списал на недоработку со стороны разрабов Unity) Чтобы все работало - нужно воспользоваться несколькими вещами: GUI.changed - который вернет true, если какой-либо контрол изменил значение входных данных. С помощью него будем детектить изменилось что-то или нет. Undo.RecordObject - который позволяет записать изменения в Undo state, позволяя отменить изменения используя undo EditorUtility.SetDirty (!!!самое главное!!!) - если в кратце, то эта команда помечает объект как "грязный" и поэтому требует сохранения. Подробнее можно почитать кликнув на ссылку. Теперь все, что нужно, так это в конце метода OnGUI() записать if (GUI.changed) { // записываем изменения над testScriptable в Undo Undo.RecordObject(testScriptable, "Test Scriptable Editor Modify"); // помечаем тот самый testScriptable как "грязный" и сохраняем. 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() { // инициализируем окно, отображаем его и устанавливаем настройки testScriptableEditorWindow = GetWindow(false, "TestScriptableEditorWindow", true); testScriptableEditorWindow.Show(); testScriptableEditorWindow.Populate(); } // здесь происходит инициализация моего ассета // с которым буду проводить манипуляции 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) { /* здесь манипуляции в случае если мой ассет 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); // Магия по созранению данных if (GUI.changed) { // записываем изменения над testScriptable в Undo Undo.RecordObject(testScriptable, "Test Scriptable Editor Modify"); // помечаем тот самый testScriptable как "грязный" и сохраняем. EditorUtility.SetDirty(testScriptable); } } void OnSelectionChange() { Populate(); Repaint(); } void OnEnable() { Populate(); } void OnFocus() { Populate(); } } Всё. Это было просто. Теперь к сложному-простому. SetDirty - это, конечно, хорошо. Однако начиная с версии > 5.3 этот метод будет признан устаревшим и, возможно, еще в более поздних - будет удален. Когда именно - неизвестно. Вместо него можно пробовать работать по-другому: Все действия в кастомном эдиторе (Editor) и окне (EditorWindow) нужно проводить между вызовами: serializedObject.Update() // Тут код эдитора serializedObject.ApplyModifiedProperties() где: serializedObject.Update() - некий рефреш значений сериализованного объекта serializedObject.ApplyModifiedProperties() - сохранение всех изменений сериализованного объекта serializedObject - это объект, который получает доступ к сериализованным (сохраненным в сцену или в ассет в проекте) свойствам (полям) объекта (или нескольких), который вы редактируете. Он применяется вкупе с: SerializedProperty - свойства, которые будут доставаться из serializedObject будут иметь данный тип, например SerializedProperty myGravity = serializedObject.FindProperty("gravity"); SerializedProperty myPlinkingDelay = serializedObject.FindProperty("plinkingDelay"); ... и т.д. SerializedObject.FindProperty - находит свойство по его имени. EditorGUILayout.PropertyField - создает поле для SerializedProperty. Последние четыре штуки как раз таки как SetDirty - пометит модифицируемый объект и сцену как "грязный" и создаст Undo state для вас. Если уложить все в голове, то получится примерно следующее: using UnityEngine; using UnityEditor; public class TestScriptableEditorWindow : EditorWindow { public static TestScriptableEditorWindow testScriptableEditorWindow; private TestScriptable testScriptable; // объявляем наш сериализованный объект, с которым будем работать в итоге private SerializedObject serializedObj; [MenuItem("Window/TestTaskIceCat/TestScriptableEditor")] public static void Init() { testScriptableEditorWindow = GetWindow(false, "TestScriptableEditorWindow", true); testScriptableEditorWindow.Show(); testScriptableEditorWindow.Populate(); } // здесь происходит инициализация моего ассета // с которым буду проводить манипуляции void Populate() { Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets); if (selection.Length > 0) { if (selection[0] == null) return; testScriptable = (TestScriptable)selection[0]; //инициализируем serializedObj, с которым будем работать serializedObj = new SerializedObject(testScriptable); } } // наши преобразования public void OnGUI() { if (testScriptable == null) { /* здесь манипуляции в случае если мой ассет null */ return; } // начинаем наши манипулиции // лучше это делать перед началом отрисовки свойств serializedObj.Update(); //получаем непосредственно нужное свойство из ассета и отрисовываем поле со значением 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); // Применяем изменения serializedObj.ApplyModifiedProperties(); } void OnSelectionChange() { Populate(); Repaint(); } void OnEnable() { Populate(); } void OnFocus() { Populate(); } } Итак, простота заключается в том, что всего лишь надо вызвать Update → действия → ApplyModifiedProperties. А сложность заключается в том, что придется танцевать с бубном вокруг кучи классов по работе со свойствами: FindProperty, PropertyField и SerializedProperty. Но если с этим моментом разобраться - становится всё просто :-)

Комментариев нет:

Отправить комментарий