#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. Но если с этим моментом разобраться - становится всё просто :-)
Комментариев нет:
Отправить комментарий