4
\$\begingroup\$

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.

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

1 Answer 1

5
\$\begingroup\$

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...

\$\endgroup\$
4
  • \$\begingroup\$ ~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 :| \$\endgroup\$
    – Discipol
    Dec 24, 2016 at 1:42
  • \$\begingroup\$ @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..... \$\endgroup\$ Dec 24, 2016 at 6:28
  • \$\begingroup\$ still have to hardcode strings :| can't I just iterate blindly through a modified object and set everything dirty? \$\endgroup\$
    – Discipol
    Dec 24, 2016 at 11:23
  • \$\begingroup\$ @Discipol I dunno. maybe to use reflection and attributes for class fields \$\endgroup\$ Dec 24, 2016 at 11:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .