Search Unity

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

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

  1. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    Awesome again!!

    I built a class with all the properties I want to setup in the menu, marked it as serializable and made a constructor for it.

    I subclass a dictionary like

    Code (CSharp):
    1. public class AiTankSpecs : SerializableDictionary <string, TankProperties> { }
    where TankProperties is the class holding all the data types.

    Initially the TankProperties was inheriting from Monobehavior (unnecesarily, just I'm used to XD), witch did cause an error.

    The class should not inherit from anything, in fact it could/should be an Struct.

    It saves and load data flawlessly.

    GREAAAAT!!!!

    @vexe I belive you appear in every conversation in unity answers regarding serializable dictionaries lol.
     
  2. Danny-vda

    Danny-vda

    Joined:
    Aug 7, 2013
    Posts:
    5
    @vexe Thanks so much for your brain and energy to put this down!
    It works flawlessly - and I could not have done this.

    Cheers!

    Danny.
     
  3. kirayamato123

    kirayamato123

    Joined:
    Oct 25, 2016
    Posts:
    1
    I've tried to use the composition method which is following what you exactly did there. But I encountered an error (The namespace '<global namespace>' already contains a definition for 'SerializableDictionary') Because both of the scripts are having the namespace, are there any simple way to solve this while not changing the script's namespace (is it possible?)

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

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    I got this error the first time I was trying to make it run.

    Simply subclass it once and the class will be available in the rest of your classes.

    I did subclass in my saveload manager.

    [Serializable]
    public class TankDic : SerializableDictionary <string, WhateverDataClass> { }
     
  5. SiLveRLunE

    SiLveRLunE

    Joined:
    Nov 2, 2016
    Posts:
    1
    I'm having trouble with saving serializableDictionary data for reloading.

    I have put SerializableDictionary and DictionaryDrawer in my project, subclassed the dictionary with

    Code (CSharp):
    1. [Serializable] public class StringStringDict : SerializableDictionary<string, string> { }
    2. [CustomPropertyDrawer(typeof(StringStringDict))]
    3. public class StringStringDictDrawer : DictionaryDrawer<string, string> { }
    In my monobehaviour I have
    Code (CSharp):
    1.     [SerializeField] private StringStringDict nameDict = new StringStringDict();
    This is all working fine in the editor, I am able to manipulate the dictionary in the inspector, and use the dictionary in my game. However, when I hit ctrl+s to save the monobehaviour gameobject and restart Unity, the data in the dictionary is all gone. I have Force Text Serialization enabled, and by viewing the .scene and .asset files directly, I don't see the dictionary being serialized and stored as 2 lists in the files, which is probably why the data is all gone after restarting Unity.

    Any ideas on why this is happening?
     
  6. Mbastias

    Mbastias

    Joined:
    Jan 1, 2015
    Posts:
    6
    I have the same problem as @SiLveRLunE and a little problem with the drawer. It's a <enum,Sprite> dictionary and the object picker doesn't appear, instead I have this:

    drawer.png

    Any help is well received

    Edit: The save problem is covered here: http://joshua-p-r-pan.blogspot.cl/2016/01/unity-how-to-serialize-dictionary.html. Now the drawer is my only concern.

    Edit2: The drawer problem was the height of the rect that receives ObjectField, I lowered it to 15 and it works fine.
     
    Last edited: Dec 30, 2016
  7. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    I'm getting crazy with an exception:

    InvalidOperationException: Enumerator version 721 != Dictionary version 722
    SerializableDictionary`2+Enumerator[System.String,TankProperties].MoveNext () (at Assets/Standard Assets/SerializableDictionary.cs:537)


    And more similar to the one that is failing, but works:

    Code (CSharp):
    1.     //SUBSTRACT
    2.     public void Entry_Substract (string tankModel){
    3.  
    4.         if ( TankDictionary.TryGetValue(tankModel, out val)){
    5.                 val.count -=1;    
    6.             if (val.count<0){    val.count =0; TankDictionary.Remove (tankModel);}
    7.             if (val.count>0) TankDictionary[tankModel]  = new TankProperties( val.isPlayer, val.team,val.faction,val.vehicleType, val.model, val.tier, val.skin, val.count);
    8.  
    9.             SaveLoad_TankData.Save(TankDictionary);
    10.         }
    11.  
    12.         //Debug.Log ("tanque enemigo" + tankModel + TankDictionary[tankModel]);
    13.  
    14.         else {
    15.             Debug.Log( tankModel + "tankModelcleared");
    16.         }
    17.     }

    And the one that fails is:
    Code (CSharp):
    1.     public void UpdateEntriesTeam (int flagFaction, int flagTeam){
    2.         int i;
    3.         i = 0;
    4.         foreach(KeyValuePair<string,TankProperties> keyVal in TankDictionary){
    5.             i+=1;
    6.             TankProperties val = keyVal.Value;
    7.            
    8.             if( keyVal.Value.faction == flagFaction){
    9.                
    10.                 TankDictionary[keyVal.Key]  = new TankProperties( val.isPlayer, flagTeam,val.faction,val.vehicleType, val.model, val.tier, val.skin, val.count);
    11.                 string _key = keyVal.Key.ToString ();
    12.  
    13.                 print ("key" + keyVal.Key);
    14.                 print ("team updated" + "faction" + flagFaction );
    15.                 print ("i" + i);
    16.  
    17.                 SaveLoad_TankData.Save(TankDictionary);
    18.  
    19.             }
    20.         }      
    21.  
    22.     }    

    What I'm trying to do in teh code that fails is that for the already available tanks in the dictionary, rewrite the team value.
    But unlinke in any other case that dictionary version issue appears. Any clues of what Ii'm doing wrong?
     
  8. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    FINAL EDIT:

    THIS POST WAS WRONG, NOT SURE HOW I MADE IT TO WORK AS IT WAS.
    the issue was that I was not loading and saving the dictionary.
    //---------------------------------------------------------------------------------------------------------------

    The enumerator version is allways 1 unit behind.

    EDIT:
    ---------------------------------------------------------
    Found this workarround:

    Function1:
    Code (CSharp):
    1.     public void Function1 (int flagFaction, int flagTeam){
    2.  
    3.         foreach ( string st in TankDictionary.Keys){
    4.             //print(st);
    5.             Function2(st,flagFaction,flagTeam);
    6.         }
    Function2: (called from function1)
    Code (CSharp):
    1.     //OverwriteTeam
    2.     public void Function2 (string key, int flagFaction, int flagTeam){
    3.    
    4.         if ( TankDictionary.TryGetValue(key, out val)){
    5.                 if (val.faction==flagFaction){
    6.                     TankDictionary[key]  = new TankProperties( val.isPlayer, flagTeam,val.faction,val.vehicleType, val.model, val.tier, val.skin, val.count);
    7.                     SaveLoad_TankData.Save(TankDictionary);
    8.                     Debug.Log( "key" + key );
    9.                     Debug.Log( "flagTeam" + flagTeam );
    10.             }
    11.         }
    12.     }
    Silly but works, splitted the original function in two parts, first part lists keys and kalls fucntion2 with the kay value and the parameters needed.

    I do not know much about coding but I could suspect some glitch when you use foreach() to write values. Hope this helps to solve it, in the meantime.... this seems to work.
     
    Last edited: Apr 26, 2017
  9. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Thank you for this code, made my life a lot easier!

    I encountered a problem though, @vexe when creating a Prefab object from a Scene object, deleting the original Scene object and then altering the Prefab pobject, the changes to the Prefab object don't save for me, but the Prefab object will "reset" to the state of its creation, when moving the Scene object into the Asset foulder to create a Prefab from it. Is this intended or reasonable ?
     
  10. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @Ardenian sounds like some editor BS. Try doing a UnityEditor.EditorUtility.SetDirty(target); in the dictionary editor/drawer after an edit's been made. See if that fixes it.
     
  11. SpatiumInteractive

    SpatiumInteractive

    Joined:
    Nov 4, 2015
    Posts:
    1
    In the inspector everything works fine , but when I clicking play, all data stored in dictionaries gets lost... It's seems like it wasn't serialized in the first place, so unity can't deserialized it... or something.... any idea how to fix this?
    btw, the same problem have @Mbastias and @SiLveRLunE
     
  12. No3371

    No3371

    Joined:
    Dec 1, 2016
    Posts:
    42
    Hey, FYI, In 5.5.1 p4 patch, there's 1 entry: (699250) - Scripting: Allow serialization of classes extending Dictionary.
     
  13. marian42

    marian42

    Joined:
    Dec 30, 2016
    Posts:
    6
    Unfortunately, the code in the OP is broken.

    I'm using a SerializableDictionary like this:

    Code (csharp):
    1. Debug.Log(dict.ContainsKey(new MapLocation(3, -5)));
    2. Debug.Log(dict.Keys.Contains(new MapLocation(3, -5)));
    The first line returns True, the second line returns False.
     
  14. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @marian42 You shouldn't be using dict.Keys.Contains directly. dict.ContainsKey calls FindIndex, which Keys.Contains obviously doesn't call if you read the source code.
     
  15. marian42

    marian42

    Joined:
    Dec 30, 2016
    Posts:
    6
    Just tried this and it doesn't work. Apparently this only applies to editor serialization, not disk serialization.
     
  16. marian42

    marian42

    Joined:
    Dec 30, 2016
    Posts:
    6
    But why would these two methods return different values? If the values are different, one of them must be broken.
     
  17. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I would consider this a bug, since I would expect both to act similarly.

    As for why it's happening... since Keys returns a copy of the array, not sure why it wouldn't... though I notice in your code you pass in new MapLocations, I don't know what MapLocation is, but I have to assume it must be a struct. Are you using a custom Comparer with the dict, does it compare correctly on arrays aside from the dict?

    Back at the original code implementation. That Keys and Values implementation is garbage heavy. I'd suggest implementing collection types for each that wrap around the dictionary internally, and calls through to the appropriate methods. Similar to how the System.Collections.Dictionary does. Just implement the ICollection interface and throw System.NotSupportedException for the methods that shouldn't be called (like Add).
     
  18. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    I've decided to post my version of serializable Dictionary. the original code came from lordofDuct's SerializableDictionary. I've since done some major work on both the class and its Property Drawer

    [Edit: Image has been removed due to hosting site not showing image]

    Here the "enabled" field is a part of the InputManager, not the Serializable dictionary. Afterwards everything else is part of the SerializableDictionary PropertyDrawer. This SerializableDictionary is binding together the types <string,NamedInputData>, with NamedInputData being an abstract ScriptableObject class.

    The toggle button in the topleft (using the RectTransform's Blueprint mode icon) enables Selection Box mode which lets the Drawer draw out the value of the selected index in a white card area below. The button to the right (with the label "Axes (13 Objects)") toggles the property.isExpanded so clicking it will toggle a show/hide of the entire dictionary. whats shown in the row is dynamic to propertyType. If its something that can't fit on a single line it'll show the property's value as a string (or its display name if its a complex property) instead. In those cases you can still edit the property in the selection box.

    Remove and Add buttons work as you'd expect. Removing actually removes the entire row (instead of setting it null which DeleteArrayElementAtIndex does). when Adding, you specify the key and the value will be set to null or default depending on the property Type, and attempting to add a duplicate key will be ignored. the Selection box itself is doesn't do anything too fancy it simply iterates over the serialized properties and uses PropertyField on them, similar thing when Getting PropertyHeight.

    now onto the code itself. Some features require the WoofTools namespace which I haven't released yet. they are bonus extra features and are not required for the core class to work. When I get some time I might come back and clean up the dependencies, however the core of the classes are accessible and anyone should be able to use it as a basis to make their own version.

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using UnityEngine;
    6. using WoofTools.API;// for IWoofSavable
    7. using WoofTools.Utilities;//Used for LinqExtensions.ForEach()
    8.  
    9. /**************
    10. * Original code of this class goes to LordofDuct in the forum thread
    11. * https://forum.unity3d.com/threads/finally-a-serializable-dictionary-for-unity-extracted-from-system-collections-generic.335797/#post-2282920
    12. * It has since been tweaked with a couple bonus features
    13. *************/
    14.  
    15.  
    16. namespace WoofTools.Collections
    17. {
    18.     //Base, non-generic class for UnityEditor to reference Drawers
    19.     public abstract class BaseSerializableDictionary
    20.     {
    21.         //Meta Data used for the Drawer labels
    22.         public string keyName = "Key";
    23.         public string valueName = "Value";
    24.         public string newKeyName = "New Key";
    25.         public bool drawSelection = false;
    26.  
    27.         public BaseSerializableDictionary(string keyName = "Key",string valueName = "Value",string newKeyName = "New Key")
    28.         {
    29.             this.keyName    = keyName;
    30.             this.valueName  = valueName;
    31.             this.newKeyName = newKeyName;
    32.         }
    33.     }
    34.  
    35.     //Abstract, generic class with most implementation
    36.     public abstract class AbstractSerializableDictionary <TKey,TValue> :
    37.         BaseSerializableDictionary,
    38.         IDictionary<TKey,TValue>,
    39.         ISerializationCallbackReceiver,
    40.         IWoofSavable
    41.     {
    42.         [SerializeField] private TKey newKey;//used by the Drawer when it want to add a new item (a key needs to be provided first)
    43.         [SerializeField] private TKey[] keys;
    44.         [SerializeField] private TValue[] values;
    45.  
    46.         [NonSerialized] private Dictionary<TKey,TValue> dict;
    47.  
    48.         public AbstractSerializableDictionary(string keyName = "Key",string valueName = "Value",string newKeyName = "New Key")
    49.             :base(keyName,valueName,newKeyName){}
    50.  
    51.         public void SetDictionary(TKey[] newKeys, TValue[] newvalues)
    52.         {
    53.             keys = newKeys;
    54.             values = newvalues;
    55.  
    56.             OnAfterDeserialize();
    57.         }
    58.  
    59.  
    60.  
    61.         #region Linq Extras
    62.         public void RemoveAll(Func<KeyValuePair<TKey,TValue>,bool> match)
    63.         {
    64.             if(dict == null) return;
    65.  
    66.             dict = dict.Where(kvp=>!match(kvp)).ToDictionary(kvp=>kvp.Key,kvp=>kvp.Value);
    67.         }
    68.         public void ForEach(Action<KeyValuePair<TKey,TValue>> action)
    69.         {
    70.             if(dict == null) return;
    71.  
    72.             dict.ForEach(kvp=>action(kvp));
    73.         }
    74.         #endregion
    75.  
    76.         #region IDictionary implementation
    77.  
    78.         public virtual bool ContainsKey(object key)
    79.         {
    80.             if(key == null) return false;
    81.  
    82.             return ContainsKey((TKey)key);
    83.         }
    84.  
    85.         public bool ContainsKey (TKey key) { return (dict == null)? false: dict.ContainsKey(key); }
    86.  
    87.         public void Add (TKey key, TValue value) { if(dict == null) dict = new Dictionary<TKey, TValue>(); dict.Add(key,value); }
    88.  
    89.         public bool Remove (TKey key) { return (dict == null)? false : dict.Remove(key); }
    90.  
    91.         public bool TryGetValue (TKey key, out TValue value)
    92.         {
    93.             if(dict==null)
    94.             {
    95.                 value = default(TValue);
    96.                 return false;
    97.             }
    98.  
    99.             return dict.TryGetValue(key,out value);
    100.         }
    101.  
    102.         public TValue this [TKey index]
    103.         {
    104.             get { if(dict==null) throw new NullReferenceException(); return dict[index];     }
    105.             set { if(dict==null) dict = new Dictionary<TKey, TValue>(); dict[index] = value; }
    106.         }
    107.  
    108.         public ICollection<TKey> Keys     { get { if(dict == null) dict = new Dictionary<TKey, TValue>(); return dict.Keys;   } }
    109.  
    110.         public ICollection<TValue> Values { get { if(dict == null) dict = new Dictionary<TKey, TValue>(); return dict.Values; } }
    111.  
    112.         #endregion
    113.  
    114.         #region ICollection implementation
    115.         public void Add (KeyValuePair<TKey, TValue> item){ if(dict == null) dict = new Dictionary<TKey, TValue>(); dict.Add(item.Key,item.Value); }
    116.  
    117.         public void Clear () { if(dict != null) dict.Clear(); }
    118.  
    119.         public bool Contains (KeyValuePair<TKey, TValue> item) { return (dict == null)? false: (dict as ICollection<KeyValuePair<TKey, TValue>>).Contains(item); }
    120.  
    121.         public void CopyTo (KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    122.         {
    123.             if(dict != null)
    124.                 (dict as ICollection<KeyValuePair<TKey, TValue>>).CopyTo(array,arrayIndex);
    125.         }
    126.  
    127.         public bool Remove (KeyValuePair<TKey, TValue> item) { return (dict == null)? false : (dict as ICollection<KeyValuePair<TKey, TValue>>).Remove(item); }
    128.  
    129.         public int Count { get { return (dict == null)? 0 : dict.Count; } }
    130.  
    131.         public bool IsReadOnly { get { return (dict == null)? false : (dict as ICollection<KeyValuePair<TKey, TValue>>).IsReadOnly; } }
    132.  
    133.         public Dictionary<TKey,TValue>.Enumerator GetEnumerator ()
    134.         {
    135.             return (dict == null)
    136.                 ? default(Dictionary<TKey,TValue>.Enumerator)
    137.                     : dict.GetEnumerator();
    138.         }
    139.  
    140.         IEnumerator IEnumerable.GetEnumerator ()
    141.         {
    142.             return (dict == null)
    143.                 ? Enumerable.Empty<KeyValuePair<TKey, TValue>>().GetEnumerator()
    144.                     : dict.GetEnumerator();
    145.         }
    146.  
    147.         IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator ()
    148.         {
    149.             return (dict == null)
    150.                 ? Enumerable.Empty<KeyValuePair<TKey, TValue>>().GetEnumerator()
    151.                     : dict.GetEnumerator();
    152.         }
    153.         #endregion
    154.  
    155.         #region ISerializationCallbackReceiver implementation
    156.  
    157.         public void OnBeforeSerialize ()
    158.         {
    159.             if(dict == null)
    160.             {
    161.                 keys = null;
    162.                 values = null;
    163.                 return;
    164.             }
    165.  
    166.             keys = new TKey[dict.Count];
    167.             values = new TValue[dict.Count];
    168.  
    169.             var e = dict.GetEnumerator();
    170.             for(int i=0;e.MoveNext();i++)
    171.             {
    172.                 keys[i]   = e.Current.Key;
    173.                 values[i] = e.Current.Value;
    174.             }
    175.         }
    176.  
    177.         public void OnAfterDeserialize ()
    178.         {
    179.             if(keys == null || values == null) return;
    180.  
    181.             dict = new Dictionary<TKey, TValue>(keys.Length);
    182.  
    183.  
    184.             for(int i=0;i<keys.Length;i++)
    185.             {
    186.                 TValue value = (i>=values.Length)?default(TValue):values[i];
    187.  
    188.                 this[keys[i]] = value;
    189.             }
    190.         }
    191.  
    192.         #endregion
    193.  
    194.         #region IWoofSavable implementation
    195.         //this class doesn't memorize a default state, so simply clear it
    196.         public void Reset(){ if(dict != null) dict.Clear(); }
    197.  
    198.         public object GetSerilizableObject()
    199.         {
    200.             OnBeforeSerialize();
    201.  
    202.             return new object[]{keys,values};
    203.         }
    204.  
    205.         public bool SetSerilizableObject(object data)
    206.         {
    207.             object[] array = data as object[];
    208.  
    209.             if(array == null || array.Length < 2 )   return false;//deserialize failed, insufficient data
    210.             if(array[0] == null || array[1] == null) return false;//deserialize failed, insufficient data
    211.  
    212.             var _keys = array[0] as TKey[];
    213.             var _values = array[1] as TValue[];
    214.  
    215.             if(_keys == null || _values == null) return false;//deserialize failed, incompatible data
    216.  
    217.             keys = _keys;
    218.             values = _values;
    219.  
    220.             OnAfterDeserialize();
    221.             return true;//deserialization succeeded
    222.         }
    223.  
    224.         #endregion
    225.     }
    226.  
    227.     /// <summary>
    228.     /// Serializable version that will be shown in the inspector, reccomended for TValue types that are not Unity Objects
    229.     /// </summary>
    230.     [Serializable] public class SerializableDictionary<TKey,TValue> : AbstractSerializableDictionary<TKey,TValue>
    231.     {
    232.         public SerializableDictionary(string keyName = "Key",string valueName = "Value",string newKeyName = "New Key")
    233.             :base(keyName,valueName,newKeyName){}
    234.     }
    235.  
    236.     /// <summary>
    237.     // Serializable version that will be shown in the inspector, reccomended for TValue types that are Unity Objects
    238.     /// </summary>
    239.     [Serializable] public class SerializableObjectDictionary<TKey,TValue> : AbstractSerializableDictionary<TKey,TValue>
    240.         where TValue:UnityEngine.Object
    241.     {
    242.         public SerializableObjectDictionary(string keyName = "Key",string valueName = "Value",string newKeyName = "New Key")
    243.             :base(keyName,valueName,newKeyName)
    244.         {
    245.             drawSelection = true;
    246.         }
    247.     }
    248. }

    Most of my Editors use WoofTools.WoofEditor extensively, this PropertyDrawer is no different. many of the special extension methods are code sugar or optimizations on EditorGUI,EditorGuiLayout, SerializedProperty, and SerializedObject classes.

    for example GetProperty an extension method added to both SerializedObject and SerializedProperty and all it is is code sugar so I don't have to memorize which one to use FindProperty and which to use FindRelativeProperty. While GetElement is an Optimization over GetArrayElementAtIndex which is slow on huge arrays. Field() is simply PropertyField (for both EditorGUI and EditorGuiLayout) with BeginChangeCheck included.

    Code (CSharp):
    1.  
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. using WoofTools.Collections;
    6. using WoofTools.WoofEditor.Extensions;
    7. using WoofTools.WoofEditor.Skin;
    8.  
    9. namespace WoofTools.WoofEditor.Drawers
    10. {
    11.  
    12.     [CustomPropertyDrawer(typeof(BaseSerializableDictionary),true)]
    13.     public class SerilizableDictionaryDrawer : PropertyDrawer
    14.     {
    15.         private static string k_newKey        = "newKey";
    16.         private static string k_Keys          = "keys";
    17.         private static string k_Values        = "values";
    18.         private static string k_drawSelection = "drawSelection";
    19.  
    20.         private static string k_keyName       = "keyName";
    21.         private static string k_valueName     = "valueName";
    22.         private static string k_newKeyName    = "newKeyName";
    23.      
    24.         private static GUIContent c_add = new GUIContent("\u2732", "Add");
    25.         private static GUIContent c_remove = new GUIContent("\u2573");//, "Remove"); //removed the tooltip since it usually obscures the button
    26.         private static GUIContent c_drawSelection = null;
    27.  
    28.         private static Color color_selected = new Color(0.5f,0.5f,1f,1f);
    29.         private static Color color_background = Color.white;
    30.  
    31.         private WoofSkin skin;
    32.         //incase you don't have the skin package you can use these styles instead, they're pretty close
    33. //        public readonly GUIStyle s_bigButton        = "LargeButton";
    34. //        public readonly GUIStyle s_card             = "sv_iconselector_labelselection";
    35. //        public readonly GUIStyle s_flatBox          = "IN BigTitle";
    36. //        public readonly GUIStyle s_footerBackground = "InnerShadowBg";
    37. //        public readonly GUIStyle s_leftButton       = "ButtonLeft";
    38. //        public readonly GUIStyle s_rightButton      = "ButtonRight";
    39. //        public readonly GUIStyle s_title            = "PreOverlayLabel";
    40.  
    41.  
    42.         private GUIContent c_key    = new GUIContent(string.Empty);
    43.         private GUIContent c_val    = new GUIContent(string.Empty);
    44.         private GUIContent c_newKey = new GUIContent(string.Empty);
    45.  
    46.         private SerializedProperty   p_keys;
    47.         private SerializedProperty   p_values;
    48.         private SerializedProperty   p_key;
    49.         private SerializedProperty   p_val;
    50.         private SerializedProperty   p_newKey;
    51.         private SerializedProperty   p_drawSelection;
    52.  
    53.         private SerializedProperty   p_keyName;
    54.         private SerializedProperty   p_valueName;
    55.         private SerializedProperty   p_newKeyName;
    56.  
    57.  
    58.         private int selectedIndex = -1;
    59.         private int indexSelectedLastFrame = -1;
    60.         private int removeIndex = -1;
    61.         private float SelectionBoxHeight = 0;
    62.         float rowHeight =0;
    63.         float dictionaryHeight =0;
    64.      
    65.         public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    66.         {
    67.             skin = skin.Use();
    68.             if(c_drawSelection == null)
    69.                 c_drawSelection = EditorGUIUtility.IconContent("RectTransformBlueprint","|Selection Box mode. draws the selected value in a card below the dictionary");
    70.  
    71.             SelectionBoxHeight = 0;
    72.             rowHeight = EditorGUIUtility.singleLineHeight+8f;// height per row
    73.             float topHeight = rowHeight;
    74.             dictionaryHeight = 0;
    75.             float addRowHeight = 0;
    76.             float endMargin = 5f; // pixel margin to separate this propertydrawer from the next
    77.  
    78.  
    79.          
    80.             //heights for each row
    81.             if (property.isExpanded)
    82.             {
    83.                 p_keys   = property.GetProperty (k_Keys);
    84.                 p_values = property.GetProperty (k_Values);
    85.                 p_drawSelection = property.GetProperty(k_drawSelection);
    86.              
    87.                 int maxRows = Mathf.Max (p_keys.arraySize, p_values.arraySize);
    88.  
    89.                 topHeight -= 3;
    90.                 dictionaryHeight = maxRows*rowHeight;
    91.                 addRowHeight = rowHeight;
    92.                  
    93.  
    94.                 if(p_drawSelection.boolValue && indexSelectedLastFrame>-1)
    95.                 {
    96.                     var p_element = p_values.GetElement(indexSelectedLastFrame);
    97.  
    98.  
    99.                     SelectionBoxHeight += 5f;// add margin between dictionary and selection card
    100.                     SelectionBoxHeight += 5f;// add top padding for Card background
    101.                     SelectionBoxHeight += p_element.AbstractHeight();//add hight of property itself
    102.                     SelectionBoxHeight += 5f;// add bottom padding for Card background
    103.                 }
    104.             }
    105.          
    106.             return topHeight + dictionaryHeight + addRowHeight + SelectionBoxHeight + endMargin;
    107.         }
    108.      
    109.         public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    110.         {
    111.             skin = skin.Use();
    112.  
    113.             int oldIndent = EditorGUI.indentLevel;
    114.             EditorGUI.indentLevel = 0;
    115.             Color backColor = GUI.backgroundColor;
    116.             GUI.backgroundColor = color_background;
    117.             removeIndex = -1;
    118.  
    119.             float width = position.width-10f;
    120.  
    121.             Rect r_drawSelection = new Rect(position.xMin+5,      position.yMin,     rowHeight,       rowHeight);
    122.             Rect r_expanded      = new Rect(r_drawSelection.xMax, position.yMin,     width-rowHeight, rowHeight);
    123.             Rect r_back          = new Rect(position.xMin+5,      r_expanded.yMax-3,   width,           dictionaryHeight);
    124.             Rect r_clickRow      = new Rect(r_back.xMin,          r_back.yMin,       width,           rowHeight);
    125.             Rect r_addRow        = new Rect(r_back.xMin,          r_back.yMax,       width,           rowHeight);
    126.             Rect r_selectionBox  = new Rect(r_back.xMin,          r_addRow.yMax+5f,  width,           SelectionBoxHeight);
    127.  
    128.             p_keys          = property.GetProperty(k_Keys);
    129.             p_drawSelection = property.GetProperty(k_drawSelection);
    130.  
    131.             GUIContent c_header = new GUIContent(label.text +" ("+p_keys.arraySize+" objects)",label.image,label.tooltip );
    132.  
    133.             if(property.isExpanded)
    134.             {
    135.                 //draw selection box before toggling p_drawSelection and to avoid single frame render errors
    136.                 if(p_drawSelection.boolValue)
    137.                     DrawSelectionBox(r_selectionBox);
    138.             }
    139.  
    140.             EditorGUI.BeginChangeCheck();
    141.             p_drawSelection.boolValue = GUI.Toggle(r_drawSelection, p_drawSelection.boolValue, c_drawSelection,skin.LeftButton);
    142.             if(EditorGUI.EndChangeCheck())p_drawSelection.SaveModifiedProperties();
    143.  
    144.  
    145.             if(GUI.Button(r_expanded,c_header , skin.RightButton)) property.isExpanded = !property.isExpanded;
    146.  
    147.  
    148.             if(property.isExpanded)
    149.             {
    150.                 GUI.Box(r_back,GUIContent.none,skin.FlatBox);
    151.  
    152.                 p_values = property.GetProperty(k_Values);
    153.                 p_newKey = property.GetProperty(k_newKey);
    154.  
    155.                 p_keyName    = property.GetProperty(k_keyName);
    156.                 p_valueName  = property.GetProperty(k_valueName);
    157.                 p_newKeyName = property.GetProperty(k_newKeyName);
    158.  
    159.                 c_key.text    = p_keyName.stringValue;
    160.                 c_val.text    = p_valueName.stringValue;
    161.                 c_newKey.text = p_newKeyName.stringValue;
    162.  
    163.                 p_values.arraySize = p_keys.arraySize;
    164.                 for(int i=0;i<p_keys.arraySize;i++)
    165.                 {
    166.  
    167.  
    168.                     if (Event.current.type == EventType.Repaint && i==selectedIndex)
    169.                     {
    170.                         GUI.backgroundColor = color_selected;
    171.  
    172.                         skin.FlatBox.Draw(r_clickRow,GUIContent.none,false,false,false,false);
    173.                         GUI.backgroundColor = color_background;
    174.                     }
    175.  
    176.                     OnDrawElement(r_clickRow,property,i);
    177.  
    178.                     r_clickRow = new Rect(r_clickRow.xMin,r_clickRow.yMin + rowHeight, r_clickRow.width, r_clickRow.height);
    179. //                    r_clickRow.y += rowHeight;
    180.                 }
    181.                 OnDrawAddRow(r_addRow, property);
    182.  
    183.  
    184.  
    185.  
    186.  
    187.                 if(removeIndex>-1)
    188.                 {
    189.                     if(selectedIndex == removeIndex)
    190.                         selectedIndex = -1;
    191.                     else if(selectedIndex>removeIndex)
    192.                         selectedIndex--;
    193.  
    194.                     p_keys.RemoveElement(removeIndex);
    195.                     p_values.RemoveElement(removeIndex);
    196.                 }
    197.             }
    198.          
    199.  
    200.             EditorGUI.indentLevel = oldIndent;
    201.             GUI.backgroundColor = backColor;
    202.             indexSelectedLastFrame = selectedIndex;
    203.  
    204.             //forces a repaint for next frame so this drawer will update every frame
    205.             // primarily so that the remove buttons appear more responsive
    206.             // to reduce overhead, only do this if the mouse is in the property
    207.             if(position.Contains(Event.current.mousePosition))
    208.                 EditorUtility.SetDirty( property.serializedObject.targetObject );
    209.         }
    210.  
    211.  
    212.  
    213.         private void OnDrawElement(Rect rect, SerializedProperty property, int index)
    214.         {
    215.  
    216.             Rect r_key    = new Rect(rect.xMin+5,  rect.yMin+2, (rect.width-40f)*0.5f, rect.height-8);
    217.             Rect r_val    = new Rect(r_key.xMax+5, rect.yMin+2, (rect.width-40f)*0.5f, rect.height-8);
    218.             Rect r_remove = new Rect(r_val.xMax+5, rect.yMin+2, 25f,                   rect.height-8);
    219.  
    220.             p_key = p_keys.GetElement(index);
    221.             p_val = p_values.GetElement(index);
    222.  
    223.  
    224.             //We don't use GUIButton here because there many be interactible GUI elements inside the row which wouldn't work when using a Button
    225.             // we want behaviour similar to button click, but without consuming the event (as internal elements might want to consume it)
    226.             if(Event.current.type == EventType.MouseUp && rect.Contains(Event.current.mousePosition))
    227.             {
    228.                 selectedIndex = index;
    229.             }
    230.  
    231.             p_key.Label (r_key,p_key.GetValueAsString(), c_key);
    232.             switch(p_val.propertyType)
    233.             {
    234.                 case SerializedPropertyType.AnimationCurve:
    235.                 case SerializedPropertyType.Bounds:
    236.                 case SerializedPropertyType.Generic:
    237.                     //these properties are complex or multi-line. simply show their name here and draw them in the selection box instead
    238.                     p_val.Label (r_val,p_val.GetValueAsString(), c_val);
    239.                     break;
    240.                 case SerializedPropertyType.ObjectReference:
    241.                     //allow the reference to be set/changed in the drawRow, object's values can be drawn in the selection box
    242.                     p_val.Field(r_val,c_val,false);
    243.                     break;
    244.                 default:
    245.                     //all other types are simple and can be drawn directly in the row
    246.                     p_val.Field(r_val,c_val);
    247.                     break;
    248.             }
    249.  
    250.  
    251.             //elements are not removed immeadiately on button press, but at the end of OnGui to avoid any IndexOutOfRange exceptions
    252.             if(!r_remove.Contains(Event.current.mousePosition))
    253.             {
    254.                 EditorGUI.LabelField(r_remove,c_remove,skin.Title);
    255.             }
    256.             else
    257.             {
    258.                 if (GUI.Button (r_remove, c_remove))
    259.                     removeIndex = index;
    260.             }
    261.  
    262.  
    263.         }
    264.  
    265.         private void OnDrawAddRow(Rect rect, SerializedProperty property)
    266.         {
    267.             Rect r_backrow = new Rect(rect.xMin,rect.yMin,rect.width,rect.height);
    268.  
    269.             if (Event.current.type == EventType.Repaint)
    270.                 skin.ListFootBackground.Draw(r_backrow,GUIContent.none,false,false,false,false);
    271.  
    272.  
    273.             Rect r_key    = new Rect(rect.xMin+5,  rect.yMin+2, rect.width - 35f, rect.height-8);
    274.             Rect r_newKey = new Rect(r_key.xMax+5, rect.yMin+2, 20f,              rect.height-8);
    275.  
    276.             p_newKey.Field(r_key,c_newKey, false);
    277.  
    278.             bool isInvalid = p_newKey.IsEmpty() || p_keys.ContainsValue(p_newKey);
    279.  
    280.             EditorGUI.BeginDisabledGroup(isInvalid);
    281.  
    282.             if(GUI.Button(r_newKey,c_add))
    283.             {
    284.                 p_keys.arraySize++;
    285.                 p_values.arraySize = p_keys.arraySize;
    286.  
    287.                 p_keys.GetElement(p_keys.arraySize-1).SetPropertyValue(p_newKey);
    288.                 p_values.GetElement(p_values.arraySize-1).SetPropertyValue(null);
    289.  
    290.                 p_newKey.SetPropertyValue(null);
    291.             }
    292.             EditorGUI.EndDisabledGroup();
    293.  
    294.         }
    295.  
    296.         private void DrawSelectionBox(Rect rect)
    297.         {
    298.             if(selectedIndex<0||SelectionBoxHeight==0) return;
    299.  
    300.             GUI.Box(rect,GUIContent.none,skin.Card);
    301.  
    302.             //provide plenty of indenting incase of dropdown arrows, plus padding for the Card itslef
    303.             rect.xMin += 15f;
    304.             rect.yMin += 5f;
    305.             rect.xMax -= 5f;
    306.             rect.yMax -= 5f;
    307.  
    308.  
    309.             p_val = p_values.GetElement(indexSelectedLastFrame);
    310.  
    311.             p_val.AbstractField(rect);
    312.         }
    313.     }
    314. }
    315.  
    316.  

    Edit: Updated my PropteryDrawer code, cleaning up some render bugs that would happen, I still kinda of want to show my WoofTools code here but I also kinda don't as the code used for the drawer alone is over 500 lines. (plus thinking about releasing it to the asset store)
     
    Last edited: Feb 26, 2018
  19. bluilisht

    bluilisht

    Joined:
    Dec 6, 2016
    Posts:
    9
    how to make this work on dictionary with value type of list or array?
     
  20. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    You'd need to wrap the list or array into a Serializable container class and make that class the value type

    Code (CSharp):
    1. [Serializable]public class  BoolList
    2. {
    3.     public string description;
    4.     public bool[] group;
    5. }
    6. [Serializable]public class  BoolListDictionary:SerializableDictionary<string,BoolList>{}
    7.  
    8. [SerializeField]private BoolListDictionary m_boolGroup = new BoolListDictionary();

    A lot of what makes that draw so well is the Abstract Height and AbstractField ExtensionMethods I wrote

    Code (CSharp):
    1.  
    2. public static float AbstractHeight(this SerializedProperty property)
    3. {
    4.     if (property == null) throw new ArgumentNullException (NullProperty);
    5.  
    6.     if (property.propertyType != SerializedPropertyType.ObjectReference)
    7.     {
    8.         return    EditorGUI.GetPropertyHeight(property);
    9.     }
    10.  
    11.     if (property.objectReferenceValue == null)
    12.         return EditorGUIUtility.singleLineHeight;
    13.  
    14.     Type concreteType = property.objectReferenceValue.GetType();
    15.     UnityEngine.Object wrapped = property.objectReferenceValue;
    16.     wrapped = (UnityEngine.Object) Convert.ChangeType(wrapped,concreteType);
    17.  
    18.     SerializedObject so_wrapped = new SerializedObject(wrapped);
    19.     if (so_wrapped == null) throw new ArgumentNullException (NullSO);
    20.          
    21.     SerializedProperty p_iterator = so_wrapped.GetIterator();
    22.     float height = 0;
    23.     bool enterchildren = true;
    24.     while (p_iterator.NextVisible(enterchildren))
    25.     {
    26.         if(p_iterator.propertyPath == "m_Script") continue;
    27.  
    28.         height += EditorGUI.GetPropertyHeight(p_iterator);
    29.  
    30.         enterchildren = false;
    31.     }
    32.     return height;
    33. }
    34.  
    35.  
    36. public static bool AbstractField(this SerializedProperty property,Rect rect)
    37. {
    38.     if (property == null) throw new ArgumentNullException (NullProperty);
    39.  
    40.     bool hasChanged =false;
    41.     switch(property.propertyType)
    42.     {
    43.         case SerializedPropertyType.ObjectReference:
    44.             if (property.objectReferenceValue == null)
    45.             {
    46.                 //field is null show the object field instead so they can inject an instance
    47.                 EditorGUI.BeginChangeCheck ();
    48.                 property.Field(rect,false);
    49.                 return EditorGUI.EndChangeCheck();
    50.             }
    51.  
    52.             Type concreteType = property.objectReferenceValue.GetType();
    53.             UnityEngine.Object wrapped = property.objectReferenceValue;
    54.             wrapped = (UnityEngine.Object) Convert.ChangeType(wrapped,concreteType);
    55.  
    56.             SerializedObject so_wrapped = new SerializedObject(wrapped);
    57.             if (so_wrapped == null) throw new ArgumentNullException (NullSO);
    58.  
    59.             SerializedProperty p_iterator = so_wrapped.GetIterator();
    60.             Rect r_prop = new Rect(rect.xMin,rect.yMin,rect.width,0);
    61.             EditorGUI.BeginChangeCheck ();
    62.             bool enterchildren = true;
    63.             while (p_iterator.NextVisible(enterchildren))
    64.             {
    65.                 if(p_iterator.propertyPath == "m_Script") continue;
    66.  
    67.                 r_prop.y += r_prop.height ;
    68.                 r_prop.height = EditorGUI.GetPropertyHeight(p_iterator);
    69.                 EditorGUI.PropertyField(r_prop,p_iterator,true);
    70.                 enterchildren = false;
    71.             }
    72.  
    73.             hasChanged = EditorGUI.EndChangeCheck();
    74.             if(hasChanged) so_wrapped.ApplyModifiedProperties();
    75.  
    76.             return hasChanged;
    77.  
    78.         case SerializedPropertyType.Generic:
    79.             property.Label(new Rect(rect.xMin+15f,rect.yMin,rect.width-15f,EditorGUIUtility.singleLineHeight),property.displayName);
    80.             goto default;
    81.  
    82.         default:
    83.             EditorGUI.BeginChangeCheck ();
    84.             property.Field(rect,true);
    85.             return EditorGUI.EndChangeCheck();
    86.     }
    87. }
     
    Last edited: Feb 26, 2018
  21. WillNode

    WillNode

    Joined:
    Nov 28, 2013
    Posts:
    423
    Hey vexe,

    Thank for the awesome dictionary script!

    Actually I've made my own editor version. Nothing different. Just the design part.

    Screenshot (181).png

    Code (csharp):
    1.  
    2.     using System;
    3.     using System.Collections.Generic;
    4.     using UnityEditor;
    5.     using UnityEngine;
    6.     using UnityObject = UnityEngine.Object;
    7.  
    8. public abstract class DictionaryDrawer<TK, TV> : PropertyDrawer
    9. {
    10.     private SerializableDictionary<TK, TV> _Dictionary;
    11.     private bool _Foldout;
    12.     private const float kButtonWidth = 22f;
    13.     private const int kMargin = 3;
    14.    
    15.     static GUIContent iconToolbarMinus = EditorGUIUtility.IconContent("Toolbar Minus", "Remove selection from list");
    16.     static GUIContent iconToolbarPlus = EditorGUIUtility.IconContent("Toolbar Plus", "Add to list");
    17.     static GUIStyle preButton = "RL FooterButton";
    18.     static GUIStyle boxBackground = "RL Background";
    19.  
    20.  
    21.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    22.     {
    23.         CheckInitialize(property, label);
    24.         if (_Foldout)
    25.             return Mathf.Max((_Dictionary.Count + 1) * 17f, 17 + 16) + kMargin * 2;
    26.         return 17f + kMargin * 2;
    27.     }
    28.  
    29.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    30.     {
    31.         CheckInitialize(property, label);
    32.  
    33.         var backgroundRect = position;
    34.         backgroundRect.xMin -= 7;
    35.         backgroundRect.height += kMargin;
    36.         if (Event.current.type == EventType.Repaint)
    37.             boxBackground.Draw(backgroundRect, false, false, false, false);
    38.  
    39.         position.y += kMargin;
    40.         position.height = 17f;
    41.  
    42.         var foldoutRect = position;
    43.         foldoutRect.width -= 2 * kButtonWidth;
    44.         EditorGUI.BeginChangeCheck();
    45.         _Foldout = EditorGUI.Foldout(foldoutRect, _Foldout, label, true);
    46.         if (EditorGUI.EndChangeCheck())
    47.             EditorPrefs.SetBool(label.text, _Foldout);
    48.  
    49.         position.xMin += kMargin;
    50.         position.xMax -= kMargin;
    51.        
    52.         var buttonRect = position;
    53.         buttonRect.xMin = position.xMax - kButtonWidth;
    54.         //buttonRect.x -= 5;
    55.  
    56.         if (GUI.Button(buttonRect, iconToolbarMinus, preButton))
    57.         {
    58.             ClearDictionary();
    59.         }
    60.  
    61.         buttonRect.x -= kButtonWidth - 1;
    62.  
    63.         if (GUI.Button(buttonRect, iconToolbarPlus, preButton))
    64.         {
    65.             AddNewItem();
    66.         }
    67.  
    68.         if (!_Foldout)
    69.             return;
    70.  
    71.         var labelRect = position;
    72.         labelRect.y += 16;
    73.         if (_Dictionary.Count == 0)
    74.             GUI.Label(labelRect, "This dictionary doesn't have any items. Click + to add one!");
    75.  
    76.         foreach (var item in _Dictionary)
    77.         {
    78.             var key = item.Key;
    79.             var value = item.Value;
    80.  
    81.             position.y += 17f;
    82.  
    83.             var keyRect = position;
    84.             keyRect.width /= 2;
    85.             keyRect.width -= 4;
    86.             EditorGUI.BeginChangeCheck();
    87.             var newKey = DoField(keyRect, typeof(TK), key);
    88.             if (EditorGUI.EndChangeCheck())
    89.             {
    90.                 try
    91.                 {
    92.                     _Dictionary.Remove(key);
    93.                     _Dictionary.Add(newKey, value);
    94.                 }
    95.                 catch (Exception e)
    96.                 {
    97.                     Debug.Log(e.Message);
    98.                 }
    99.                 break;
    100.             }
    101.  
    102.             var valueRect = position;
    103.             valueRect.xMin = keyRect.xMax;
    104.             valueRect.xMax = position.xMax - kButtonWidth;
    105.             EditorGUI.BeginChangeCheck();
    106.             value = DoField(valueRect, typeof(TV), value);
    107.             if (EditorGUI.EndChangeCheck())
    108.             {
    109.                 _Dictionary[key] = value;
    110.                 break;
    111.             }
    112.  
    113.             var removeRect = position;
    114.             removeRect.xMin = removeRect.xMax - kButtonWidth;
    115.             if (GUI.Button(removeRect, iconToolbarMinus, preButton))
    116.             {
    117.                 RemoveItem(key);
    118.                 break;
    119.             }
    120.         }
    121.     }
    122.  
    123.     private void RemoveItem(TK key)
    124.     {
    125.         _Dictionary.Remove(key);
    126.     }
    127.  
    128.     private void CheckInitialize(SerializedProperty property, GUIContent label)
    129.     {
    130.         if (_Dictionary == null)
    131.         {
    132.             var target = property.serializedObject.targetObject;
    133.             _Dictionary = fieldInfo.GetValue(target) as SerializableDictionary<TK, TV>;
    134.             if (_Dictionary == null)
    135.             {
    136.                 _Dictionary = new SerializableDictionary<TK, TV>();
    137.                 fieldInfo.SetValue(target, _Dictionary);
    138.             }
    139.  
    140.             _Foldout = EditorPrefs.GetBool(label.text);
    141.         }
    142.     }
    143.  
    144.     private static readonly Dictionary<Type, Func<Rect, object, object>> _Fields =
    145.         new Dictionary<Type, Func<Rect, object, object>>()
    146.         {
    147.                 { typeof(int), (rect, value) => EditorGUI.IntField(rect, (int)value) },
    148.                 { typeof(float), (rect, value) => EditorGUI.FloatField(rect, (float)value) },
    149.                 { typeof(string), (rect, value) => EditorGUI.TextField(rect, (string)value) },
    150.                 { typeof(bool), (rect, value) => EditorGUI.Toggle(rect, (bool)value) },
    151.                 { typeof(Vector2), (rect, value) => EditorGUI.Vector2Field(rect, GUIContent.none, (Vector2)value) },
    152.                 { typeof(Vector3), (rect, value) => EditorGUI.Vector3Field(rect, GUIContent.none, (Vector3)value) },
    153.                 { typeof(Bounds), (rect, value) => EditorGUI.BoundsField(rect, (Bounds)value) },
    154.                 { typeof(Rect), (rect, value) => EditorGUI.RectField(rect, (Rect)value) },
    155.         };
    156.  
    157.     private static T DoField<T>(Rect rect, Type type, T value)
    158.     {
    159.         Func<Rect, object, object> field;
    160.         if (_Fields.TryGetValue(type, out field))
    161.             return (T)field(rect, value);
    162.  
    163.         if (type.IsEnum)
    164.             return (T)(object)EditorGUI.EnumPopup(rect, (Enum)(object)value);
    165.  
    166.         if (typeof(UnityObject).IsAssignableFrom(type))
    167.             return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true);
    168.  
    169.         Debug.Log("Type is not supported: " + type);
    170.         return value;
    171.     }
    172.  
    173.     private void ClearDictionary()
    174.     {
    175.         _Dictionary.Clear();
    176.     }
    177.  
    178.     private void AddNewItem()
    179.     {
    180.         TK key;
    181.         if (typeof(TK) == typeof(string))
    182.             key = (TK)(object)"";
    183.         else key = default(TK);
    184.  
    185.         var value = default(TV);
    186.         try
    187.         {
    188.             _Dictionary.Add(key, value);
    189.         }
    190.         catch (Exception e)
    191.         {
    192.             Debug.Log(e.Message);
    193.         }
    194.     }
    195. }
    196.  
    197. // This one is for your custom-made dictionaries.
    198. [CustomPropertyDrawer(typeof(DictionaryString))]
    199.     public class DictionaryStringStringDrawer : DictionaryDrawer<string, string> { }
    200.  
    201.  
    Feel free to use the code!
     
  22. EyePD

    EyePD

    Joined:
    Feb 26, 2016
    Posts:
    63
    sand_lantern, Nick62 and tarun2000 like this.
  23. BitwiseGuy

    BitwiseGuy

    Joined:
    Mar 14, 2013
    Posts:
    4
    I was struggling with this as well, and even putting SetDirty even when nothing was changed was doing it. It turns out that SetDirty doesn't do quite what you'd expect any more, and you need to use Undo.RecordObject() now.

    I'm still using Vexe's original code but the change can likely be used for any version - essentially you just need to add
    Code (csharp):
    1. Undo.RecordObject(property.serializedObject.targetObject, "Undo Dictionary Change");
    to the beginning of your OnGUI.
     
  24. hawaiian_lasagne

    hawaiian_lasagne

    Joined:
    May 15, 2013
    Posts:
    123
    Thank you man! This just worked out of the box. Great work
     
  25. Xepherys

    Xepherys

    Joined:
    Sep 9, 2012
    Posts:
    204
    @vexe - this is good stuff. Thought I'd drop this in for any future readers that may still be learning C# or Unity Editor extensions. My dictionary is keyed with an int, but the default AddNewItem in the PropertyDrawer always tried to use `default(TK)` which is `0`. You can add to this by adding an elseif to the AddNewItems for int, as such:


    Code (CSharp):
    1.     private void AddNewItem()
    2.     {
    3.         TK key;
    4.         if (typeof(TK) == typeof(string))
    5.             key = (TK)(object)"";
    6.         else if (typeof(TK) == typeof(int))
    7.             key = (TK)(object)_Dictionary.Count;
    8.         else key = default(TK);
    9.  
    10.         var value = default(TV);
    11.         try
    12.         {
    13.             _Dictionary.Add(key, value);
    14.         }
    15.         catch (Exception e)
    16.         {
    17.             Debug.Log(e.Message);
    18.         }
    19.     }
    Really, you can extend this to pretty much any keyed type you might be using if you need it to auto-increment a value (this could even be a string or, really, anything if you create it correctly).
     
  26. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    Hi @vexe:

    Im a noob, but how do i persist the values across Unity reloads?

    Shouldnt the SerializableDictionary data save to scene .asset as soon as i close the scene/modify the values? Do i need to mark it Dirty myself whenever i add/change value in the dictionary? As at the moment when i exit Unity and come back, the SerializableDictionary data i set via inspector is gone. Do i need to use XML/PlayerPrefs/binary external file? This seems like a lot of extra work, as i am storing simple scene/gameplay-related data, and this data doesnt need to change in playmode (only be set in edit mode).

    EDIT: so Im now marking the scene dirty every time the field value is changed or item is added/removed using:
    Code (csharp):
    1. EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
    This does persist the values across engine reloads, as the values are properly saved into scene's .asset file when i hit Ctrl+S. Guess one could just save the scene on each change using
    Code (csharp):
    1. EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene());
    but that seems even bigger overkill than the mark dirty one.

    Overkill? Is there lighter/better way to persist the values across engine reloads?
     
    Last edited: Feb 23, 2018
    Pawl likes this.
  27. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    You can do a couple of things:
    1) do not destroy your dictionary between scenes (dont destroy on load)

    2) serialize dictionary to a file and load it hwen you need.
    Binary formatter wont serialize an unity dictionari this is why you would need this but....

    3) You could use json.net (now free) serialize that will crush unity dictionaries into binary files without issues. The only con is that it does feel slower so you may whant to write to dicitonary once you've all values in dictionary set and don´t save to file as you could with binary formatter and serializable dictionary as seemed to have almost not performance hit.
     
    genaray likes this.
  28. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    Nono, my use case is different, i dont need to save to harddrive using other means than built-in unity SCENE serialization - which should happen automatically when object/scene is marked dirty. As i mentioned i only need to save in EditMode, and read in play mode. Also i am only using 1 scene. This should be serializable into the scene asset file. As i wrote already in the update to my post, when i mark the scene as dirty in OnGUI, i can save the scene and it will persist the changes. Meaning it did serialize into scene .asset file. No XML/BinaryFormatter/PlayerPrefs/whatever needed.

    What im looking now at is why its not saving/Dirty-ing when i change the value of a key, or value of value. I can see @vexe is already using the EditorGUI.EndChangeCheck, which should run when i modify the above mentioned. It runs but doesnt Dirty the object so the changes arent serialized into scene .asset file. Im almost there. I dont want to mark the whole scene dirty, only the object, but i cant mark _Dictionary dirty, since its not an object.. Im pretty noob at this so im slow..

    EDIT: i've updated my original post; and unless someone comes with a better hint for me to use, ill resign to use Scene dirtying to persist the changes.
     
    Last edited: Feb 23, 2018
    genaray likes this.
  29. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Good evening !

    First of all great stuff and thanks for this "tutorial" !

    But i have a iusse... i want to add an custom data holder to my dictionary, somehow i always get following error "Type of EntityData is not supported"... could someone take a look at my code and tell me where the iusse is ?


    Serializable]
    public class EntityData
    {
    [SerializeField]
    public GameObject visible;
    [SerializeField]
    public GameObject transparent;
    }
    [CreateAssetMenu(fileName = "EntityHolder", menuName = "ParallelOrigin/Map/Entitys/EntityHolder", order = 1)]
    public class EntityHolder : ScriptableObject {
    [Serializable]
    public class EntitysDictionary : SerializableDictionary<int, EntityData> { }
    [CustomPropertyDrawer(typeof(EntitysDictionary))]
    public class EntitysDictionaryDrawer : DictionaryDrawer<int, EntityData> { }
    [SerializeField]
    public EntitysDictionary entitysDictionary = new EntitysDictionary();
    }


    Thanks a lot for your time and effort ! :)
     
  30. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    as far as I know Unity cannot serialize GameObject with BinaryFormatter. I'm assuming youre using BF tho.
     
    genaray likes this.
  31. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Well thats strange... when i just use a gameobject as second parameter (
    public class EntitysDictionary : SerializableDictionary<int, GameObject> { } ) It works fine. So theres no work around for it ? When i change EntityData like this :

    [Serializable]
    public class EntityData
    {
    [SerializeField]
    public string visible;
    [SerializeField]
    public string transparent;
    }


    it still get the same error message and i dont use gameobjects anywhere else in the dic.
     
  32. TooManySugar

    TooManySugar

    Joined:
    Aug 2, 2015
    Posts:
    864
    1st. Not sure why you would need to serialize a gameobject. you would rather serialize values of specific interest as you did in your second attemp.

    I think you're missing a constructor.

    This is one of the values I do serialize in my dictionary.

    This specific code uses regular dicitonaries because I moved to Json.net due to win store not using BF but I would say [serialized field] is not necesary

    Code (CSharp):
    1. public class TankProperties   {
    2.  
    3.     public bool isPlayer;
    4.     public int team;
    5.     public int faction;
    6.     public int  vehicleType;
    7.     //public string model;
    8.     public vehicleModel model;
    9.     public int tier;
    10.     public int skin;
    11.     public int count;
    12.     public int era;
    13.  
    14.  
    15.     //public  TankProperties  (bool _isPlayer, int _team, int _faction,int  _vehicleType, string _model, int _tier , int _skin, int _count)
    16.     public  TankProperties  (bool _isPlayer, int _team, int _faction,int  _vehicleType, vehicleModel _model, int _tier , int _skin, int _count, int _era)
    17.  
    18.     {
    19.         isPlayer     =     _isPlayer;
    20.         team         =    _team;
    21.         faction     =     _faction;
    22.         vehicleType =     _vehicleType;
    23.         model         =    _model;
    24.         tier         =     _tier;
    25.         skin         =    _skin;
    26.         count         =    _count;
    27.         era            =   _era;
    28.     }
    29.  
    30. }

    edit: for the type of field you're using like transparent etc... I think I would use a bool rather than a string. I'm far from a pro but I think you may first go trought all the entry level tutorials.
     
  33. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191

    No matter what i do... it doesnt work somehow. I always get following error message : "Type EntityHolder not supported". I need to assign Gameobjects to the dictionary in my case.
     
  34. Rotary-Heart

    Rotary-Heart

    Joined:
    Dec 18, 2012
    Posts:
    813
    Deleted User and genaray like this.
  35. TomPo

    TomPo

    Joined:
    Nov 30, 2013
    Posts:
    86
    Hi
    Simple directory solution working like a charm, but I have a problem with UnityEngine.Object as key.
    Read the manual and did what you wrote but got an error:
    Couldn't find [classname] reference.

    I need to have a ScriptableObject as a key.

    I've added to the RequiredReferences.cs this piece of code
    Code (CSharp):
    1. [SerializeField]
    2. private ScriptableObject _so;
    I've dragged and dropped Reference ScriptableObject on my DIctionary but when trying to add an element I got this error I've mentioned.
    I've tried to drag and drop my empty SO as a reference on your ReferenceScriptableObject but the error is still the same.

    I'm confused :/
     
  36. Pawl

    Pawl

    Joined:
    Jun 23, 2013
    Posts:
    113
    I ran into a similar issue as @_watcher_ where I'm using a SerializableDictionary inside of a prefab (Unity 2019.1.8f1) and I wasn't seeing the changes persist, even after manually saving the open scene.

    After a little digging, I ended up adding the following code snippet to the PropertyDrawer to run anytime a change was made inside OnGUI().
    Code (CSharp):
    1. private void DidChangeProperty(SerializedProperty property) {
    2.     var target = property.serializedObject.targetObject;
    3.     PrefabUtility.RecordPrefabInstancePropertyModifications(target);
    4.     EditorSceneManager.MarkSceneDirty(UnityEngine.SceneManagement.SceneManager.GetActiveScene());
    5. }
    I still have to manually click Overrides -> Apply All on the prefab instance, but at least now the changes persist.

    Big thanks to @vexe for saving me a ton of time with the initial implementation!

    OnGUI() integration code for completeness below:
     
    _watcher_ likes this.
  37. Bubsavvy

    Bubsavvy

    Joined:
    Sep 18, 2017
    Posts:
    48
    @lordofduct as of unity 2019.1.9f1 your serialized dictionary property drawer does not seem to work. It has something to do with arraySize++. I think there may have been a change internally that does not allow array elements to be added via this trick anymore. Any ideas on how to get past this?
     
  38. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I'd have to look at the documentation for the new changes to see how you add new elements to an array. I'm not currently only 2019*. I'll take a look when I have a break from work.
     
    Bubsavvy likes this.
  39. Rotary-Heart

    Rotary-Heart

    Joined:
    Dec 18, 2012
    Posts:
    813
    I don't think that's the issue, since I'm using it on my asset and I'm currently using it on 2019* version.
     
    Bubsavvy likes this.
  40. Bubsavvy

    Bubsavvy

    Joined:
    Sep 18, 2017
    Posts:
    48
    What sub-version are you using? I can download and test on that sub-version. Maybe its an issue with the sub-version I am on. When I forked the space puppy repo, I stripped out everything except for the dictionary utilities. Maybe there is something I should have kept?

    • SerializableDictionaryBase
    • ConvertUtil
    • DictionaryPropertyDrawer
    To be fair. This version has had some strange bugs that occur in the inspector when there is large amounts of items displayed in foldouts so maybe it is just this sub-version.
     
  41. Rotary-Heart

    Rotary-Heart

    Joined:
    Dec 18, 2012
    Posts:
    813
    I have it tested on 2019.1.14 and currently using it on 2019.2.0
     
  42. K0ST4S

    K0ST4S

    Joined:
    Feb 2, 2017
    Posts:
    35
    One common cause of boxing is the use of enum types as keys for Dictionaries. Declaring an enum creates a new value type that is treated like an integer behind the scenes, but enforces type-safety rules at compile time. That's why writting your own EqualityComparer for enum keyed dictionaries is important. Has anyone managed to write a generic serializable dictionary with variable EqualityComparer?
     
  43. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    The problem with that isn't with the Dictionary, but that the Dictionary defaults to EqualityComparer<T>.Default for the given type. And the enum has to be dealt with in a special way.

    So to write one requires some way to generically create an EqualityComparer for enums that doesn't cause boxing.

    In older versions of .net, not as easy.

    But in newer versions of .net, it is doable. MS has already done it.

    Here you'll see that Dictionary still uses EqualityComparer<T>.Default:
    https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,94

    And here in EqualityComparer<T>.CreateComparer it has special case to deal with enums:
    https://referencesource.microsoft.com/#mscorlib/system/collections/generic/equalitycomparer.cs,61

    It creates these special enum specific comparers:
    https://referencesource.microsoft.com/#mscorlib/system/collections/generic/equalitycomparer.cs,384

    That rely on this JitHelper to get it as its respective numeric type:
    https://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/jithelpers.cs,179

    And when targeting 4.x in newer versions of Unity it doesn't create garbage. As can be see here: upload_2020-7-24_10-15-6.png

    The only thing creating garbage here is my Debug.Log:
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using System.Linq;
    6.  
    7. public class zTest01 : MonoBehaviour
    8. {
    9.  
    10.     public enum SomeEnum
    11.     {
    12.         A,
    13.         B,
    14.         C
    15.     }
    16.  
    17.     public SomeEnum A;
    18.     public SomeEnum B;
    19.  
    20.     private void Update()
    21.     {
    22.         bool result = IsEqual(this.A, this.B);
    23.  
    24.         Debug.Log(result ? "TRUE" : "FALSE");
    25.     }
    26.  
    27.     public bool IsEqual(SomeEnum a, SomeEnum b)
    28.     {
    29.         return System.Collections.Generic.EqualityComparer<SomeEnum>.Default.Equals(a, b);
    30.     }
    31.  
    32. }
    33.  
    No Debug.Log, no garbage:
    upload_2020-7-24_10-16-47.png
     
    Last edited: Jul 24, 2020
  44. HITBZack

    HITBZack

    Joined:
    Apr 23, 2020
    Posts:
    1
    I just wanted to say thank you for this, works beyond flawless for me in 2020.1.1f1
     
    Last edited: Sep 24, 2020
  45. Dr_McFish

    Dr_McFish

    Joined:
    Oct 26, 2020
    Posts:
    1
    Hey, so is there a way to set initial keys to this dictionary when you create it inside the MonoBehavier script so that it shows it to you in the editor?

    This is what I have:
    Code (CSharp):
    1. [System.Serializable] public class MuscleDictionary : SerializableDictionary<string, MuscleSpring>{
    2.     public MuscleDictionary(params string[] args){
    3.         foreach(string arg in args){
    4.             this.Add(arg, null);
    5.         }
    6.     }
    7.     ~MuscleDictionary(){
    8.         Debug.Log("Dictionary destroyed");
    9.     }
    10. }
    (MuscleSpring is another class that inherits from monobehavier)

    then I have this
    Code (CSharp):
    1. public class MuscleControler : MonoBehaviour
    2. {
    3.     public MuscleDictionary muscles = new MuscleDictionary("Leg1", "Leg2");
    4.     (...)
    5. }
    but I do not see these entries in the inspector.

    even more strangely, if I put
    Code (CSharp):
    1.  
    2. vod Start(){
    3.     muscles = new MuscleDictionary("Leg1", "Leg2");
    4. }
    then I can see the 2 entries in the inspector when I hit play, and they disappear when I exit out. The dictionary seems to get destroyed before void Start(). I am new to Unity, please excuse my newbieness
     
    Last edited: Oct 28, 2020
  46. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    SerializableDictionary relies on using backing variables to store the keys and values (likely stored in private arrays or lists) that will be serialized. These backing variables are only used during serialization and its these values that the inspector reads and writes to. "this.Add()" is likely just directly adding to the internal dictionary and it not updating the backing variables. The internal dictionary is what is used during runtime and it is a completely separate data structure. The Inspector can't read dictionaries which is why the SerializableDictionary class exists, its an adapter. Your constructor likely should also need to update the backing variables if you want it to show those default values in the inspector.

    Possibly because the first dictionary you initialized with was dereferenced when you created the new dictionary to replace it in Start(). The monobehaviour instances during editmode are different instances from the ones you see during play mode. For scene instances, Unity serialzes a snapshot of the data during edit mode before entering playmode. It creates a new instance in playmode and serializes the snapshot into the instance to get everything into the starting state. When you return from playmode, Unity deletes those instances, makes new ones, and recovers that snapshot so that all the data returns to its inital state. Project assets on the other hand can be modified during playmode and those modifications can persist when you return to editmode.
     
  47. lennardbeers

    lennardbeers

    Joined:
    Apr 28, 2017
    Posts:
    10
    I had a problem with using this with the TileBase class.
    I Think the same would happen for every ScriptableObject inherited type.

    Whenever I clicked on the add button, I got a key cannot be null error.
    This is because you get null when trying to do default(T) with T being a ScriptableObject.

    I fixed this by modifying the DictionaryDrawer class and SerializableDictionary class:

    In SerializableDictionary.cs I added this field:

    Code (CSharp):
    1.  
    2.     [SerializeField, HideInInspector] public TKey DefaultKey;
    3.  
    In DictionaryDrawer.cs I added to OnGUI():

    Code (CSharp):
    1.        
    2.     EditorGUILayout.Space();
    3.  
    4.     if (typeof(UnityObject).IsAssignableFrom(typeof(TK)))
    5.         _Dictionary.DefaultKey = (TK)(object)EditorGUILayout.ObjectField("Default Key", (UnityObject)(object)_Dictionary.DefaultKey, typeof(TK), false);
    6.  

    and modified AddNewItem():

    Code (CSharp):
    1.    
    2.     private void AddNewItem()
    3.     {
    4.         TK key;
    5.         if (typeof(TK) == typeof(string))
    6.             key = (TK)(object)"";
    7.         else key = default(TK);
    8.  
    9.         if (key == null)
    10.         {
    11.             if (_Dictionary.DefaultKey != null) key = _Dictionary.DefaultKey;
    12.         }
    13.  
    14.         var value = default(TV);
    15.         try
    16.         {
    17.             _Dictionary.Add(key, value);
    18.         }
    19.         catch (Exception e)
    20.         {
    21.             Debug.Log(e.Message);
    22.         }
    23.     }
    24.  
    You can then select a default key to add to the dictionary, when pressing the add item button.
    I only need this for the key, but for the value it could just be copied and would work the same way.

    I hope this helps.
     
  48. losingisfun

    losingisfun

    Joined:
    May 26, 2016
    Posts:
    36
    I know it's been like 5 years since the start of this thread. Has the Unity dev team seriously not considered serializing dictionaries yet?! Any decent programmer is using dictionaries regularly it seems bizarre that these aren't supported for serialization.

    Anyone have their ear to the ground on this?
     
  49. ZhavShaw

    ZhavShaw

    Joined:
    Aug 12, 2013
    Posts:
    173
    I've always wondered if implementing features like this into the engine is too complicated, too much work, or being held back to sell assets.
     
    MikeMnD likes this.
  50. losingisfun

    losingisfun

    Joined:
    May 26, 2016
    Posts:
    36
    For more complex features that seems true. However, this is a fairly basic programming need. It's like if Unity could serialise Arrays but not Lists. That would be bizarre considering how common they are.

    It's basically the same thing. I'm using Dictionaries just as often as arrays and lists. I wouldn't even call it a feature it's just an expected level of support for any basic programming.
     
    gabrimo, Ne0mega, NMJ_GD and 4 others like this.