Search Unity

Finally, a serializable dictionary for Unity! (extracted from System.Collections.Generic)

Discussion in 'Scripting' started by vexe, Jun 24, 2015.

  1. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Hey guys,

    so as all you all know Unity doesn't know how to serialize generic dictionaries. Since 4.6 we got the ISerializationCallbackReceiver which allows us to use custom serialization to serialize types that Unity cannot. However; when using this interface to serialize dictionaries it's not idea. For example, if you inherit Dictionary<TK, TV> and add the serialized key/values lists, you'll come across some really weird issues (most probably threading related) as seen here and here. Another approach is to use composition instead of inheritance, and let the dictionary object live within the serialized dictionary class alongside the serialized key/value lists, i.e.

    Code (csharp):
    1.  
    2. public class SerializableDictionary<TK, TV> : ISerializationCallbackReceiver
    3. {
    4.     private Dictionary<TK, TV> _Dictionary;
    5.     [SerializeField] List<TK> _Keys;
    6.     [SerializeField] List<TV> _Values;
    7.  
    8.     // wrapper methods, serialization, etc...
    9. }
    10.  
    instead of:
    Code (csharp):
    1.  
    2. public class SerializableDictionary<TK, TV> : Dictionary<TK, TV>, ISerializationCallbackReceiver
    3. {
    4.     [SerializeField] List<TK> _Keys;
    5.     [SerializeField] List<TV> _Values;
    6.  
    7.     // serialization, etc...
    8. }
    9.  
    But that also feels redundant to me, we're having to write wrappers and add two lists just to serialize...

    Wouldn't it be nice if the Dictionary class itself was serializable by Unity?

    Well that's what I did by basically extracting the Dictionary<TK, TV> code and marking the necessary fields with [SerializeField] - And slightly refactored/cleaned it up (renamed the fields, removed the implementation of the non-generic interfaces, and factored out the 'hashCode', 'next', 'key', and 'value' from the 'Entry' class to be arrays instead)

    One bit I left commented was the "IsWellKnownEqualityComparer" as I wasn't really sure about it - I did some tests and it didn't seem to affect anything. If you know for a fact it's critical let me know.

    Of course, you'd still have to subclass, i.e.

    Code (csharp):
    1.  
    2. [Serializable]
    3. public class MyDictionary : SerializableDictionary<string, int> { }
    4.  
    5. public class Test : MonoBehaviour
    6. {
    7.     public MyDictionary dictionary;
    8. }
    9.  
    I've also added an indexer that accepts a default value to return if the key wasn't found. As well as an AsDictionary property which is mainly for debugging reasons.

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Linq;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6. using System.Diagnostics;
    7. using UnityEngine;
    8.  
    9. [Serializable, DebuggerDisplay("Count = {Count}")]
    10. public class SerializableDictionary<TKey, TValue> : IDictionary<TKey, TValue>
    11. {
    12.     [SerializeField, HideInInspector] int[] _Buckets;
    13.     [SerializeField, HideInInspector] int[] _HashCodes;
    14.     [SerializeField, HideInInspector] int[] _Next;
    15.     [SerializeField, HideInInspector] int _Count;
    16.     [SerializeField, HideInInspector] int _Version;
    17.     [SerializeField, HideInInspector] int _FreeList;
    18.     [SerializeField, HideInInspector] int _FreeCount;
    19.     [SerializeField, HideInInspector] TKey[] _Keys;
    20.     [SerializeField, HideInInspector] TValue[] _Values;
    21.  
    22.     readonly IEqualityComparer<TKey> _Comparer;
    23.  
    24.     // Mainly for debugging purposes - to get the key-value pairs display
    25.     public Dictionary<TKey, TValue> AsDictionary
    26.     {
    27.         get { return new Dictionary<TKey, TValue>(this); }
    28.     }
    29.  
    30.     public int Count
    31.     {
    32.         get { return _Count - _FreeCount; }
    33.     }
    34.  
    35.     public TValue this[TKey key, TValue defaultValue]
    36.     {
    37.         get
    38.         {
    39.             int index = FindIndex(key);
    40.             if (index >= 0)
    41.                 return _Values[index];
    42.             return defaultValue;
    43.         }
    44.     }
    45.  
    46.     public TValue this[TKey key]
    47.     {
    48.         get
    49.         {
    50.             int index = FindIndex(key);
    51.             if (index >= 0)
    52.                 return _Values[index];
    53.             throw new KeyNotFoundException(key.ToString());
    54.         }
    55.  
    56.         set { Insert(key, value, false); }
    57.     }
    58.  
    59.     public SerializableDictionary()
    60.         : this(0, null)
    61.     {
    62.     }
    63.  
    64.     public SerializableDictionary(int capacity)
    65.         : this(capacity, null)
    66.     {
    67.     }
    68.  
    69.     public SerializableDictionary(IEqualityComparer<TKey> comparer)
    70.         : this(0, comparer)
    71.     {
    72.     }
    73.  
    74.     public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer)
    75.     {
    76.         if (capacity < 0)
    77.             throw new ArgumentOutOfRangeException("capacity");
    78.  
    79.         Initialize(capacity);
    80.  
    81.         _Comparer = (comparer ?? EqualityComparer<TKey>.Default);
    82.     }
    83.  
    84.     public SerializableDictionary(IDictionary<TKey, TValue> dictionary)
    85.         : this(dictionary, null)
    86.     {
    87.     }
    88.  
    89.     public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
    90.         : this((dictionary != null) ? dictionary.Count : 0, comparer)
    91.     {
    92.         if (dictionary == null)
    93.             throw new ArgumentNullException("dictionary");
    94.  
    95.         foreach (KeyValuePair<TKey, TValue> current in dictionary)
    96.             Add(current.Key, current.Value);
    97.     }
    98.  
    99.     public bool ContainsValue(TValue value)
    100.     {
    101.         if (value == null)
    102.         {
    103.             for (int i = 0; i < _Count; i++)
    104.             {
    105.                 if (_HashCodes[i] >= 0 && _Values[i] == null)
    106.                     return true;
    107.             }
    108.         }
    109.         else
    110.         {
    111.             var defaultComparer = EqualityComparer<TValue>.Default;
    112.             for (int i = 0; i < _Count; i++)
    113.             {
    114.                 if (_HashCodes[i] >= 0 && defaultComparer.Equals(_Values[i], value))
    115.                     return true;
    116.             }
    117.         }
    118.         return false;
    119.     }
    120.  
    121.     public bool ContainsKey(TKey key)
    122.     {
    123.         return FindIndex(key) >= 0;
    124.     }
    125.  
    126.     public void Clear()
    127.     {
    128.         if (_Count <= 0)
    129.             return;
    130.  
    131.         for (int i = 0; i < _Buckets.Length; i++)
    132.             _Buckets[i] = -1;
    133.  
    134.         Array.Clear(_Keys, 0, _Count);
    135.         Array.Clear(_Values, 0, _Count);
    136.         Array.Clear(_HashCodes, 0, _Count);
    137.         Array.Clear(_Next, 0, _Count);
    138.  
    139.         _FreeList = -1;
    140.         _Count = 0;
    141.         _FreeCount = 0;
    142.         _Version++;
    143.     }
    144.  
    145.     public void Add(TKey key, TValue value)
    146.     {
    147.         Insert(key, value, true);
    148.     }
    149.  
    150.     private void Resize(int newSize, bool forceNewHashCodes)
    151.     {
    152.         int[] bucketsCopy = new int[newSize];
    153.         for (int i = 0; i < bucketsCopy.Length; i++)
    154.             bucketsCopy[i] = -1;
    155.  
    156.         var keysCopy = new TKey[newSize];
    157.         var valuesCopy = new TValue[newSize];
    158.         var hashCodesCopy = new int[newSize];
    159.         var nextCopy = new int[newSize];
    160.  
    161.         Array.Copy(_Values, 0, valuesCopy, 0, _Count);
    162.         Array.Copy(_Keys, 0, keysCopy, 0, _Count);
    163.         Array.Copy(_HashCodes, 0, hashCodesCopy, 0, _Count);
    164.         Array.Copy(_Next, 0, nextCopy, 0, _Count);
    165.  
    166.         if (forceNewHashCodes)
    167.         {
    168.             for (int i = 0; i < _Count; i++)
    169.             {
    170.                 if (hashCodesCopy[i] != -1)
    171.                     hashCodesCopy[i] = (_Comparer.GetHashCode(keysCopy[i]) & 2147483647);
    172.             }
    173.         }
    174.  
    175.         for (int i = 0; i < _Count; i++)
    176.         {
    177.             int index = hashCodesCopy[i] % newSize;
    178.             nextCopy[i] = bucketsCopy[index];
    179.             bucketsCopy[index] = i;
    180.         }
    181.  
    182.         _Buckets = bucketsCopy;
    183.         _Keys = keysCopy;
    184.         _Values = valuesCopy;
    185.         _HashCodes = hashCodesCopy;
    186.         _Next = nextCopy;
    187.     }
    188.  
    189.     private void Resize()
    190.     {
    191.         Resize(PrimeHelper.ExpandPrime(_Count), false);
    192.     }
    193.  
    194.     public bool Remove(TKey key)
    195.     {
    196.         if (key == null)
    197.             throw new ArgumentNullException("key");
    198.  
    199.         int hash = _Comparer.GetHashCode(key) & 2147483647;
    200.         int index = hash % _Buckets.Length;
    201.         int num = -1;
    202.         for (int i = _Buckets[index]; i >= 0; i = _Next[i])
    203.         {
    204.             if (_HashCodes[i] == hash && _Comparer.Equals(_Keys[i], key))
    205.             {
    206.                 if (num < 0)
    207.                     _Buckets[index] = _Next[i];
    208.                 else
    209.                     _Next[num] = _Next[i];
    210.  
    211.                 _HashCodes[i] = -1;
    212.                 _Next[i] = _FreeList;
    213.                 _Keys[i] = default(TKey);
    214.                 _Values[i] = default(TValue);
    215.                 _FreeList = i;
    216.                 _FreeCount++;
    217.                 _Version++;
    218.                 return true;
    219.             }
    220.             num = i;
    221.         }
    222.         return false;
    223.     }
    224.  
    225.     private void Insert(TKey key, TValue value, bool add)
    226.     {
    227.         if (key == null)
    228.             throw new ArgumentNullException("key");
    229.  
    230.         if (_Buckets == null)
    231.             Initialize(0);
    232.  
    233.         int hash = _Comparer.GetHashCode(key) & 2147483647;
    234.         int index = hash % _Buckets.Length;
    235.         int num1 = 0;
    236.         for (int i = _Buckets[index]; i >= 0; i = _Next[i])
    237.         {
    238.             if (_HashCodes[i] == hash && _Comparer.Equals(_Keys[i], key))
    239.             {
    240.                 if (add)
    241.                     throw new ArgumentException("Key already exists: " + key);
    242.  
    243.                 _Values[i] = value;
    244.                 _Version++;
    245.                 return;
    246.             }
    247.             num1++;
    248.         }
    249.         int num2;
    250.         if (_FreeCount > 0)
    251.         {
    252.             num2 = _FreeList;
    253.             _FreeList = _Next[num2];
    254.             _FreeCount--;
    255.         }
    256.         else
    257.         {
    258.             if (_Count == _Keys.Length)
    259.             {
    260.                 Resize();
    261.                 index = hash % _Buckets.Length;
    262.             }
    263.             num2 = _Count;
    264.             _Count++;
    265.         }
    266.         _HashCodes[num2] = hash;
    267.         _Next[num2] = _Buckets[index];
    268.         _Keys[num2] = key;
    269.         _Values[num2] = value;
    270.         _Buckets[index] = num2;
    271.         _Version++;
    272.  
    273.         //if (num3 > 100 && HashHelpers.IsWellKnownEqualityComparer(comparer))
    274.         //{
    275.         //    comparer = (IEqualityComparer<TKey>)HashHelpers.GetRandomizedEqualityComparer(comparer);
    276.         //    Resize(entries.Length, true);
    277.         //}
    278.     }
    279.  
    280.     private void Initialize(int capacity)
    281.     {
    282.         int prime = PrimeHelper.GetPrime(capacity);
    283.  
    284.         _Buckets = new int[prime];
    285.         for (int i = 0; i < _Buckets.Length; i++)
    286.             _Buckets[i] = -1;
    287.  
    288.         _Keys = new TKey[prime];
    289.         _Values = new TValue[prime];
    290.         _HashCodes = new int[prime];
    291.         _Next = new int[prime];
    292.  
    293.         _FreeList = -1;
    294.     }
    295.  
    296.     private int FindIndex(TKey key)
    297.     {
    298.         if (key == null)
    299.             throw new ArgumentNullException("key");
    300.  
    301.         if (_Buckets != null)
    302.         {
    303.             int hash = _Comparer.GetHashCode(key) & 2147483647;
    304.             for (int i = _Buckets[hash % _Buckets.Length]; i >= 0; i = _Next[i])
    305.             {
    306.                 if (_HashCodes[i] == hash && _Comparer.Equals(_Keys[i], key))
    307.                     return i;
    308.             }
    309.         }
    310.         return -1;
    311.     }
    312.  
    313.     public bool TryGetValue(TKey key, out TValue value)
    314.     {
    315.         int index = FindIndex(key);
    316.         if (index >= 0)
    317.         {
    318.             value = _Values[index];
    319.             return true;
    320.         }
    321.         value = default(TValue);
    322.         return false;
    323.     }
    324.  
    325.     private static class PrimeHelper
    326.     {
    327.         public static readonly int[] Primes = new int[]
    328.         {
    329.             3,
    330.             7,
    331.             11,
    332.             17,
    333.             23,
    334.             29,
    335.             37,
    336.             47,
    337.             59,
    338.             71,
    339.             89,
    340.             107,
    341.             131,
    342.             163,
    343.             197,
    344.             239,
    345.             293,
    346.             353,
    347.             431,
    348.             521,
    349.             631,
    350.             761,
    351.             919,
    352.             1103,
    353.             1327,
    354.             1597,
    355.             1931,
    356.             2333,
    357.             2801,
    358.             3371,
    359.             4049,
    360.             4861,
    361.             5839,
    362.             7013,
    363.             8419,
    364.             10103,
    365.             12143,
    366.             14591,
    367.             17519,
    368.             21023,
    369.             25229,
    370.             30293,
    371.             36353,
    372.             43627,
    373.             52361,
    374.             62851,
    375.             75431,
    376.             90523,
    377.             108631,
    378.             130363,
    379.             156437,
    380.             187751,
    381.             225307,
    382.             270371,
    383.             324449,
    384.             389357,
    385.             467237,
    386.             560689,
    387.             672827,
    388.             807403,
    389.             968897,
    390.             1162687,
    391.             1395263,
    392.             1674319,
    393.             2009191,
    394.             2411033,
    395.             2893249,
    396.             3471899,
    397.             4166287,
    398.             4999559,
    399.             5999471,
    400.             7199369
    401.         };
    402.  
    403.         public static bool IsPrime(int candidate)
    404.         {
    405.             if ((candidate & 1) != 0)
    406.             {
    407.                 int num = (int)Math.Sqrt((double)candidate);
    408.                 for (int i = 3; i <= num; i += 2)
    409.                 {
    410.                     if (candidate % i == 0)
    411.                     {
    412.                         return false;
    413.                     }
    414.                 }
    415.                 return true;
    416.             }
    417.             return candidate == 2;
    418.         }
    419.  
    420.         public static int GetPrime(int min)
    421.         {
    422.             if (min < 0)
    423.                 throw new ArgumentException("min < 0");
    424.  
    425.             for (int i = 0; i < PrimeHelper.Primes.Length; i++)
    426.             {
    427.                 int prime = PrimeHelper.Primes[i];
    428.                 if (prime >= min)
    429.                     return prime;
    430.             }
    431.             for (int i = min | 1; i < 2147483647; i += 2)
    432.             {
    433.                 if (PrimeHelper.IsPrime(i) && (i - 1) % 101 != 0)
    434.                     return i;
    435.             }
    436.             return min;
    437.         }
    438.  
    439.         public static int ExpandPrime(int oldSize)
    440.         {
    441.             int num = 2 * oldSize;
    442.             if (num > 2146435069 && 2146435069 > oldSize)
    443.             {
    444.                 return 2146435069;
    445.             }
    446.             return PrimeHelper.GetPrime(num);
    447.         }
    448.     }
    449.  
    450.    public ICollection<TKey> Keys
    451.    {
    452.         get { return _Keys.Take(Count).ToArray(); }
    453.    }
    454.  
    455.    public ICollection<TValue> Values
    456.    {
    457.         get { return _Values.Take(Count).ToArray(); }
    458.    }
    459.  
    460.     public void Add(KeyValuePair<TKey, TValue> item)
    461.     {
    462.         Add(item.Key, item.Value);
    463.     }
    464.  
    465.     public bool Contains(KeyValuePair<TKey, TValue> item)
    466.     {
    467.         int index = FindIndex(item.Key);
    468.         return index >= 0 &&
    469.             EqualityComparer<TValue>.Default.Equals(_Values[index], item.Value);
    470.     }
    471.  
    472.     public void CopyTo(KeyValuePair<TKey, TValue>[] array, int index)
    473.     {
    474.         if (array == null)
    475.             throw new ArgumentNullException("array");
    476.  
    477.         if (index < 0 || index > array.Length)
    478.             throw new ArgumentOutOfRangeException(string.Format("index = {0} array.Length = {1}", index, array.Length));
    479.  
    480.         if (array.Length - index < Count)
    481.             throw new ArgumentException(string.Format("The number of elements in the dictionary ({0}) is greater than the available space from index to the end of the destination array {1}.", Count, array.Length));
    482.  
    483.         for (int i = 0; i < _Count; i++)
    484.         {
    485.             if (_HashCodes[i] >= 0)
    486.                 array[index++] = new KeyValuePair<TKey, TValue>(_Keys[i], _Values[i]);
    487.         }
    488.     }
    489.  
    490.     public bool IsReadOnly
    491.     {
    492.         get { return false; }
    493.     }
    494.  
    495.     public bool Remove(KeyValuePair<TKey, TValue> item)
    496.     {
    497.         return Remove(item.Key);
    498.     }
    499.  
    500.     public Enumerator GetEnumerator()
    501.     {
    502.         return new Enumerator(this);
    503.     }
    504.  
    505.     IEnumerator IEnumerable.GetEnumerator()
    506.     {
    507.         return GetEnumerator();
    508.     }
    509.  
    510.     IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
    511.     {
    512.         return GetEnumerator();
    513.     }
    514.  
    515.     public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
    516.     {
    517.         private readonly SerializableDictionary<TKey, TValue> _Dictionary;
    518.         private int _Version;
    519.         private int _Index;
    520.         private KeyValuePair<TKey, TValue> _Current;
    521.  
    522.         public KeyValuePair<TKey, TValue> Current
    523.         {
    524.             get { return _Current; }
    525.         }
    526.  
    527.         internal Enumerator(SerializableDictionary<TKey, TValue> dictionary)
    528.         {
    529.             _Dictionary = dictionary;
    530.             _Version = dictionary._Version;
    531.             _Current = default(KeyValuePair<TKey, TValue>);
    532.             _Index = 0;
    533.         }
    534.  
    535.         public bool MoveNext()
    536.         {
    537.             if (_Version != _Dictionary._Version)
    538.                 throw new InvalidOperationException(string.Format("Enumerator version {0} != Dictionary version {1}", _Version, _Dictionary._Version));
    539.  
    540.             while (_Index < _Dictionary._Count)
    541.             {
    542.                 if (_Dictionary._HashCodes[_Index] >= 0)
    543.                 {
    544.                     _Current = new KeyValuePair<TKey, TValue>(_Dictionary._Keys[_Index], _Dictionary._Values[_Index]);
    545.                     _Index++;
    546.                     return true;
    547.                 }
    548.                 _Index++;
    549.             }
    550.  
    551.             _Index = _Dictionary._Count + 1;
    552.             _Current = default(KeyValuePair<TKey, TValue>);
    553.             return false;
    554.         }
    555.  
    556.         void IEnumerator.Reset()
    557.         {
    558.             if (_Version != _Dictionary._Version)
    559.                 throw new InvalidOperationException(string.Format("Enumerator version {0} != Dictionary version {1}", _Version, _Dictionary._Version));
    560.  
    561.             _Index = 0;
    562.             _Current = default(KeyValuePair<TKey, TValue>);
    563.         }
    564.  
    565.         object IEnumerator.Current
    566.         {
    567.             get { return Current; }
    568.         }
    569.  
    570.         public void Dispose()
    571.         {
    572.         }
    573.     }
    574. }
    A property drawer will come soon!

    Cheers!
     
    Last edited: Jun 24, 2015
  2. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    So here's a drawer, not the best, but hey it works!



    - Does not support reference types as keys (except for strings)
    - You'd have to add a new property drawer for each dictionary type you make (see the bottom of the script)
    - The drawer is open for extension by community, please feel free to improve it!
    - Again, for better dictionary inspection see my framework. I offer per key/value attribute annotation!

    Code (csharp):
    1.  
    2. [Serializable] public class MyDictionary1 : SerializableDictionary<string, int> { }
    3. [Serializable] public class MyDictionary2 : SerializableDictionary<KeyCode, GameObject> { }
    4.  
    5. public class Test : MonoBehaviour
    6. {
    7.   public MyDictionary1 dictionary1;
    8.   public MyDictionary2 dictionary2;
    9. }
    10.  
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEngine;
    5. using UnityObject = UnityEngine.Object;
    6.  
    7. public abstract class DictionaryDrawer<TK, TV> : PropertyDrawer
    8. {
    9.     private SerializableDictionary<TK, TV> _Dictionary;
    10.     private bool _Foldout;
    11.     private const float kButtonWidth = 18f;
    12.  
    13.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    14.     {
    15.         CheckInitialize(property, label);
    16.         if (_Foldout)
    17.             return (_Dictionary.Count + 1) * 17f;
    18.         return 17f;
    19.     }
    20.  
    21.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    22.     {
    23.         CheckInitialize(property, label);
    24.  
    25.         position.height = 17f;
    26.  
    27.         var foldoutRect = position;
    28.         foldoutRect.width -= 2 * kButtonWidth;
    29.         EditorGUI.BeginChangeCheck();
    30.         _Foldout = EditorGUI.Foldout(foldoutRect, _Foldout, label, true);
    31.         if (EditorGUI.EndChangeCheck())
    32.             EditorPrefs.SetBool(label.text, _Foldout);
    33.  
    34.         var buttonRect = position;
    35.         buttonRect.x = position.width - kButtonWidth + position.x;
    36.         buttonRect.width = kButtonWidth + 2;
    37.  
    38.         if (GUI.Button(buttonRect, new GUIContent("+", "Add item"), EditorStyles.miniButton))
    39.         {
    40.             AddNewItem();
    41.         }
    42.  
    43.         buttonRect.x -= kButtonWidth;
    44.  
    45.         if (GUI.Button(buttonRect, new GUIContent("X", "Clear dictionary"), EditorStyles.miniButtonRight))
    46.         {
    47.             ClearDictionary();
    48.         }
    49.  
    50.         if (!_Foldout)
    51.             return;
    52.  
    53.         foreach (var item in _Dictionary)
    54.         {
    55.             var key = item.Key;
    56.             var value = item.Value;
    57.  
    58.             position.y += 17f;
    59.  
    60.             var keyRect = position;
    61.             keyRect.width /= 2;
    62.             keyRect.width -= 4;
    63.             EditorGUI.BeginChangeCheck();
    64.             var newKey = DoField(keyRect, typeof(TK), key);
    65.             if (EditorGUI.EndChangeCheck())
    66.             {
    67.                 try
    68.                 {
    69.                     _Dictionary.Remove(key);
    70.                     _Dictionary.Add(newKey, value);
    71.                 }
    72.                 catch(Exception e)
    73.                 {
    74.                     Debug.Log(e.Message);
    75.                 }
    76.                 break;
    77.             }
    78.  
    79.             var valueRect = position;
    80.             valueRect.x = position.width / 2 + 15;
    81.             valueRect.width = keyRect.width - kButtonWidth;
    82.             EditorGUI.BeginChangeCheck();
    83.             value = DoField(valueRect, typeof(TV), value);
    84.             if (EditorGUI.EndChangeCheck())
    85.             {
    86.                 _Dictionary[key] = value;
    87.                 break;
    88.             }
    89.  
    90.             var removeRect = valueRect;
    91.             removeRect.x = valueRect.xMax + 2;
    92.             removeRect.width = kButtonWidth;
    93.             if (GUI.Button(removeRect, new GUIContent("x", "Remove item"), EditorStyles.miniButtonRight))
    94.             {
    95.                 RemoveItem(key);
    96.                 break;
    97.             }
    98.         }
    99.     }
    100.  
    101.     private void RemoveItem(TK key)
    102.     {
    103.         _Dictionary.Remove(key);
    104.     }
    105.  
    106.     private void CheckInitialize(SerializedProperty property, GUIContent label)
    107.     {
    108.         if (_Dictionary == null)
    109.         {
    110.             var target = property.serializedObject.targetObject;
    111.             _Dictionary = fieldInfo.GetValue(target) as SerializableDictionary<TK, TV>;
    112.             if (_Dictionary == null)
    113.             {
    114.                 _Dictionary = new SerializableDictionary<TK, TV>();
    115.                 fieldInfo.SetValue(target, _Dictionary);
    116.             }
    117.  
    118.             _Foldout = EditorPrefs.GetBool(label.text);
    119.         }
    120.     }
    121.  
    122.     private static readonly Dictionary<Type, Func<Rect, object, object>> _Fields =
    123.         new Dictionary<Type,Func<Rect,object,object>>()
    124.         {
    125.             { typeof(int), (rect, value) => EditorGUI.IntField(rect, (int)value) },
    126.             { typeof(float), (rect, value) => EditorGUI.FloatField(rect, (float)value) },
    127.             { typeof(string), (rect, value) => EditorGUI.TextField(rect, (string)value) },
    128.             { typeof(bool), (rect, value) => EditorGUI.Toggle(rect, (bool)value) },
    129.             { typeof(Vector2), (rect, value) => EditorGUI.Vector2Field(rect, GUIContent.none, (Vector2)value) },
    130.             { typeof(Vector3), (rect, value) => EditorGUI.Vector3Field(rect, GUIContent.none, (Vector3)value) },
    131.             { typeof(Bounds), (rect, value) => EditorGUI.BoundsField(rect, (Bounds)value) },
    132.             { typeof(Rect), (rect, value) => EditorGUI.RectField(rect, (Rect)value) },
    133.         };
    134.  
    135.     private static T DoField<T>(Rect rect, Type type, T value)
    136.     {
    137.         Func<Rect, object, object> field;
    138.         if (_Fields.TryGetValue(type, out field))
    139.             return (T)field(rect, value);
    140.  
    141.         if (type.IsEnum)
    142.             return (T)(object)EditorGUI.EnumPopup(rect, (Enum)(object)value);
    143.  
    144.         if (typeof(UnityObject).IsAssignableFrom(type))
    145.             return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true);
    146.  
    147.         Debug.Log("Type is not supported: " + type);
    148.         return value;
    149.     }
    150.  
    151.     private void ClearDictionary()
    152.     {
    153.         _Dictionary.Clear();
    154.     }
    155.  
    156.     private void AddNewItem()
    157.     {
    158.         TK key;
    159.         if (typeof(TK) == typeof(string))
    160.             key = (TK)(object)"";
    161.         else key = default(TK);
    162.  
    163.         var value = default(TV);
    164.         try
    165.         {
    166.             _Dictionary.Add(key, value);
    167.         }
    168.         catch(Exception e)
    169.         {
    170.             Debug.Log(e.Message);
    171.         }
    172.     }
    173. }
    174.  
    175. [CustomPropertyDrawer(typeof(MyDictionary1))]
    176. public class MyDictionaryDrawer1 : DictionaryDrawer<string, int> { }
    177.  
    178. [CustomPropertyDrawer(typeof(MyDictionary2))]
    179. public class MyDictionaryDrawer2 : DictionaryDrawer<KeyCode, GameObject> { }
     
  3. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    So returning the keys/values directly in the Keys/Values property is not a good idea since their size is equal to the dictionary capacity, and not Count - So I'm returning copies of them taking only 'Count' elements.
     
    jeffersonrcgouveia likes this.
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Is there a newer version of unity that is supporting serializing generic classes now?

    Unity 5 maybe (I'm still on Unity 4.6.1f1)?


    [edit]
    ignore that, you answer my question in there. Just didn't read it all...
     
  5. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    This is EXCELLENT. Well done!

    I had been using custom classes similar to the first two. Thanks very much!
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    I haven't read through the class thoroughly yet.

    But couldn't a lot of the hash information be generated on deserialize? Implement ISerializationCallbackReceiver and do the leg work there. Reducing the need for all those fields being serialized, thus reducing the serialized size of the dictionary.

    The keys and values are still in paired indexes in their respective arrays. So really... the pairing is implied just by that, meaning really only those 2 arrays would have to be serialized. Everything else can be recalculated based on that assumption.

    Also, the IEqualityComparer, since it's readonly, becomes useless. It can't be serialized, and can only be set during the constructor. This means it's going to always be null after unity deserializes it. Either it should be removed, or allow it to be changed, so the user can set the comparer at any time (after deserialization).
     
    Last edited: Jun 24, 2015
  7. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @lordofduct you're right about IComparer, just something I forgot about. One thing we could do besides making it public is add a serialized string and assign it the assembly qualified name of the user-specified comparer - Then we could write something like:

    Code (csharp):
    1.  
    2. [SerializeField] string _comparerTypeName;
    3. private IEqualityComparer<TK> _comparer;
    4. private IEqualityComparer<TK> _Comparer
    5. {
    6.        set { _comparer = value; }
    7.        get {
    8.             if (_comparer == null)
    9.             {
    10.                 if (string.IsNullOrEmpty(_comparerTypeName))
    11.                     _comparer = IEqualityComparer<TK>.Default;
    12.                 else _comparer = Activator.CreateInstance(Type.GetType(_comparerTypeName)) as IEqualityComparer<TK>
    13.             }
    14.             return _comparer;
    15.        }
    16. }
    17.  
    18. public ctor (IEqualityComparer comparer..)
    19. {
    20.       Initialize .. etc
    21.       if (comparer == null)
    22.       {
    23.            _comparerTypeName = null;
    24.             _Comparer = EqualityComparer<TKey>.Default;
    25.       }
    26.       else
    27.       {
    28.            _comparerTypeName = comparer.GetType().AssemblyQualifiedName;
    29.             _Comparer = comparer;
    30.       }
    31. }
    32.  
    You know something along those lines could work...

    About serializing the fields, it would be ideal to serialize only the keys/values yes.But the point was not to use ISerializationCallbackReceiver avoiding the problems mentioned in the links first mentioned in the first page ^
    But I will give it a try, one thing though we'd have to be more familiar with the code to see how we'd construct the rest of the data purely from the keys/values, like the _Next array for example.
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Yeah, you can do that.

    That's actually the basis behind my 'TypeReference' class:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/TypeReference.cs
    propertydrawer:
    https://github.com/lordofduct/space...ase/Inspectors/TypeReferencePropertyDrawer.cs

    As for the links you refer to. What's going on there is a threading issue combined with the serializer. And locks won't work because the unity serializer doesn't attempt locks (a lock doesn't prevent access to an object, it causes another call to lock on the same object to block). The classes in those examples inherit from their respective types (Dictionary, HashSet, etc). These classes are flagged in the .Net/Mono framework as being serializable with the very same SerializableAttribute. This tells unity to go through the fields of that class and check if they need to be serialized. SO, while your callback is going on it might be looking into the fields of the parent class... here's the thing. Unity uses the System.Serializable and the System.NonSerialized attributes, but it ALSO uses its special UnityEngine.SerializeField attribute. So Dictionary and HashSet don't use this attribute... and here's the annoying factor. Unity SAYS that private fields aren't serialized, and that you don't have to tag them with the System.NonSerializedAttribute... but they fail to mention they still touch those fields, for whatever reason. And since .Net/mono has them flagged for .Net/mono style serialization, they're NOT set as System.NonSerialized.

    In your class though, those fields you have direct control over. You can flag them as System.NonSerialized, which signals to the unity serializer to not even bother with them at all. So they won't get touched during the callback.

    Of course, you may want to continue serializing those fields for speed sake. Recalculating on the callbacks creates that overhead. And since any call to things like 'Instantiate' actually perform a serialize/deserialize, this means any time you clone a GameObject with a script that has this dictionary on it, it takes on that overhead. So it's about balancing the speed vs the memory...


    Honestly, I REALLY hate the way that unity implemented there serialization callback. It still relies on fields in the object. It means that you now need a field for the actual value, and a field for the serialized representation of it, that persists through runtime after serialization.

    If they just implemented a serialization callback more similar to the .Net ISerializable interface, where they hand you a representation of the serialized data that can have entries read and wrote to, all of this would be avoided.

    I have this feeling that just like a lot of their other design choices that they think lay users may find this confusing or hard to use. But as a result we end up with a system that has "magic" behaviour, and memory overhead that the lay user is mostly left ignorant of.
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Oh, and I have ran into these same exceptions as well. So in the cases where it can't be avoided, you have to use it and you don't have full control of everything. Here's what I do.

    1) create wrappers when possible, I know... this may sound annoying, but do it where it's not a total chaos fest.

    2) ALWAYS mark things SerializeField or System.NonSerialized, this is regardless of using the ISerializationCallbackReceiver interface. Just ALWAYS do it, private/public/internal/protected fields alike. Be VERY explicit about your intent, the serializer seems to obey these attributes adequately.

    3) Modifying fields flagged as not serialized, you're pretty safe to do whatever with them (as long as they themselves don't access the unity API). But when modifying serialized properties, do them in one fail swoop. Don't enumerate over a field NOT flagged as NonSerialized, use arrays that are just set.

    For example this is how I'd serialize a dictionary:

    Code (csharp):
    1.  
    2.     void ISerializationCallbackReceiver.OnBeforeSerialize()
    3.     {
    4.         _keys = this.Keys.ToArray();
    5.         _values = this.Values.ToArray();
    6.     }
    7.  
    Where _keys and _values are arrays.

    The only sucky thing about the arrays is it generates a lot of garbage during serialization calls (at runtime this would be cloning).
     
  10. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @lordofduct Interesting, thanks for the info! I've always wondered why I sometimes see NonSerialized on private fields...

    You could Lists instead of arrays ^ for your keys/values. Just clear them in OnBeforeSerialize and iterate over the dictionary manually via GetEnumerator and MoveNext (although you're doing the same thing as foreach, but for some reason foreach generates garbage when used to iter dictionaries, at least on my machine..)

    Now how are we to go about reconstructing some of the stuff like _Next, _Buckets and _Version if we don't serialize them?

    I'm not sure about _Version, the only usage I found was so that if you ever modify the dictionary while you're enumerating it you'd get exceptions. Maybe we don't even need to reconstruct it and start from 0?
     
    Last edited: Jun 25, 2015
  11. dnemtuc

    dnemtuc

    Joined:
    Nov 15, 2013
    Posts:
    1
    Awesome work!
    Thank you very much!
     
  12. shloob

    shloob

    Joined:
    Jun 1, 2013
    Posts:
    10
    First off, thank you for posting this! I was successful with SerializableDictionary<int, float>, however I run into issues when I try to use SerializableDictionary<int, Vector3> when used in conjunction with the SerializableDictionaryDrawer. It seems to be a problem with reflection. Here is the Debug Error Log:

    ArgumentException: Object type SerializableDictionary`2[System.Int32,System.Single] cannot be converted to target type: SerializableDictionaryIntVector3
    Parameter name: val
    System.Reflection.MonoField.SetValue (System.Object obj, System.Object val, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoField.cs:133)
    System.Reflection.FieldInfo.SetValue (System.Object obj, System.Object value) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/FieldInfo.cs:150)
    SerializableDictionaryDrawer`2[System.Int32,System.Single].CheckInitialize (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at Assets/Scripts/Editor/SerializableDictionaryDrawer.cs:120)
    SerializableDictionaryDrawer`2[System.Int32,System.Single].GetPropertyHeight (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at Assets/Scripts/Editor/SerializableDictionaryDrawer.cs:20)
    UnityEditor.PropertyDrawer.GetPropertyHeightSafe (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at C:/buildslave/unity/build/Editor/Mono/ScriptAttributeGUI/PropertyDrawer.cs:37)
    UnityEditor.PropertyHandler.GetHeight (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, Boolean includeChildren) (at C:/buildslave/unity/build/Editor/Mono/ScriptAttributeGUI/PropertyHandler.cs:208)
    UnityEditor.EditorGUI.GetPropertyHeightInternal (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, Boolean includeChildren) (at C:/buildslave/unity/build/Editor/Mono/EditorGUI.cs:4803)
    UnityEditor.EditorGUI.GetPropertyHeight (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, Boolean includeChildren) (at C:/buildslave/unity/build/artifacts/generated/common/editor/EditorGUIBindings.gen.cs:746)
    UnityEditor.Editor.GetOptimizedGUIBlockImplementation (Boolean isDirty, Boolean isVisible, UnityEditor.OptimizedGUIBlock& block, System.Single& height) (at C:/buildslave/unity/build/artifacts/generated/common/editor/EditorBindings.gen.cs:204)
    UnityEditor.GenericInspector.GetOptimizedGUIBlock (Boolean isDirty, Boolean isVisible, UnityEditor.OptimizedGUIBlock& block, System.Single& height) (at C:/buildslave/unity/build/Editor/Mono/Inspector/GenericInspector.cs:14)
    UnityEditor.InspectorWindow.DrawEditor (UnityEditor.Editor editor, Int32 editorIndex, Boolean forceDirty, System.Boolean& showImportedObjectBarNext, UnityEngine.Rect& importedObjectBarRect, Boolean eyeDropperDirty) (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:1124)
    UnityEditor.InspectorWindow.DrawEditors (UnityEditor.Editor[] editors) (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:969)
    UnityEditor.InspectorWindow.OnGUI () (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:350)
    System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:222)

    Any ideas would be welcome! :)
     
  13. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    It says right there in the top line of your error.
    Object type SerializableDictionary`2[System.Int32,System.Single] cannot be converted to target type: SerializableDictionaryIntVector3

    You're trying to cast a Dictionary<int,float> to a Dictionary<int,Vector3>, somewhere.
     
  14. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    I have a question, in your prime number array, some numbers are missing like 5,7,13,19... is it intentional?
    this list is from wikipedia for prime numbers bellow 1000, as you see there are many missing numebrs in your list

    2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997

    also this may help?

    https://www.mathsisfun.com/numbers/prime-number-lists.html
     
    Last edited: Sep 5, 2015
  15. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528

    So the use of primes is only to create an array that is a length that when modulo'd with a hash you get an index with low probability of collision.

    This use case only requires that the length is prime... it doesn't necessarily have to be all primes.

    As your collection grows, the odds increase of course. If you have a collection of capacity 11 and 2 elements, very low odds of collision. A collection of capacity 11 and 9 elements, there are high odds of collision. Over 11 and you have guaranteed collision.

    So when you get close to the size of the capacity, the array inside your collection, you resize to a length of a larger prime number. Doesn't have to be the next, just larger.

    Because, if that next prime number is only 2 away... like going from 11 to 13. And you were already really close to full, like earlier when I described 9 elements in a collection sized at 11. You haven't really reduced your risk of collision at all!

    Within just a couple adds, you'll be back to having to resize your array yet again.

    Thing is... array resizing is NOT cheap. You want to reduce the frequency of that.

    So, you spread out the primes. Creating larger and larger gaps as your collection grows. But too large so you don't consume unnecessary chunks of memory.

    The spacing vexe is using appears to be a fairly standard spacing for hash tables. I'm betting vexe didn't sit down and calculate those primes themselves, and probably pulled a list from somewhere. And rightfully so... who wants to sit there and calculate a well distributed set of primes every time they go and write something like this... might as reuse a pre-compiled list that has shown to be well distributed.
     
    Last edited: Sep 5, 2015
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Oh, and I should point out, looking back on OP code.

    I want to repeat that you really shouldn't be serializing all that information, and rather recalculate all of it on deserialize, aside from the key/value arrays.

    One reason is that 'hashcodes' aren't supposed to be considered persistent across systems. From MSDN documentation for 'GetHashCode':
    https://msdn.microsoft.com/en-us/library/system.object.gethashcode(v=vs.110).aspx
     
    FuriantMedia likes this.
  17. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    I got it, thanks for info,
    But lordofduct, can you implement your own version without serializing all those fields? I am really new to serialization but in serious need of a serializable dictionary, maybe the OP won't do it any more, so, is it possible that you put a little time and do it? I would appreciate it a lot
     
  18. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Sure...

    But I will point out that I would NOT use the technique above. A hashtable would not store compactly at all, and wouldn't travel between app domains well. To get everything, you'd end up storing more data then if you were to just serialize up the key/value pairs.

    Actually, even Microsoft agrees, because if you look at their implementation. They only store the key/value pairs and rebuild the table on deserialize.

    This is the reason I don't mind slapping this together... I technically have been meaning to do it for quite some time, and also it's not that much work.



    There comes another issue though... the inspector for this would be UGLY. So you'll want an inspector that better describes the situation.


    The result:

    SerializableDictionaryBase:
    https://github.com/lordofduct/space...ase/Collections/SerializableDictionaryBase.cs
    Note that it inherits from 'DrawableDictionary', this is so that we can define a PropertyDrawer that always applies. You can't create a PropertyDrawer that targets the generics version of the class, it only targets concrete classes. So instead we have to do this.

    This is also why I have to implement IDictionary, instead of just directly inherit from Dictionary. All the methods in there for the implementation really just forward to the actual dict.

    The serialization just turns the dict into arrays.

    I would have rather inherited directly from Dictionary... but alas, the limitations with unity serialization and editor extension.



    The PropertyDrawer:
    https://github.com/lordofduct/space...ditor/Collections/DictionaryPropertyDrawer.cs
    Honestly, the PropertyDrawer is the more complicated part.

    Because it's generic, you have to create concrete classes in your scripts:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. using com.spacepuppy;
    6. using com.spacepuppy.Collections;
    7.  
    8. public class TestScript05 : MonoBehaviour {
    9.  
    10.  
    11.   [SerializeField()]
    12.   private LocalDictionary _dict;
    13.  
    14.   public LocalDictionary Dictionary
    15.   {
    16.   get { return _dict; }
    17.   }
    18.  
    19.   [System.Serializable()]
    20.   public class LocalDictionary : SerializableDictionaryBase<string, int>
    21.   {
    22.  
    23.   }
    24.  
    25. }
    26.  
    And it looks like:
    SerializedDict.png


    WARNING - I have not done tons of testing. But I can tell you that dictionary's do NOT guarantee order. This means that the order you define the entries in the inspector isn't guaranteed to be the same the next time. Just that the same entries exist.

    Because inspectors may serialize/deserialize during drawing, you could actually end up with the order changing on you when you click the add/remove buttons in my inspector.

    This is actually why I went with this add/remove technique. The resizable array comes with huge issues... especially when resizing the 'keys' array. Because the keys have to be unique, and resizing arrays in the inspector causes dupes, you can't resize.


    I would have made it prettier, the inspector that is, but I didn't want to create a lot of dependencies to the rest of my framework. There still one dependency, and that's to 'EditorHelper.SetPropertyValue', which you can find here:
    https://github.com/lordofduct/space...b/master/SpacepuppyBaseEditor/EditorHelper.cs

    To anyone in the future, this thing will probably be getting expanded further when I have the time... so there will probably be more dependencies added.
     
    Last edited: Sep 6, 2015
  19. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    well i should thank you to no end,
    1- the order is really not important, normally when someone goes for dictionaries, he is thinking about a collection without specific orders, so that should be ok
    2- for the property drawer, did you considered using the reorderable inspector stuff? that is the nicest thing for lists i saw, although it's internal for unity but available for us to use ;)
     
  20. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    It is, and I want to, but like I said I wanted to avoid integrating it too tight with my framework. And the way I use ReorderableList in PropertyDrawer would have done so.

    It also takes a little bit more work then the simple slapdash one I did here.

    I'll be updating it in the future when I have time.
     
  21. Deleted User

    Deleted User

    Guest

    I thought your asset VFW could serialize all that stuff?
     
  22. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    Well I tried a really fast one myself, then it accured to me, the reorderable list is working on List<> and the dictionary would have two lists not one, so the only way would be to have a list<KeyPairValue<>> but then we need to define the type for that damn keypair and no matter what i tried it wouldn't work in the property drawer (I am not that pro in generics). but if there was a way to get the keypairvalue from the base class then it will be easy, really easy to draw a reorderable list
     
  23. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    You have to implement the 'drawElementCallback', and in that callback you do what I'm doing inside the for loop.

    Then you implement the onAddCallback, and in that do what I do in the if statement in mine that adds.
     
  24. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    the problem will be the definition of reorderable list itself, it will need to refer to a serialized property itself?
     
  25. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Depends how you set it up.

    You could make it reference the '_keys' property, and loop over that, and just get the values from the value property stored as a field on the PropertyDrawer.

    Or we could rewrite the dictionary to use a serializable keyvaluepair struct, and then just use that instead.
     
    Last edited: Sep 8, 2015
  26. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    well here is two problems with going for the _keys :
    1- when you want to get access to _value to draw it, the serialized property of the element index will be the key and it won't find _value in that because well, it's not really a child of key
    2- if by any hack we get access to _value, if we want to draw the whole thing using your code, again, we would need to use your helper classes and i am trying to avoid it if possible.

    to solve both solutions, if we could make the serializable dictionary from keypair value, i think we would have better chance?

    I tried to add a serializeField inside the dictionary class, before serialization i would make it null, and after deserialization, i would populate it from the dictionary itself, but for some reason when i try to get that serialized property, it returns null and can't use that List<KeyPairValue> !!
     
  27. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    did i derp?

    Code (CSharp):
    1. [SerializeField] private KeyValuePair<TKey, TValue>[] _list;
    Code (CSharp):
    1. public void OnBeforeSerialize()
    2.         {
    3.             if(_dict == null || _dict.Count == 0)
    4.             {
    5.                 _keys = null;
    6.                 _values = null;
    7.             }
    8.             else
    9.             {
    10.                 int cnt = _dict.Count;
    11.                 _keys = new TKey[cnt];
    12.                 _values = new TValue[cnt];
    13.                 int i = 0;
    14.                 var e = _dict.GetEnumerator();
    15.                 while (e.MoveNext())
    16.                 {
    17.                     _keys[i] = e.Current.Key;
    18.                     _values[i] = e.Current.Value;
    19.                     i++;
    20.                 }
    21.             }
    22.             //_list = null;
    23.         }
    24.  
    25.         public void OnAfterDeserialize()
    26.         {
    27.             if(_keys != null && _values != null)
    28.             {
    29.                 if (_dict == null)
    30.                     _dict = new Dictionary<TKey, TValue>(_keys.Length);
    31.                 else _dict.Clear();
    32.                 for(int i = 0; i < _keys.Length; i++)
    33.                 {
    34.                     if (i < _values.Length)
    35.                         _dict[_keys[i]] = _values[i];
    36.                     else
    37.                         _dict[_keys[i]] = default(TValue);
    38.                 }
    39.             }
    40.  
    41.             _keys = null;
    42.             _values = null;
    43.             _list = GetList();
    44.         }
    45.  
    46. public KeyValuePair<TKey, TValue>[] GetList()
    47.         {
    48.             List<KeyValuePair<TKey, TValue>> list = new List<KeyValuePair<TKey, TValue>>();
    49.             if (_dict == null || _dict.Count == 0)
    50.                 return list.ToArray();
    51.             list.AddRange(_dict.Select((t, i) => new KeyValuePair<TKey, TValue>(_dict.Keys.ElementAt(i), _dict.Values.ElementAt(i))));
    52.             return _list.ToArray();
    53.         }
     
  28. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Sorry, I derped, it's the morning for me right now.

    You can't create a serializable generic struct. It just won't work with the way unity serializes things.

    Nor does the built in System.KeyValuePair serialize.

    That's why I did 2 distinct arrays... I actually was thinking to myself while typing that previous post "why did I use 2 arrays???", I forgot, I did because of this very issue.
     
  29. Deleted User

    Deleted User

    Guest

    If you have money you could buy FullInspector. It's really awesome and serializes generics, structs, dictionaries, etc.
     
  30. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    Yah just learned it the hard way now, tried your approach, the first element shows up but then can't change the name or the value lol, so basically we don't have a way to use the reorderable list ?
     
  31. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    @supremegrandruler it's not about money or using another asset, i am developing a big RPG Core API myself, and don't want to relay on something like the asset you name, having one or two methods are ok, but a whole solution, nah
    also
    @lordofduct if i want to use your original property drawer, then i would need to rip few methods here and there from your api is that ok? specially knowing that i am recording video tutorials on them and people see your git coords and stuff?
     
  32. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    Totally fine, the entire framework as is on github is is free to use.
     
  33. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    Thanks for permissions sir :D I'm sure 200 subscribers of my channel will be very happy, I didn't look around in your git that much yet but I think you had done awesome job and worth attention
     
  34. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    Hey lord, i have a new question, the thing is that i am developing a custom editor window, and i can't figure how to utilize your serializable dictionary to show up with the same design you have for the property drawer in the custom editor window, also, i noticed that the guids are not being serialized properly and the dictionary empty itself, i was using a serializableDictionary<Guid, int> if that helps.
     
  35. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    Sigh, found a new problem:
    @lordofduct, SerializableDictionary <string, List<int>> or SerializableDictionary <string, int[]> these two don't get serialized properly :/
     
  36. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    unity probably doesn't support serializing arrays of arrays.
     
  37. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    So a long while back I actually wrote another serialization system that I haven't played with a whole lot (I don't actually do a lot of serialization like this). It's basically a wrapper around .net serialization that pulls out any UnityEngine.Object references for safe storing, but serializes everything else into binary data, and stores that as a byte array. It's not exactly the most efficient, especially for super large collections. But it'd probably get the job done.

    It's all inside of here:
    https://github.com/lordofduct/spacepuppy-unity-framework/tree/master/SpacepuppyBase/Serialization

    You'd have to include a using statement:
    using com.spacepuppy.Serialization;

    And replace the ISerializationCallbackReceiver Interface section with this:

    Code (csharp):
    1.  
    2.   #region ISerializationCallbackReceiver
    3.  
    4.    [UnityEngine.SerializeField()]
    5.    private UnityData _data;
    6.  
    7.    void UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize()
    8.    {
    9.      if(_data != null)
    10.      {
    11.        _dict = SerializationHelper.BinaryDeserialize(_data) as Dictionary<TKey,TValue>;
    12.        _data.Clear();
    13.      }
    14.    }
    15.  
    16.    void UnityEngine.ISerializationCallbackReceiver.OnBeforeSerialize()
    17.    {
    18.      if(_dict == null || _dict.Count == 0)
    19.      {
    20.        _data = null;
    21.      }
    22.      else
    23.      {
    24.        if(_data == null) _data = new UnityData();
    25.        SerializationHelper.BinarySerialize(_data, _dict);
    26.      }
    27.    }
    28.  
    29.    #endregion
    30.  
    I haven't tested this with dictionaries myself, nor actually replaced it in the framework, as I don't really use the serializable dictionary class for anything.

    We also have a game releasing on Friday, so I don't really have the time to do that.

    Go ahead and give it a try, let me know if it works.
     
  38. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    Well I didn't try this one, what i ended up with was having a serializable class that keeps the fields i want to serialize and then use a list of that class instead of the dictionary, it was a bit tricky to make it work but now everything is working.
     
  39. zhuchun

    zhuchun

    Joined:
    Aug 11, 2012
    Posts:
    433
    It's quite frustrated that Unity says they won't support serialize dictionary in docs, they know we want it, damn! Thank your for contributing code and drawer @vexe !
     
  40. VengeanceDesign

    VengeanceDesign

    Joined:
    Sep 7, 2014
    Posts:
    84
    Thank you for this code. I hoped it would work for arrays as a type but I found a way to do what I needed to without arrays.
     
    Digimaster likes this.
  41. zero_null

    zero_null

    Joined:
    Mar 11, 2014
    Posts:
    159
    I am not aware, what to try and how?
    Please help
     
  42. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    This method has been working quite well for me. Uses a list of serializable objects to configure in the inspector. Then on awake it converts to a real dictionary for lookups.

    Code (CSharp):
    1. [Serializable]
    2. public class MyDictionaryEntry
    3. {
    4.     public GameObject key;
    5.     public float value;
    6. }
    7.  
    8. [SerializeField]
    9. private List<MyDictionaryEntry> inspectorDictionary;
    10.  
    11. private Dictionary<GameObject, float> myDictionary;
    12.  
    13. private void Awake()
    14. {
    15.     myDictionary = new Dictionary<GameObject, float>();
    16.     foreach(MyDictionaryEntry entry in inspectorDictionary)
    17.     {
    18.         myDictionary.Add(entry.key, entry.value);
    19.     }
    20. }
     
    Last edited: Nov 2, 2017
    renuke, boysenberry, izyplay and 2 others like this.
  43. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    Excuse my noobness but once you've you feed your dictionary how do you read it from another scene?

    I was using playerprefsX and storing the data in a list but its not enough I need a key,value like collection and that is not supported by playerprefsX. Started to look on how store a dictionary and all said its neccesary to serialize so I finally ended up here.

    I understand serializable dictionary data storage process but then what, how do I save so it can be read read from other scene?
     
  44. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @TooManySugar You could do it in multiple ways.
    - Store the dictionary in a scene object that you call DontDestroyOnLoad on, that way the object still persists between scene loads.
    - Store the dictionary in an asset, i.e. an object that doesn't live in the scene like a ScriptableObject. Asset files are not affected by scene loads.
    - Complicated but, before you load your new scene, you serialize the dictionary to a string, save it to PlayerPrefs and load it back/deserialize it in your other scene.

    I'd personally avoid all of those. Save yourself the headache and don't use multiple scenes. Just use one and dynamically load/unload things in/out of it. All your levels/elements could be prefabs that you load in/out.
     
    Last edited: Sep 29, 2016
  45. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    @vexe thanks! I've my game fully build& running so can´t do the last thing you mention. I'm extending it and found myself on this situation.

    The don´t destroy on load looks like a nice option!
    Using the playerprefs method was what I thought but I do not know what deserialize involves/ how its done.. My dictionary is not very big as it stores the enemy type and an it of how many of each are need to be spawned, this dictionary is to be read on spawn process.

    edit:reading now about scriptable objects. New for me XD
     
    Last edited: Oct 1, 2016
  46. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    @vexe Hello again. I'm back for beggin a little help.

    I finally decided to serialize the dictionary to be able to save it so I can read later on in other scene and further be able to restore in other game sesion.

    The dictionary is used to save the enemy tank type as well as the count of each tank.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class AiTankSpecs : SerializableDictionary<string, int> { }
    4.  
    5. public  class PlayerPrefsMenu : MonoBehaviour {
    6.  
    7.     [Serializable]
    8.    
    9.  
    10.     public AiTankSpecs EnemyTankDictionary;
    11.  
    12.     public static PlayerPrefsMenu _PPMenu;
    13.  
    14.     //private List< string> AI_enemyTanks_List = new List<string>();
    15.  
    16.     private string[] AI_enemyTanks_Array;
    17.  
    18.     private int e_tankCount;//enemy tank count value del diccionario


    This is how I fill the dictionary:

    Code (CSharp):
    1.     public void enemy_AI_add (string tankName){
    2.         if ( EnemyTankDictionary.TryGetValue(tankName, out e_tankCount)){ e_tankCount +=1; EnemyTankDictionary[tankName] = e_tankCount; }//si existe añadimos 1
    3.         else {EnemyTankDictionary.Add(tankName,1);}
    4.  
    5.         Debug.Log ("tanque enemigo" + tankName + EnemyTankDictionary[tankName]);
    6.     }
    7.  
    8.     public void enemy_AI_substract (string tankName){
    9.         if ( EnemyTankDictionary.TryGetValue(tankName, out e_tankCount)){
    10.             e_tankCount -=1;
    11.             if (e_tankCount<0){    e_tankCount =0; EnemyTankDictionary.Remove (tankName);}
    12.             if (e_tankCount>0) EnemyTankDictionary[tankName] = e_tankCount; }//si restamos 1 hasta 0
    13.     }



    So, once I've the dictionary with all the stuff I want to serialize it. (never done this before :) ). So. I tried this:

    Code (CSharp):
    1.  
    2.   public static void Save() {
    3.        BinaryFormatter bf = new BinaryFormatter();
    4.  
    5.        FileStream file = File.Create (Application.persistentDataPath + "/enemydata.dat");
    6.  
    7.        bf.Serialize(file, EnemyTankDictionary );
    8.        file.Close();
    9.      }
    10.  
    11.    public static void Load() {
    12.        if(File.Exists(Application.persistentDataPath + "/enemydata.dat")) {
    13.          BinaryFormatter bf = new BinaryFormatter();
    14.          FileStream file = File.Open(Application.persistentDataPath + "/enemydata.dat", FileMode.Open);
    15.          AiTankSpecs EnemyTankDictionaryReadTest = (AiTankSpecs) bf.Deserialize(file);
    16.          file.Close();
    17.        }
    18.      }

    But I'm gettin this error:
    Assets/06_varis/scripts/Menu/PlayerPrefsMenu.cs(221,44): error CS0120: An object reference is required to access non-static member `PlayerPrefsMenu.EnemyTankDictionary'

    SerializableDictionary.cs means to be attached to an in scene object may be? I got totally stuck in there :\
     
    Last edited: Oct 17, 2016
  47. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    I'm totally stuck with this.

    Shouldnt this:
    Code (CSharp):
    1.  public AiTankSpecs EnemyTankDictionary;
    be enough to create an instance of the custom dictionary Class?

    It works fine until I try to serialize it
    Code (CSharp):
    1.  bf.Serialize(file, EnemyTankDictionary );
     
  48. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @TooManySugar your 'Save' function is static trying to access the member field 'EnemyTankDictionary'. - One way to fix it is to pass the dictionary to the 'Save' function.
     
  49. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    I'm deffinetly retarded.
    1) Why the hell did I make the save load static.
    2) it was in front of my face!. Console is clean! Will post if I manage to make it run!

    My plan is to build a custom class "Values" that holds a bunch of variables such as tier, skin etc.. I'll make the class serializable as the types are simple such string, and int.

    A BIG THANK U!
     
  50. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    WOW! managed to make it work with a <string, int> type like dictionary! awesome!

    May be in editor is different but in runtime I had to

    Code (CSharp):
    1.     public AiTankSpecs EnemyTankDictionary  = new AiTankSpecs();
    2.  
    instead of the as suggested in this thread

    Code (CSharp):
    1.     public AiTankSpecs EnemyTankDictionary ;
    Now I'll try with custom class that allocates several variables.