Search code examples
c#unity-game-engineserialization

How to serialise HashSet for usage in inspector


Unity version: 2017.3.1f1

I'm trying to work with a HashSet in the Unity inspector. Specifically, I need some MonoBehaviours to have HashSet fields which can have their contents modified via the inspector.

To achieve this goal, I've created a concrete class that subclasses HashSet, and uses a List internally for (de)serialisation, in a very similar manner to the Dictionary in this guide:

However I'm encountering an issue where the list displays in the inspector, but I cannot set more than 1 value within it. If I set the size of the list to 2 or greater, it immediately is set back to 1.

In an attempt to debug the problem, I found that the OnBeforeSerialize (and not OnAfterDeserialize) was being executed every frame, continuously resetting the value. I'm not sure why it was setting it to 1 though.

Note that if I entered a string into the 1 available slot, it would not be reset. So this approach is currently "functional" for a HashSet of 0 or 1 strings, but not more. Also, the outcome does not change if I use a HashSet field instead of inheriting from it (like what was done in the above link).

Here is a minimal example:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TEST : MonoBehaviour
{
    public StringHashSet test;
}

[System.Serializable]
public class SerializableHashSet<T> : HashSet<T>, ISerializationCallbackReceiver
{
    public List<T> values = new List<T> ();

    public void OnBeforeSerialize ()
    {
        values.Clear ();

        foreach (T val in this) {
            values.Add (val);
        }
    }

    public void OnAfterDeserialize ()
    {
        this.Clear ();

        foreach (T val in values) {
            this.Add (val);
        }
    }
}

[System.Serializable]
public class StringHashSet : SerializableHashSet<string>
{
}
  1. How can I get this working as expected (arbitrary sized list of strings being (de)serialized to a HashSet)?
  2. Also, why is OnBeforeSerialize being executed every frame, even if no changes are being made in the inspector?

More information


I've figured out it's because when the size of the list is changed, all the new elements in the list by default have the same value as the previous element, which will therefore all be squished to 1 value in the HashSet.

Thus while question 2 remains from above, question 1 has evolved to ask for a workaround for this while maintaining the desired HashSet functionality.


Solution

  • Here's how I've solved this problem considering the discoveries added to the More information section in the question:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [System.Serializable]
    public class SerializableHashSet<T> : HashSet<T>, ISerializationCallbackReceiver
    {
        public List<T> values = new List<T> ();
    
        public void OnBeforeSerialize ()
        {
            var cur = new HashSet<T> (values);
    
            foreach (var val in this) {
                if (!cur.Contains (val)) {
                    values.Add (val);
                }
            }
        }
    
        public void OnAfterDeserialize ()
        {
            this.Clear ();
    
            foreach (var val in values) {
                this.Add (val);
            }
        }
    }
    
    [System.Serializable]
    public class StringHashSet : SerializableHashSet<string>
    {
    }
    

    OnAfterDeserialize is identical.

    OnBeforeSerialize no longer clears the values list. Instead, it adds new values from the HashSet to the List -- specifically, values that exist in the HashSet but not in the List.

    This allows functionality to continue when new elements are added to the list in the inspector: duplicate and blank entries will work fine, because the list won't be cleared at any point, only added to.


    Regarding the second question, I found this information regarding why OnBeforeSerialize was being executed so frequently: http://answers.unity.com/answers/796853/view.html

    The relevant information:

    Called every frame if the inspector for the object is open in the editor