1

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.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Bilal Akil
  • 4,716
  • 5
  • 32
  • 52
  • The immediate question that comes to mind is, why do you need to use a HashSet? Could you get away with extending a `List` and overriding the Add function, eg `if (!this.Contains(value)) base.Add(value);`? – Immersive Apr 28 '18 at 07:38
  • `HashSet` has irrelevant ordering, constant time retrieval, and linear set equivalence comparison. I won't get these with a `List`. – Bilal Akil Apr 28 '18 at 12:02

2 Answers2

3

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

Bilal Akil
  • 4,716
  • 5
  • 32
  • 52
0

I had the need to have a unique list of objects in the Inspector too and came across this question, this is how I solved it:

Declare your List:

public List<T> list = new List<T>();

Create an OnValidate method:

   private void OnValidate() {
        int count = list.Count;

        // remove duplicates
        HashSet<T> uniqueList = new HashSet<T>(list);

        list= uniqueList.ToList();

        if (count > list.Count) {
            list.Add(null);
        }
    }

Hope that helps someone.

mcdixon
  • 61
  • 1
  • 3