I have an asset saved in my project which represents a serializable scriptable object. Code of the object is very simple:

using UnityEngine;
using System.Collections;

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

There is no problem changing values for this object in the inspector, and the changes do persist and survive after exiting → entering Unity.

введите сюда описание изображения

I am trying to mimic the inspector behavior in an Editor Window. But any changes I do in the Editor Window, though reflected in the Inspector, do not persist. Here is my two scripts which lay inside the Editor folder:

the first one (auxiliary) - this script replaces inspector fields (see image above) with button, which calls my custom EditorWindow.

using UnityEngine;
using UnityEditor;

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

введите сюда описание изображения

the second (with my problem) - script, where I'm trying to change my asset values:

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(); }

}

My questions is: what am I doing wrong? What could be the problem? How to fix it? Am I loading the asset wrongly in the Editor Window? Or what? Any help/ideas would be appreciated.

  • Well, I can hint you like... your code must contain "SerializedObject" and "ApplyModifiedProperties" in it. :) Read Unity Docs about them. Do NOT use SetDirty() – Петър Петров Jan 13 '17 at 17:04
  • @ПетърПетров yeap, I read it. And I wrote an explanation in my answer ;) – Alexey Shimansky Jan 13 '17 at 17:12
  • But at the end, I REALLY hated the serialized object. I ended up using SetDirty and intentionally ditching undo – Петър Петров Jan 20 '17 at 18:37
up vote 5 down vote accepted

Well, everything is simple and complicated, and simple again at the same time.

Despite the visual changes in the inspector - it doesn't mean that the data were actually changed. It looks like everything works fine, but......In my opinion it is shortcoming of the Unity

For correctly work you should use few things:

  • GUI.changed - returns true if any controls changed the value of the input data. We would be using it for changes detection.
  • Undo.RecordObject - records any changes done on the object after the RecordObject function. The Undo state is recorded, allowing you to revert the change using the Undo system.
  • EditorUtility.SetDirty (!!!the most important thing!!!) - shortly: marks target object as "dirty" and therefore requiring a save. For more information - click the link.

Now, all we need to do is to write some code at the bottom of the OnGUI() method;

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); 
}

i.e. your code will be like this:

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(); }
}

That's all. It was simple and easy.


Now the complicated-simple part of the story...

SetDirty - is certainly good. But this function is due to be deprecated in versions of Unity > 5.3. And also in some versions it will be removed. When? I dont't know. Instead of using SetDirty you could go another way:

All actions in the custom Editor or EditorWindow you should do between two calls:

serializedObject.Update()

// Here is some of your code

serializedObject.ApplyModifiedProperties()

This code contains:

The last four is like the SetDirty: they will mark the modified object (or/and scene) as "dirty" and create Undo states for you.

So, knowing that, we could get something like this:

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 create 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(); }
}

So, it is simple because you should use just

Update → actions → ApplyModifiedProperties.

But it is complicated because you should do a lot of work with bunch of property classes: FindProperty, PropertyField и SerializedProperty.

But when you understand how it works - it is becoming so easy...

  • ~alexey-shimansky @alexey-shimansky Is it possible to avoid using the FindProperty method? I want to avoid having hardcoded strings to describe methods, any rename or maybe obfuscation will break the link between the string and actual method :| – Discipol Dec 24 '16 at 1:42
  • @Discipol you can keep props in a list/dictionary (as field of a class), e.g. private Dictionary<string, Dictionary<string, bool>> ..... add all properties into it.... also you can write method void SetField(string name, string content, bool includeChildren) { EditorGUILayout.PropertyField(serializedObj.FindProperty(name), new GUIContent(content), includeChildren); } and then just iterate through a Dictionary by calling SetField method anywhere you need..... – Alexey Shimansky Dec 24 '16 at 6:28
  • still have to hardcode strings :| can't I just iterate blindly through a modified object and set everything dirty? – Discipol Dec 24 '16 at 11:23
  • @Discipol I dunno. maybe to use reflection and attributes for class fields – Alexey Shimansky Dec 24 '16 at 11:31

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.