Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Custom editor, changes lost on run

Discussion in 'Scripting' started by JoeStrout, Aug 27, 2014.

  1. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    I'm trying to make a custom editor for some rather complex data I have attached to a MonoBehaviour subclass. It appears to work, right up to the point where I click the Run button. Then, all the data my custom editor added gets lost, and the object goes back to the default state (and so, of course, doesn't do what I want).

    It all begins with a custom Editor (i.e. inspector), which displays an edit button:
    Code (csharp):
    1.     public override void OnInspectorGUI() {
    2.         ScritchBehaviour sb = target as ScritchBehaviour;
    3.         EditorGUILayout.LabelField("Steps", sb.steps.StepCount.ToString());
    4.         if (GUILayout.Button("Edit")) {
    5.             Debug.Log("EDIT!");
    6.             ScriptEditWindow window = (ScriptEditWindow)EditorWindow.GetWindow(typeof(ScriptEditWindow));
    7.             if (sb.steps == null) sb.steps = new ListOfSteps();
    8.             window.target = sb;
    9.             window.title = "Scritch";
    10.             window.Focus();
    11.         }
    12.     }
    Then the editor window's OnGUI does stuff like this:
    Code (csharp):
    1.         GUILayout.Label("Steps: " + target.steps.StepCount);
    2.         EditorGUILayout.Space();
    3.  
    4.         if (GUILayout.Button("Add Move(10)")) {
    5.             target.steps.steps.Add(new MoveForward(10));
    6.             EditorUtility.SetDirty(target);
    7.         }
    I can see, both in the inspector and in the custom editor window, that I am adding steps (by looking at the step count). These steps stick around even when I deselect the object, then re-select it again. But as soon as I click the Run button, Poof! They're gone.

    I have verified that all the types in the data hierarchy are either core types (int, float) or small classes of these, marked with [System.Serializable]. I'm calling SetDirty as you can see. What am I missing?

    Thanks,
    - Joe
     
  2. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    steps.steps.Add?

    Mind posting your class definition?
     
  3. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    Sure... it's several related classes, but here are the key bits.
    Code (csharp):
    1. [RequireComponent (typeof(ScritchSprite))]
    2. public class ScritchBehaviour : MonoBehaviour {
    3.     public ListOfSteps steps;
    4.     ...
    5. }
    6.  
    Code (csharp):
    1.     [System.Serializable]
    2.     public abstract class Step {
    3.         ...
    4.         public virtual int StepCount {
    5.             get { return 1; }
    6.         }
    7.     }
    Code (csharp):
    1.     [System.Serializable]
    2.     public class ListOfSteps : Step {
    3.         public List<Step> steps;
    4.         int curStepIndex;
    5.  
    6.         public override int StepCount {
    7.             get {
    8.                 int count = 0;
    9.                 if (steps != null) {
    10.                     foreach (Step step in steps) {
    11.                         count += step.StepCount;
    12.                     }
    13.                 }
    14.                 return count;
    15.             }
    16.         }
    17.  
    18.         public ListOfSteps() {
    19.             if (this.steps == null) this.steps = new List<Step>();
    20.         }
    21.  
    22.         public ListOfSteps(params Step[] steps) {
    23.             this.steps = new List<Step>(steps);
    24.         }
    25.         ...
    26.     }
    So, yeah, the steps.steps thing may indicate some poor naming, but at the moment I'm just trying to hack out something that works.

    Another clue: if I do "Copy Component" on one of these things when it has a bunch of steps, and then "Paste Component as New," the new component has 0 steps. So clearly, I'm somehow failing to properly serialize the list of steps somewhere. But where?
     
  4. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Step is abstract, which makes me guess you have multiple class deriving from it. Unity is unable to properly serialize polymorphic instances that does not derive from ScriptableObject or MonoBehaviour. So on every context reload, the list is reverted to the base type - Step - and since it is abstract, no instance can be created.
     
  5. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    Polymorphic indeed, I had no idea that mattered. Thanks for that tip!

    OK, I've reworked the whole Step hierarchy to derive from ScriptableObject, and I feel it's getting closer. I can see that, in the editor, Awake and Start are getting called on these objects when I instantiate them (though this is undocumented, it appears to work just like MonoBehaviour in this regard). But when I run the project, I get "The referenced script on this Behaviour is missing!" errors in the console — it looks like one for the ListOfSteps, and one for each step (subclass) it contains. And at that point, the step count on the component drops right back down to 0 again.

    (Unity folks: if you're listening, that error message would be a lot more useful if it would include the name of the script it's failing to find!)

    Here's the revised code — again, trimmed for brevity, but hopefully not omitting anything important:

    Code (csharp):
    1.     public class Step : ScriptableObject {
    2.         ...
    3.         public virtual int StepCount {
    4.             get { return 1; }
    5.         }
    6.     }
    Code (csharp):
    1.     public class ListOfSteps : Step {
    2.         public List<Step> steps;
    3.         int curStepIndex;
    4.  
    5.         public override int StepCount {
    6.             get {
    7.                 int count = 0;
    8.                 if (steps != null) {
    9.                     foreach (Step step in steps) {
    10.                         count += step.StepCount;
    11.                     }
    12.                 }
    13.                 return count;
    14.             }
    15.         }
    16.  
    17.         void Start() {
    18.             Debug.Log("ListOfSteps.Start");
    19.         }
    20.  
    21.         void Awake() {
    22.             Debug.Log("ListOfSteps.Awake");
    23.             if (steps == null) steps = new List<Step>();
    24.         }
    25.     }
    Code (csharp):
    1.     public class MoveForward : Step {
    2.         public FloatExpression distance = 10;
    3.  
    4.         void Awake() {
    5.             Debug.Log("MoveForward.Awake");
    6.         }
    7.         ...
    8.     }
    Code (csharp):
    1. public class ScritchBehaviour : MonoBehaviour {
    2.     public ListOfSteps steps;
    3.     ...
    4. }
    ...and finally, the custom editor now adds steps with code like
    Code (csharp):
    1. target.steps.steps.Add(ScriptableObject.CreateInstance<MoveForward>());
    So. Since ListOfSteps, and all the things it contains, are now ScriptableBehaviours, I would expect it to be storing the class names (Scritch.ListOfSteps and Scritch.MoveForward — this is all in a "Scritch" namespace), and instantiating via those at runtime.

    I'll try losing the namespace to see if that makes it work. If not, I'm stumped (again)!
     
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    ....aaaand, no, removing the namespace doesn't make it work. Drat.

    Any ideas will be greatly appreciated.
     
  7. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    ScriptableObject needs to be serialized independently somewhere. They can exist in 2 places; as a file in your Asset, or in a Scene.
    A prefab cannot target a SO in a scene and cannot store one in itself. So most of the time, people tends to save them as independent files.
     
  8. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    Ugh, this is getting uglier and uglier. I had no idea I was opening such a meaty can of worms.

    But now, armed with the proper search terms and a basic understanding of what's required, I can find the footsteps of others who have gone down this path before. I'll follow them a while and see if I can wrestle this thing into submission. Thanks for pointing me in the right direction.
     
  9. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    For those who find this thread later, here's a decent summary of the problem, though with a proposed solution (using MonoBehaviour for everything) that is impractical in my case (these Step objects are tiny and one object could have hundreds of them):

    http://www.codingjargames.com/blog/2012/11/30/advanced-unity-serialization/

    ...and here's the Feedback case asking for Unity to fix this properly, so people like us don't keep stumbling into this morass of unwieldy half-solutions:

    http://feedback.unity3d.com/suggestions/serialization-of-polymorphic-dat

    The reference above is from 2012, though... if anything has changed since then (I've heard rumors of serialization callbacks that may apply), or anybody knows of a more recent and complete how-to on this topic, please share!
     
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    You can also check the Advanced Inspector in my signature. It has a feature called "Sub-components"...



    It creates MonoBehaviour, but hide them, and only display them as being child of the script that created them. If the parent script is destroyed, the children are also destroyed. If you read the manual, look for "ComponentMonoBehaviour" and "CreateDerived".
     
  11. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    Thanks. In my case, because I have a large number of very lightweight objects and no need for shared references, I've decided to just write some code to convert these to/from a string, and store this string on the MonoBehaviour. My custom editor can either manipulate this string directly, or manipulate the objects and have them convert themselves into a string, which the editor will then stuff back onto the object. Then in Start(), the MonoBehavior will reconstitute the objects from the string, and off we go.

    It seems a bit insane, but it's far simpler than any of the alternatives I can find.
     
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Just hope you are not trying to spawn many of those instance... Because the string comparison cost could prevent smooth spawning.
     
  13. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    Yeah, I thought about that. It's not just a string comparison; it's going to be (potentially) some fairly serious parsing. But I think it will always be at level load, rather than while the level is running; once the level is running, when an object wants to spawn a clone, I can copy the parsed data structures over (or even share them) rather than re-parsing.

    Um, unless somebody instantiates a prefab... in which case I'll have to parse. But unless (as you say) they're spawning a whole bunch of instances at once, I think the parsing will be fast enough not to cause jitter.