Search Unity

InvalidCastException on Editor auto-refresh for List of object (who was valid before)

Discussion in 'Scripting' started by ArthyShow, Feb 4, 2020.

  1. ArthyShow

    ArthyShow

    Joined:
    Aug 6, 2016
    Posts:
    23
    Updated post, old at the end.

    I'm working on a project manager who define scripts who can be used by multiple of others project later (by extending some part to update them).

    I actually have:
    - 2 namespaces Foo (the manager) and Bar (the project)
    - Both define an object Character (Bar.Character extend from Foo.Character)
    - FooEngine is here to list Foo.Character (List<Foo.Character>)
    - FooEngineEditor (defined on Bar namespace) is here in order to add (both Foo or Bar Character) and display Characters (and their cast)
    Foo
    Code (CSharp):
    1. namespace Foo
    2. {
    3.     [System.Serializable]
    4.     public class Character
    5.     {
    6.         public int ID;
    7.         public Character(int ID)
    8.         {
    9.             this.ID = ID;
    10.         }
    11.     }
    12.  
    13.  
    14.     public class FooEngine : MonoBehaviour
    15.     {
    16.         public List<Foo.Character> list = new List<Foo.Character>();
    17.     }
    18. }

    Bar
    Code (CSharp):
    1. namespace Bar
    2. {
    3.     [System.Serializable]
    4.     public class Character: Foo.Character
    5.     {
    6.         public string name;
    7.         public MyObject(int ID, string name)
    8.             : base (ID) {
    9.             this.name= name;
    10.         }
    11.     }
    12.  
    13.     [CustomEditor(typeof(Foo.FooEngine))]
    14.     public class FooEngineEditor : Editor
    15.     {
    16.         public override void OnInspectorGUI()
    17.         {
    18.             Foo.FooEngine fooEngine = (Foo.FooEngine)target;
    19.        
    20.             // --- Default inspector
    21.             DrawDefaultInspector();
    22.  
    23.             // --- Action
    24.  
    25.             int countExistig = fooEngine.list.Count;
    26.  
    27.             GUILayout.BeginHorizontal();
    28.             if (GUILayout.Button("Add Foo.Character"))
    29.             {
    30.                 fooEngine.list.Add(new Foo.Character(countExistig));
    31.             }
    32.             if (GUILayout.Button("Add Bar"))
    33.             {
    34.                 fooEngine.list.Add(new Bar.Character(countExistig, "abc"));
    35.             }
    36.  
    37.             // --- Review
    38.             for (int i = 0, l = fooEngine.list.Count; i < l; i++)
    39.             {
    40.                 EditorGUILayout.LabelField(" - " + i + " > " + fooEngine.list.GetType());
    41.             }
    42.         }
    43.     }
    44. }

    Everyting is working fine and I can use the Editor script in order to Add Foo.Character or Bar.Character into the same list (and casting works fine).

    But when I save code, List are re-casted into Foo.Character and will create InvalidCastException Error if I try to cast `Bar.Character character = (Bar.Character)fooEngine.list[0];`



    What's append and what's causing the list to be recasted ??


    Hi everyone, I'm working on a Manager who will be used by every project in order to redefine some part and not recode basic everywhere.
    (I'm using Microsoft Visual Studio)


    I create a project example in order to reproduce:

    The Manager: Foo

    Goal:
    His job is simply to keep a list of serialized object (MyObject), save them into files and be able to reserialize into Object:

    Foo.MyObject // Serialized Object to list and save.

    Code (CSharp):
    1.  
    2. namespace Foo
    3. {
    4.     [System.Serializable]
    5.     public class MyObject
    6.     {
    7.         public int ID;
    8.  
    9.         public MyObject(int ID)
    10.         {
    11.             this.ID = ID;
    12.         }
    13.     }
    14. }
    Foo.FooMono // Mono who handle MyObjects

    Code (CSharp):
    1.  
    2. namespace Foo
    3. {
    4.     public class FooMono : MonoBehaviour
    5.     {
    6.         public List<Foo.MyObject> listOfObjects = new List<Foo.MyObject>();
    7.  
    8.         string GetFileDirectory()
    9.         {
    10.             string path = Path.Combine(Application.persistentDataPath, "examples");
    11.             if (!Directory.Exists(path))
    12.             {
    13.                 Directory.CreateDirectory(path);
    14.             }
    15.             return path;
    16.         }
    17.  
    18.         public void Load()
    19.         {
    20.             this.listOfObjects = new List<Foo.MyObject>();
    21.             foreach (string filePath in System.IO.Directory.GetFiles(this.GetFileDirectory()))
    22.             {
    23.                 if (File.Exists(filePath))
    24.                 {
    25.                     BinaryFormatter bf = new BinaryFormatter();
    26.                     FileStream file = File.Open(filePath, FileMode.Open);
    27.                     this.listOfObjects.Add((Foo.MyObject)bf.Deserialize(file));
    28.                     file.Close();
    29.                 }
    30.             }
    31.         }
    32.  
    33.         public void Save(Foo.MyObject myObject)
    34.         {
    35.             BinaryFormatter bf = new BinaryFormatter();
    36.             FileStream file = File.Create(this.GetFileDirectory() + "/" + myObject.ID + ".gd");
    37.             bf.Serialize(file, myObject);
    38.             file.Close();
    39.         }
    40.  
    41.         public void RemoveAll() {
    42.             this.listOfObjects = new List<Foo.MyObject>();
    43.             foreach (string filePath in System.IO.Directory.GetFiles(this.GetFileDirectory()))
    44.             {
    45.                 if (File.Exists(filePath))
    46.                 {
    47.                     File.Delete(filePath);
    48.                 }
    49.             }
    50.         }
    51.     }
    52. }
    Foo.FooMonoEditor

    Code (CSharp):
    1.  
    2. namespace Foo
    3. {
    4.     [CustomEditor(typeof(FooMono))]
    5.     public class FooMonoEditor : Editor
    6.     {
    7.  
    8.         public override void OnInspectorGUI()
    9.         {
    10.             FooMono foo = (FooMono)target;
    11.  
    12.             // --- Default inspector
    13.  
    14.             DrawDefaultInspector();
    15.  
    16.             // --- Action
    17.             if (GUILayout.Button("Load Foo"))
    18.             {
    19.                 foo.Load();
    20.             }
    21.             if (GUILayout.Button("Add Foo"))
    22.             {
    23.                 foo.Save(new Foo.MyObject(foo.listOfObjects.Count));
    24.                 foo.Load();
    25.             }
    26.             if (GUILayout.Button("Remove all"))
    27.             {
    28.                 foo.RemoveAll();
    29.             }
    30.  
    31.             // --- Review
    32.             for (int i = 0, l = foo.listOfObjects.Count; i < l; i++) {
    33.                 EditorGUILayout.LabelField(" - " + foo.listOfObjects[i].ID + " / " + foo.listOfObjects[i].GetType());
    34.             }
    35.         }
    36.  
    37.     }
    38. }
    From here, everything works very well:



    The Project: Bar

    Goal: The bar project implement a new version of `Foo.MyObject` into a `Bar.MyObject` to implement an arbitrary new field seed.

    Bar.MyObject Foo.MyObject extended object

    Code (CSharp):
    1.  
    2. namespace Bar
    3. {
    4.     [System.Serializable]
    5.     public class MyObject : Foo.MyObject
    6.     {
    7.         public int seed;
    8.  
    9.         public MyObject(int ID, int seed)
    10.             : base (ID) {
    11.             this.seed = seed;
    12.         }
    13.     }
    14. }
    Bar.BarMono Link to FooMono to implement Bar Project features

    Code (CSharp):
    1.  
    2. namespace Bar
    3. {
    4.     public class BarMono : MonoBehaviour
    5.     {
    6.         public Foo.FooMono fooMono; // Do not forgot to ref Foo Mono from editor
    7.     }
    8. }
    Bar.BarMonoEditor Allow to add Bar.MyObject into the List<Foo.MyObject> and try to cast all object into this list into Bar.MyObject.

    Code (CSharp):
    1.  
    2. namespace Bar
    3. {
    4.     [CustomEditor(typeof(BarMono))]
    5.     public class BarMonoEditor : Editor
    6.     {
    7.         public override void OnInspectorGUI()
    8.         {
    9.             BarMono bar = (BarMono)target;
    10.             Foo.FooMono foo = bar.fooMono;
    11.             // --- Default inspector
    12.  
    13.             DrawDefaultInspector();
    14.  
    15.             // --- Action
    16.             if (GUILayout.Button("Load Foo"))
    17.             {
    18.                 foo.Load();
    19.             }
    20.             if (GUILayout.Button("Add Bar"))
    21.             {
    22.                 foo.Save(new Bar.MyObject(foo.listOfObjects.Count, 123));
    23.                 foo.Load();
    24.             }
    25.             if (GUILayout.Button("Remove all"))
    26.             {
    27.                 foo.RemoveAll();
    28.             }
    29.  
    30.             // --- Review
    31.             for (int i = 0, l = foo.listOfObjects.Count; i < l; i++) {
    32.                 Bar.MyObject casted = (Bar.MyObject)foo.listOfObjects[i]; // << Error here
    33.                 EditorGUILayout.LabelField(" - " + casted.ID + " - " + casted.seed + " / " + casted.GetType());
    34.             }
    35.         }
    36.  
    37.     }
    38. }
    Results:

    - First: Casting is working well:

    If i click multiple times into BarMono [Add Bar] button, both handle it fine :



    - Second: Casting wrong type didn't work (as attended)

    If I click on FooMono [Add Foo] in order to create a new Foo.MyObject the go into BarMono who will try to cast it into Bar.MyObject (but can't) and I'll have for the first time the `InvalidCastException`:



    - Finally !! Casting Error on Editor auto-refresh.

    Now if I'm adding a `Bar.MyObject` into the list (it's ok). I can click on Load Foo (for FooMono, or BarMono) and both will Load files and cast a `Bar.MyObject` into the list.
    But when I save my code, the Bar.MyObject is casted into a Foo.MyObject and I got the `InvalidCastException`..

    I don't get why I got an issue here ?! What's going one with this ??

    PS: I don't know if it's change something but I got the same result when Bar.BarMono extend from Foo.FooMono and it's instantiate alone (instead of both instantiate with a reference from one to another)

    - A bad solution:

    I can fix it by adding a force Load into `OnValidate` who will fix the casting just before I got the error.

    Code (CSharp):
    1. private void OnValidate()
    2. {
    3.        this.Load();  
    4. }

    ---

    I'm very sorry for this long post but I recreate this all project to reproduce the issue and cannot do less ^^

    Thank for eveyone
     
    Last edited: Feb 4, 2020
  2. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    It's a bit difficult to follow your post and the overall idea, but the error makes sense. You cannot cast a "Foo.MyObject" to "Bar.MyObject" (given that the latter is a subtype of the former) if the actual instance is nothing more than a Foo.MyObject.
     
  3. ArthyShow

    ArthyShow

    Joined:
    Aug 6, 2016
    Posts:
    23

    Yep I know, this is what I test on the Second result case. I create a Foo.MyObject into listOfObjects and try to cast it into a Barr.MyObject. And this didn't works (as you say)..

    But then I create a Bar.MyObject into the list it's works fine too until unity auto-refresh and only at this point i got a casting error ( but my object was a Bar.MyObject) and when I Load again from file, it's re-working fine (so original object is a Bar.MyObject)
     
  4. ArthyShow

    ArthyShow

    Joined:
    Aug 6, 2016
    Posts:
    23
    I updated my post to avoid some part that i figure was useless on the concept.
     
  5. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Thanks, that's way less confusing and I guess I know what you were actually asking.

    When you create those instances, you save both to the list. The types are marked serializable, so is the generic list of it.
    What you observe is likely due to how Unity's serialization system works: it cannot serialize polymorphic types (unless you switch to the most recent version 2019.3+).

    The thing is, your list is declared as being List<Foo.Character> and that's what the serializer knows. It does not care which sort of instances are actually stored in the list, it only cares 1) whether Foo.Character is serializable and non-abstract 2) when that's true, it only cares about the "Foo.Character" part of any instance that's stored in it.

    Hence the information of instances that are a more specialized type are "cut off" during serialization, and during deserialization, it recovers lots of "Foo.Characters" even when they used to be of a more specialized type, such as "Bar.Characters".

    With the most recent versions mentioned above, you can actually serialize all sorts of instances correctly, and any references to such (with some limitations) are restored correctly.
    There are other ways to implement it though, one of them being ScriptableObjects or using a interim save format + serializer that takes care of the actual instance types.
     
    Last edited: Feb 5, 2020
  6. ArthyShow

    ArthyShow

    Joined:
    Aug 6, 2016
    Posts:
    23
    Okay thanks, I got it :)