У меня в проекте есть 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
// здесь происходит инициализация моего ассета
// с которым буду проводить манипуляции
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
Всё. Это было просто.
Теперь к сложному-простому.
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
// здесь происходит инициализация моего ассета
// с которым буду проводить манипуляции
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. Но если с этим моментом разобраться - становится всё просто :-)
Комментариев нет:
Отправить комментарий