Страницы

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

понедельник, 15 июля 2019 г.

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

У меня в проекте есть 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(); }
}

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


Ответ

На самом деле все просто и сложно и еще раз просто одновременно.
Не смотря на то, что даже при изменении данных в окне вы видите их изменение в инспекторе - не значит, что они реально изменяются. Выглядит как будто все работает, но.....Лично я бы это списал на недоработку со стороны разрабов 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. Но если с этим моментом разобраться - становится всё просто :-)

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

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