Scriptable Object causes private fields to become null

	public abstract class Reward
	{
		public GameObject GameObject;
		public int Value;
	}
    public class Bundle : Reward
    {
        private HashSet<Item> UsedItems = new();

        public Bundle()
        {
            Debug.Log(UsedItems);
        }

        public void Test()
        {
            Debug.Log(UsedItems);
        }
    }

In the editor (outside of play mode) the Bundle object gets instantiated and added to a public List<Reward> inside a ScriptableObject (via a button).

If UsedItems is private, the log in the constructor logs it as initialized, but the log in Test logs it as null. If it’s public, it’s not null.

UsedItems is never assigned to anywhere else in the code, nor is its value serialized anywhere (according to Rider). I’ve tried adding non-serialized to the field, but that makes no difference.

The Bundle object itself is not null, I see it in the inspector and other functions work. This has been an ongoing struggle with Scriptable Objects.

For the SO to serialize your Reward items, they need to be flagged as [Serializable]. Like so:

[System.Serializable]
public class Bundle : Reward

By default custom serializable classes do NOT support polymorphism / inheritance as the serialization of such custom serializable classes is based on the field type. So when you define a List of “Reward” items, once deserialized you would loose the concrete types.

However Unity now supports the SerializeReference attribute which can be used on a field that holds a serializable class that is not a UnityEngine.Object derived type. SerializeReference generally changes how the data of the field is serialized. Here you get support for inheritance as Unity will serialize those instances in a separate section along with the actual instance type. However when using SerializeReference, Unity will not automatically initialize instances like it usually does as it now even supports “null” as a value. Of course when inheritance is used, Unity can’t know what class instance it should create for you. So when you want to create instances in the inspector, you need some custom inspector code to do that. However if you’re just interested in “viewing” the instances in the inspector, this should work out-of-the-box. Changing primitive values also works, you just can’t replace instances with different types without custom editor code.

2 Likes

I’ve tried with and without [Serializable] and it doesn’t change the behavior.

Do you know why it works with public fields, but not private ones? I tried adding [SerializeReference] to the list, but that just causes it to become null upon entering play mode (the list was previously serialized by Odin).

Initializing the list via inspector button is just:

            Rewards = new List<Reward>()
            {
                new Bundle()
            };

What do I need to do to make it work? The whole ScriptableObject workflow just seems really buggy… I always have to manually clear lists in the inspector before re-initializing/re-creating them and sometimes it just doesn’t work at all until I restart the editor.

EDIT:

Using both [Serializable] on the Reward class and [SerializeReference] on the list causes the List to not be null when entering play mode; but UsedItems is still null.

That does seem odd if you are using SerializeReference. I’d chalk this one up to Unity’s serialization system being very picky. You could make a bug report but I suspect it would end up as “Working as intended” simply due to the nature of the system. Can you instatiate your set within the constructor? It’s a long shot but still worth a try. Failing al of that your best bet is probably some kind of externally called Init method - a tale old as time when working around Unity’s serializer (and especially SOs)

I think this has nothing to do with scriptable objects, but actually to do with Odin Serialization - which was an important detail that should’ve been mentioned from post one.

The Odin serializer does not invoke constructors or inline initialisers when deserialising something. So the inline initialiser for private HashSet<Item> UsedItems = new(); isn’t going to be called, leaving it null.

1 Like

Yeah, I didn’t realize Odin was automatically serializing it until I saw the icon in Rider (I don’t have [OdinSerialize] on the list).

What I really forgot to mention is that it’s a SerializedScriptableObject (but I did try making it a regular SO with and without [System.Serialized] and [SerializeReference] and it still doesn’t work.

I’m still very confused here because:

  1. It still works if it’s public, but not private.
  2. If I use [SerializeReference], it’s serialized by Unity, not Odin.
  3. I have another public list in the Bundle class with an inline initializer that also works.

There’s no Odin serialization in the Reward classes. I did try initializing the hashset in the constructor and that yields the same result.

Can you post a full code example of where the behaviour happens. Both the scriptable object and the classes in question.

That would be about 1000 lines, but here’s a trimmed down version:

    [InlineEditor]
    [CreateAssetMenu(menuName = "ScriptableObjects/Battle")]
    public class Battle : SerializedScriptableObject
    {
        public List<Reward> Rewards = new();

        [Button("Add Test Rewards", ButtonSizes.Large), GUIColor(1f, 0f, 0.31f)]
        public void AddTestRewards()
        {
            Rewards = new List<Reward>()
            {
                new Bundle(3, 120)
            };
        }
    }
   [System.Serializable]
    public class Bundle : Reward
    {
        public int Options;
        public int TotalValue;
        public List<List<Reward>> FinalOptions = new();
        private HashSet<Item> UsedItems = new();

        public Bundle(int options, int totalValue)
        {
            Options = options;
            TotalValue = totalValue;
        }

        public void GetFinalOptions()
        {
            UsedItems.Clear();  // NullReferenceException here
            FinalOptions.Clear();  // This is properly initialized
            ...
        }
 [System.Serializable]
	public abstract class Reward
	{
		public GameObject GameObject;
		public int Value;
	}

I mean in that example its going to be serialized with Odin, and you will get the behaviour I mentioned.

If you want it serialized with Unity using [SerializeReference], then your Bundle class will need a public parameterless constructor, and Unity will use that to initialise an instance when deserialising. Otherwise it will make an uninitialised default instance, and inline initialisers won’t get called.

1 Like

OK, that works. I originally had two of the three pieces of the puzzle: [Serializable] and a parameterless constructor and then I removed them when I started troubleshooting this because they didn’t seem to be doing anything useful.

Adding [SerializeReference] in the SO was the final piece to make this work. Thanks @spiney199 and @Bunny83.

The one remaining mystery is why it works with Odin when it’s public, but not private.

Because if public, Odin will then serialize the HashSet. If private, it doesn’t.

1 Like

Sure enough, if I put [OdinSerialize] on the private hashset everything works, even without [Serializable], [SerializeReference] and parameterless constructor.