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

Question ISerializationCallbackReceiver fails to get referenced ScriptableObject instances?

Discussion in 'Scripting' started by Ardenian, Sep 7, 2020.

  1. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    For my project in 2020.1.4.f1, I try to use the ISerializationCallbackReceiver interface to get a working dictionary at runtime. However, it seems that it fails to get the reference to a ScriptableObject when referencing it via an interface.

    In detail:
    • I have a list that references a bunch of MonoBehaviour components.
    • Each of these components in the list implements a generic interface that allows to retrieve a key object from that component (having the value and getting the right key to it for "faking" a KeyValuePair). This key object is always a ScriptableObject (basically allowing to have a flexible C# enumeration).
    • Then, in
      OnAfterDeserialize()
      , I would like to convert this list of components into a dictionary. Instead of using explicit types, I would like to use interfaces in the dictionary.
    • However, I keep getting the error message "FloatPropertyProvider: Trying to register an entity property that has no valid key.", which means that there is no ScriptableObject referenced, although I can clearly see in the inspector of the component that there is an instance referenced.
    • My first thought was that the ScriptableObject is not loaded, but we are not allowed to use
      Resources.Load(string)
      in serialization functions. Furthermore, strangely enough, increasing the size of the list in the inspector and thus having a duplicate component referenced will correctly show "FloatPropertyProvider: Trying to register an entity property that is already registered with this entity.", but now suddenly the serialization works and the previous error message about an invalid key does not show up anymore. Nonetheless, after saving changes in the IDE and recompiling, the same error about invalid key pops up again.
    Has someone an idea what is going on here?

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class EntityPropertyProvider<T> : MonoBehaviour, IPropertyProvider<T>, ISerializationCallbackReceiver
    6. {
    7.     [SerializeField]
    8.     private GameObject host = null;
    9.  
    10.     [SerializeField]
    11.     private List<EntityProperty<T>> properties = new List<EntityProperty<T>>();
    12.  
    13.     [NonSerialized]
    14.     private Dictionary<IEntityPropertyKey<T>, IEntityProperty<T>> table = new Dictionary<IEntityPropertyKey<T>, IEntityProperty<T>>();
    15.  
    16.     public bool TryGet(IEntityPropertyKey<T> key, out IEntityProperty<T> property)
    17.     {
    18.         return table.TryGetValue(key, out property);
    19.     }
    20.  
    21.     #region ISerializationCallbackReceiver
    22.     void ISerializationCallbackReceiver.OnAfterDeserialize()
    23.     {
    24.         if (table == null)
    25.         {
    26.             table = new Dictionary<IEntityPropertyKey<T>, IEntityProperty<T>>();
    27.         }
    28.         else
    29.         {
    30.             table.Clear();
    31.         }
    32.  
    33.         if (properties.Count > 0)
    34.         {
    35.             IEntityProperty<T> value;
    36.             for (int index = 0; index < properties.Count; index++)
    37.             {
    38.                 value = properties[index];
    39.                 if (value != null)
    40.                 {
    41.                     IEntityPropertyKey<T> key = value.Key;
    42.                     if (key == null)
    43.                     {
    44.                         Debug.LogWarning($"{this.GetType().Name}: Trying to register an entity property that has no valid key.");
    45.                     }
    46.                     else if (table.ContainsKey(key))
    47.                     {
    48.                         Debug.LogWarning($"{this.GetType().Name}: Trying to register an entity property that is already registered with this entity.");
    49.                     }
    50.                     else
    51.                     {
    52.                         table.Add(key, value);
    53.                     }
    54.                 }
    55.             }
    56.         }
    57.     }
    58.  
    59.     void ISerializationCallbackReceiver.OnBeforeSerialize()
    60.     {
    61.         properties.Clear();
    62.         if (host != null)
    63.         {
    64.             host.GetComponentsInChildren<EntityProperty<T>>(properties);
    65.         }
    66.     }
    67.     #endregion
    68. }

    EDIT: After some more testing, the key object is already null before returning it over the property and interface, as the debug statement before returning it logs into the console, although clearly, it is not null in the inspector:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. public abstract class EntityProperty : MonoBehaviour
    5. {
    6.  
    7. }
    8.  
    9. public abstract class EntityProperty<T> : EntityProperty, IEntityProperty<T>
    10. {
    11.     [SerializeField]
    12.     private EntityPropertyData<T> data = null;
    13.  
    14.     [Space]
    15.     [SerializeField]
    16.     private T value = default(T);
    17.  
    18.     public T Value { get => value; }
    19.  
    20.     IEntityPropertyKey<T> IEntityProperty<T>.Key
    21.     {
    22.         get
    23.         {
    24.             Debug.Log(data); // <-- this logs null although it is not null in the inspector
    25.             return data;
    26.         }
    27.     }
    28.  
    29.     T IValueProvider<T>.Value => value;
    30.  
    31.     private void OnValidate()
    32.     {
    33.         if (data == null)
    34.         {
    35.             var name = gameObject.name;
    36.             var data = Resources.Load<EntityPropertyData<T>>($"Property Data/{name} Property Data");
    37.             if (data != null)
    38.             {
    39.                 this.data = data;
    40.             }
    41.         }
    42.     }
    43. }
     
    Last edited: Sep 7, 2020
  2. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Here is a simplified example that comes with its own bug:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu]
    4. public class MyScriptableObject : ScriptableObject
    5. {
    6.  
    7. }

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class MyComponent : MonoBehaviour
    4. {
    5.     [SerializeField]
    6.     private MyScriptableObject obj = null;
    7.  
    8.     public MyScriptableObject Key { get => obj; }
    9. }

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class MyProvider : MonoBehaviour, ISerializationCallbackReceiver
    6. {
    7.     [SerializeField]
    8.     private List<MyComponent> list = new List<MyComponent>();
    9.  
    10.     [NonSerialized]
    11.     private Dictionary<MyScriptableObject, MyComponent> dict;
    12.  
    13.     void ISerializationCallbackReceiver.OnAfterDeserialize()
    14.     {
    15.         if (dict == null)
    16.         {
    17.             dict = new Dictionary<MyScriptableObject, MyComponent>();
    18.         }
    19.         else
    20.         {
    21.             dict.Clear();
    22.         }
    23.  
    24.         foreach (var value in list)
    25.         {
    26.             Debug.Log($"Component isNull={value == null}");
    27.  
    28.             if (value != null)
    29.             {
    30.                 var key = value.Key;
    31.                 if (key == null)
    32.                 {
    33.                     Debug.LogError("Key shouldn't be null here, should it.");
    34.                 }
    35.                 else if (dict.ContainsKey(key))
    36.                 {
    37.                     Debug.LogWarning("Dict already contains a component with this key.");
    38.                 }
    39.                 else
    40.                 {
    41.                     Debug.Log("Added item with key to dict.");
    42.                     dict.Add(key, value);
    43.                 }
    44.             }
    45.  
    46.         }
    47.     }
    48.  
    49.     void ISerializationCallbackReceiver.OnBeforeSerialize()
    50.     {
    51.         list.Clear();
    52.         gameObject.GetComponents<MyComponent>(list);
    53.     }
    54. }
    55. }

    editor-inspector.png

    In this example, the SO instance from the component is never
    null
    , but instead the component itself is
    null
    sometimes. This is how my console looks like after opening the editor:

    editor-console.png

    So what exactly is going on here?
     
    Last edited: Sep 7, 2020
  3. tranos

    tranos

    Joined:
    Feb 19, 2014
    Posts:
    180
    Last edited: Sep 7, 2020
  4. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I see, thank you. Is there any way to tell Unity to wait with this until all related UnityEngine.Object references are deserialized? How else am I supposed to access another referenced object if I don't know if it is already deserialized?
     
  5. tranos

    tranos

    Joined:
    Feb 19, 2014
    Posts:
    180
    @Ardenian Your problem is that you cannot get the Scriptable object via the interface? Is it because the interface is not visible in the editor maybe? You can use a base class that is visible in the editor.

    At Awake() everything is ready I believe.
    Sorry if I did not get your problem right.
     
  6. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    In the case provided in my opening post, I use ScriptableObject instances as one would C# enumeration values in other cases. I don't use a C# enumeration because they don't scale well (adding new values or changing existing values might invalidate the serialized enumeration values).

    What exactly is wrong in the first post eludes me, since it does not happen in the second post. However, my first idea about it being the interface does not seem to be the problem, since the key object (the SO instance) is already null before I cast it to the interface. You can see this in the snippet that I posted for
    EntityProperty<T>
    .

    I have three ideas what could be wrong:
    • It is the issue that you mentioned and the SO instance is not deserialized when I try to access it, resulting in
      null
      . However, this opens the question why this does not happen in the second example in my second post, where the component is null and not the key object from within the component, as it is in my first post.
    • Something goes wrong with using
      [SerializeField]
      and a specific specialization of the generic SO type. I noticed that Unity does not suggest instances of that type as it does with non-generic SO instances, so maybe that's the problem somewhere somehow.
    • The SO instance isn't loaded, which one does with
      Resources.Load(string)
      , but as far as I know, SO instances are always loaded in the editor.
    No clue how to solve any of those problems though.

    Awake()
    could be a solution, but shouldn't be because it is something completely else. I try to create the dictionary as part of the object deserialization process, whereas using
    Awake()
    would make the creation of the dictionary a part of the object initialization process. As similar as that looks like, they are fundamentally different things.

    Sure, the documentation does say:
    Nonetheless, I would like to avoid doing that in this particular case, because it is a different representation of already existing data and that's not something for the constructor, but part of the deserialization.
     
  7. Nstdspace

    Nstdspace

    Joined:
    Feb 6, 2020
    Posts:
    26
    For anyone else arriving here for the exact same reason, the explanation and maybe a fix for this problem can be found here.