Skip to content

Instantly share code, notes, and snippets.

@Marsgames
Last active May 10, 2024 00:02
Show Gist options
  • Save Marsgames/88fccdd22e50c326e835377ff88d8c2a to your computer and use it in GitHub Desktop.
Save Marsgames/88fccdd22e50c326e835377ff88d8c2a to your computer and use it in GitHub Desktop.
Unity scripts to show dictionaries in the inspector

Dictionaries shown in inspector

Render


  • Put DictionaryDrawer.cs into your Assets/Editor folder

  • Put SerializableDictionary.cs into your Scripts folder

  • Create a custom dictionary

[Serializable] public class CustomDictionary : SerializableDictionary<string, bool> { } 
// Set the key / value type that you want
  • Modify DictionaryDrawer.cs (you should do this for each custom dictionaries that you created)

[CustomPropertyDrawer(typeof(CustomDictionary))] // Name of your class (same as above)
public class CustomDictionaryDrawer : DictionaryDrawer<string, bool> { } // chose same types as your dictionary

(Si on veut créer un dictionaire ayant pour clé un GameObject, utiliser à la place UnityEngine.Object, et on pourra le remplir de GameObject)




Sources

// THIS SHOULD BE PUT IN YOUR ASSETS/EDITOR FOLDER
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityObject = UnityEngine.Object;
[HelpURL("https://forum.unity.com/threads/finally-a-serializable-dictionary-for-unity-extracted-from-system-collections-generic.335797/page-2")]
public abstract class DictionaryDrawer<TK, TV> : PropertyDrawer
{
private SerializableDictionary<TK, TV> _Dictionary;
private bool _Foldout;
private const float kButtonWidth = 22f;
private const int kMargin = 3;
static readonly GUIContent iconToolbarMinus = EditorGUIUtility.IconContent("Toolbar Minus", "Remove selection from list");
static readonly GUIContent iconToolbarPlus = EditorGUIUtility.IconContent("Toolbar Plus", "Add to list");
static readonly GUIStyle preButton = "RL FooterButton";
static readonly GUIStyle boxBackground = "RL Background";
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
CheckInitialize(property, label);
if (_Foldout)
return Mathf.Max((_Dictionary.Count + 1) * 17f, 17 + 16) + kMargin * 2;
return 17f + kMargin * 2;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
CheckInitialize(property, label);
var backgroundRect = position;
backgroundRect.xMin -= 7;
backgroundRect.height += kMargin;
if (Event.current.type == EventType.Repaint)
boxBackground.Draw(backgroundRect, false, false, false, false);
position.y += kMargin;
position.height = 17f;
var foldoutRect = position;
foldoutRect.width -= 2 * kButtonWidth;
EditorGUI.BeginChangeCheck();
_Foldout = EditorGUI.Foldout(foldoutRect, _Foldout, label, true);
if (EditorGUI.EndChangeCheck())
EditorPrefs.SetBool(label.text, _Foldout);
position.xMin += kMargin;
position.xMax -= kMargin;
var buttonRect = position;
buttonRect.xMin = position.xMax - kButtonWidth;
if (GUI.Button(buttonRect, iconToolbarMinus, preButton))
{
ClearDictionary();
}
buttonRect.x -= kButtonWidth - 1;
if (GUI.Button(buttonRect, iconToolbarPlus, preButton))
{
AddNewItem();
}
if (!_Foldout)
return;
var labelRect = position;
labelRect.y += 16;
if (_Dictionary.Count == 0)
GUI.Label(labelRect, "This dictionary doesn't have any items. Click + to add one!");
foreach (var item in _Dictionary)
{
var key = item.Key;
var value = item.Value;
position.y += 17f;
var keyRect = position;
keyRect.width /= 2;
keyRect.width -= 4;
EditorGUI.BeginChangeCheck();
var newKey = DoField(keyRect, typeof(TK), key);
if (EditorGUI.EndChangeCheck())
{
try
{
_Dictionary.Remove(key);
_Dictionary.Add(newKey, value);
}
catch (Exception e)
{
Debug.Log(e.Message);
}
break;
}
var valueRect = position;
valueRect.xMin = keyRect.xMax;
valueRect.xMax = position.xMax - kButtonWidth;
EditorGUI.BeginChangeCheck();
value = DoField(valueRect, typeof(TV), value);
if (EditorGUI.EndChangeCheck())
{
_Dictionary[key] = value;
break;
}
var removeRect = position;
removeRect.xMin = removeRect.xMax - kButtonWidth;
if (GUI.Button(removeRect, iconToolbarMinus, preButton))
{
RemoveItem(key);
break;
}
}
}
private void RemoveItem(TK key)
{
_Dictionary.Remove(key);
}
private void CheckInitialize(SerializedProperty property, GUIContent label)
{
if (_Dictionary == null)
{
var target = property.serializedObject.targetObject;
_Dictionary = fieldInfo.GetValue(target) as SerializableDictionary<TK, TV>;
if (_Dictionary == null)
{
_Dictionary = new SerializableDictionary<TK, TV>();
fieldInfo.SetValue(target, _Dictionary);
}
_Foldout = EditorPrefs.GetBool(label.text);
}
}
private static readonly Dictionary<Type, Func<Rect, object, object>> _Fields =
new Dictionary<Type, Func<Rect, object, object>>()
{
{ typeof(int), (rect, value) => EditorGUI.IntField(rect, (int)value) },
{ typeof(float), (rect, value) => EditorGUI.FloatField(rect, (float)value) },
{ typeof(string), (rect, value) => EditorGUI.TextField(rect, (string)value) },
{ typeof(bool), (rect, value) => EditorGUI.Toggle(rect, (bool)value) },
{ typeof(Vector2), (rect, value) => EditorGUI.Vector2Field(rect, GUIContent.none, (Vector2)value) },
{ typeof(Vector3), (rect, value) => EditorGUI.Vector3Field(rect, GUIContent.none, (Vector3)value) },
{ typeof(Bounds), (rect, value) => EditorGUI.BoundsField(rect, (Bounds)value) },
{ typeof(Rect), (rect, value) => EditorGUI.RectField(rect, (Rect)value) },
};
private static T DoField<T>(Rect rect, Type type, T value)
{
if (_Fields.TryGetValue(type, out Func<Rect, object, object> field))
return (T)field(rect, value);
if (type.IsEnum)
return (T)(object)EditorGUI.EnumPopup(rect, (Enum)(object)value);
if (typeof(UnityObject).IsAssignableFrom(type))
return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true);
Debug.Log("Type is not supported: " + type);
return value;
}
private void ClearDictionary()
{
_Dictionary.Clear();
}
private void AddNewItem()
{
TK key;
if (typeof(TK) == typeof(string))
key = (TK)(object)"";
else key = default;
var value = default(TV);
try
{
_Dictionary.Add(key, value);
}
catch (Exception e)
{
Debug.Log(e.Message);
}
}
}
//[CustomPropertyDrawer(typeof(CustomDictionary))]
//public class CustomDictionaryDrawer : DictionaryDrawer<string, bool> { }
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using UnityEngine;
[Serializable, DebuggerDisplay("Count = {Count}"), HelpURL("https://forum.unity.com/threads/finally-a-serializable-dictionary-for-unity-extracted-from-system-collections-generic.335797/")]
public class SerializableDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
[SerializeField, HideInInspector] int[] _Buckets;
[SerializeField, HideInInspector] int[] _HashCodes;
[SerializeField, HideInInspector] int[] _Next;
[SerializeField, HideInInspector] int _Count;
[SerializeField, HideInInspector] int _Version;
[SerializeField, HideInInspector] int _FreeList;
[SerializeField, HideInInspector] int _FreeCount;
[SerializeField, HideInInspector] TKey[] _Keys;
[SerializeField, HideInInspector] TValue[] _Values;
readonly IEqualityComparer<TKey> _Comparer;
// Mainly for debugging purposes - to get the key-value pairs display
public Dictionary<TKey, TValue> AsDictionary
{
get { return new Dictionary<TKey, TValue>(this); }
}
public int Count
{
get { return _Count - _FreeCount; }
}
public TValue this[TKey key, TValue defaultValue]
{
get
{
int index = FindIndex(key);
if (index >= 0)
return _Values[index];
return defaultValue;
}
}
public TValue this[TKey key]
{
get
{
int index = FindIndex(key);
if (index >= 0)
return _Values[index];
throw new KeyNotFoundException(key.ToString());
}
set { Insert(key, value, false); }
}
public SerializableDictionary()
: this(0, null)
{
}
public SerializableDictionary(int capacity)
: this(capacity, null)
{
}
public SerializableDictionary(IEqualityComparer<TKey> comparer)
: this(0, comparer)
{
}
public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException("capacity");
Initialize(capacity);
_Comparer = (comparer ?? EqualityComparer<TKey>.Default);
}
public SerializableDictionary(IDictionary<TKey, TValue> dictionary)
: this(dictionary, null)
{
}
public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
: this((dictionary != null) ? dictionary.Count : 0, comparer)
{
if (dictionary == null)
throw new ArgumentNullException("dictionary");
foreach (KeyValuePair<TKey, TValue> current in dictionary)
Add(current.Key, current.Value);
}
public bool ContainsValue(TValue value)
{
if (value == null)
{
for (int i = 0; i < _Count; i++)
{
if (_HashCodes[i] >= 0 && _Values[i] == null)
return true;
}
}
else
{
var defaultComparer = EqualityComparer<TValue>.Default;
for (int i = 0; i < _Count; i++)
{
if (_HashCodes[i] >= 0 && defaultComparer.Equals(_Values[i], value))
return true;
}
}
return false;
}
public bool ContainsKey(TKey key)
{
return FindIndex(key) >= 0;
}
public void Clear()
{
if (_Count <= 0)
return;
for (int i = 0; i < _Buckets.Length; i++)
_Buckets[i] = -1;
Array.Clear(_Keys, 0, _Count);
Array.Clear(_Values, 0, _Count);
Array.Clear(_HashCodes, 0, _Count);
Array.Clear(_Next, 0, _Count);
_FreeList = -1;
_Count = 0;
_FreeCount = 0;
_Version++;
}
public void Add(TKey key, TValue value)
{
Insert(key, value, true);
}
private void Resize(int newSize, bool forceNewHashCodes)
{
int[] bucketsCopy = new int[newSize];
for (int i = 0; i < bucketsCopy.Length; i++)
bucketsCopy[i] = -1;
var keysCopy = new TKey[newSize];
var valuesCopy = new TValue[newSize];
var hashCodesCopy = new int[newSize];
var nextCopy = new int[newSize];
Array.Copy(_Values, 0, valuesCopy, 0, _Count);
Array.Copy(_Keys, 0, keysCopy, 0, _Count);
Array.Copy(_HashCodes, 0, hashCodesCopy, 0, _Count);
Array.Copy(_Next, 0, nextCopy, 0, _Count);
if (forceNewHashCodes)
{
for (int i = 0; i < _Count; i++)
{
if (hashCodesCopy[i] != -1)
hashCodesCopy[i] = (_Comparer.GetHashCode(keysCopy[i]) & 2147483647);
}
}
for (int i = 0; i < _Count; i++)
{
int index = hashCodesCopy[i] % newSize;
nextCopy[i] = bucketsCopy[index];
bucketsCopy[index] = i;
}
_Buckets = bucketsCopy;
_Keys = keysCopy;
_Values = valuesCopy;
_HashCodes = hashCodesCopy;
_Next = nextCopy;
}
private void Resize()
{
Resize(PrimeHelper.ExpandPrime(_Count), false);
}
public bool Remove(TKey key)
{
if (key == null)
throw new ArgumentNullException("key");
int hash = _Comparer.GetHashCode(key) & 2147483647;
int index = hash % _Buckets.Length;
int num = -1;
for (int i = _Buckets[index]; i >= 0; i = _Next[i])
{
if (_HashCodes[i] == hash && _Comparer.Equals(_Keys[i], key))
{
if (num < 0)
_Buckets[index] = _Next[i];
else
_Next[num] = _Next[i];
_HashCodes[i] = -1;
_Next[i] = _FreeList;
_Keys[i] = default(TKey);
_Values[i] = default(TValue);
_FreeList = i;
_FreeCount++;
_Version++;
return true;
}
num = i;
}
return false;
}
private void Insert(TKey key, TValue value, bool add)
{
if (key == null)
{
if (typeof(TKey) == typeof(UnityEngine.Object))
{
key = ObjectToTKey(new UnityEngine.Object());
}
else
{
throw new ArgumentNullException("key");
}
}
if (_Buckets == null)
Initialize(0);
int hash = _Comparer.GetHashCode(key) & 2147483647;
int index = hash % _Buckets.Length;
int num1 = 0;
for (int i = _Buckets[index]; i >= 0; i = _Next[i])
{
if (_HashCodes[i] == hash && _Comparer.Equals(_Keys[i], key))
{
if (add)
throw new ArgumentException("Key already exists: " + key);
_Values[i] = value;
_Version++;
return;
}
num1++;
}
int num2;
if (_FreeCount > 0)
{
num2 = _FreeList;
_FreeList = _Next[num2];
_FreeCount--;
}
else
{
if (_Count == _Keys.Length)
{
Resize();
index = hash % _Buckets.Length;
}
num2 = _Count;
_Count++;
}
_HashCodes[num2] = hash;
_Next[num2] = _Buckets[index];
_Keys[num2] = key;
_Values[num2] = value;
_Buckets[index] = num2;
_Version++;
}
private TKey ObjectToTKey(UnityEngine.Object key)
{
return (TKey)Convert.ChangeType(key, typeof(TKey));
}
private void Initialize(int capacity)
{
int prime = PrimeHelper.GetPrime(capacity);
_Buckets = new int[prime];
for (int i = 0; i < _Buckets.Length; i++)
_Buckets[i] = -1;
_Keys = new TKey[prime];
_Values = new TValue[prime];
_HashCodes = new int[prime];
_Next = new int[prime];
_FreeList = -1;
}
private int FindIndex(TKey key)
{
if (key == null)
{
throw new ArgumentNullException("key");
}
if (_Buckets != null)
{
int hash = _Comparer.GetHashCode(key) & 2147483647;
for (int i = _Buckets[hash % _Buckets.Length]; i >= 0; i = _Next[i])
{
if (_HashCodes[i] == hash && _Comparer.Equals(_Keys[i], key))
return i;
}
}
return -1;
}
public bool TryGetValue(TKey key, out TValue value)
{
int index = FindIndex(key);
if (index >= 0)
{
value = _Values[index];
return true;
}
value = default(TValue);
return false;
}
private static class PrimeHelper
{
public static readonly int[] Primes = new int[]
{
3,
7,
11,
17,
23,
29,
37,
47,
59,
71,
89,
107,
131,
163,
197,
239,
293,
353,
431,
521,
631,
761,
919,
1103,
1327,
1597,
1931,
2333,
2801,
3371,
4049,
4861,
5839,
7013,
8419,
10103,
12143,
14591,
17519,
21023,
25229,
30293,
36353,
43627,
52361,
62851,
75431,
90523,
108631,
130363,
156437,
187751,
225307,
270371,
324449,
389357,
467237,
560689,
672827,
807403,
968897,
1162687,
1395263,
1674319,
2009191,
2411033,
2893249,
3471899,
4166287,
4999559,
5999471,
7199369
};
public static bool IsPrime(int candidate)
{
if ((candidate & 1) != 0)
{
int num = (int)Math.Sqrt((double)candidate);
for (int i = 3; i <= num; i += 2)
{
if (candidate % i == 0)
{
return false;
}
}
return true;
}
return candidate == 2;
}
public static int GetPrime(int min)
{
if (min < 0)
throw new ArgumentException("min < 0");
for (int i = 0; i < PrimeHelper.Primes.Length; i++)
{
int prime = PrimeHelper.Primes[i];
if (prime >= min)
return prime;
}
for (int i = min | 1; i < 2147483647; i += 2)
{
if (PrimeHelper.IsPrime(i) && (i - 1) % 101 != 0)
return i;
}
return min;
}
public static int ExpandPrime(int oldSize)
{
int num = 2 * oldSize;
if (num > 2146435069 && 2146435069 > oldSize)
{
return 2146435069;
}
return PrimeHelper.GetPrime(num);
}
}
public ICollection<TKey> Keys
{
get { return _Keys.Take(Count).ToArray(); }
}
public ICollection<TValue> Values
{
get { return _Values.Take(Count).ToArray(); }
}
public void Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
public bool Contains(KeyValuePair<TKey, TValue> item)
{
int index = FindIndex(item.Key);
return index >= 0 &&
EqualityComparer<TValue>.Default.Equals(_Values[index], item.Value);
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int index)
{
if (array == null)
throw new ArgumentNullException("array");
if (index < 0 || index > array.Length)
throw new ArgumentOutOfRangeException(string.Format("index = {0} array.Length = {1}", index, array.Length));
if (array.Length - index < Count)
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));
for (int i = 0; i < _Count; i++)
{
if (_HashCodes[i] >= 0)
array[index++] = new KeyValuePair<TKey, TValue>(_Keys[i], _Values[i]);
}
}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
return Remove(item.Key);
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
{
private readonly SerializableDictionary<TKey, TValue> _Dictionary;
private int _Version;
private int _Index;
private KeyValuePair<TKey, TValue> _Current;
public KeyValuePair<TKey, TValue> Current
{
get { return _Current; }
}
internal Enumerator(SerializableDictionary<TKey, TValue> dictionary)
{
_Dictionary = dictionary;
_Version = dictionary._Version;
_Current = default;
_Index = 0;
}
public bool MoveNext()
{
if (_Version != _Dictionary._Version)
throw new InvalidOperationException(string.Format("Enumerator version {0} != Dictionary version {1}", _Version, _Dictionary._Version));
while (_Index < _Dictionary._Count)
{
if (_Dictionary._HashCodes[_Index] >= 0)
{
_Current = new KeyValuePair<TKey, TValue>(_Dictionary._Keys[_Index], _Dictionary._Values[_Index]);
_Index++;
return true;
}
_Index++;
}
_Index = _Dictionary._Count + 1;
_Current = default(KeyValuePair<TKey, TValue>);
return false;
}
void IEnumerator.Reset()
{
if (_Version != _Dictionary._Version)
throw new InvalidOperationException(string.Format("Enumerator version {0} != Dictionary version {1}", _Version, _Dictionary._Version));
_Index = 0;
_Current = default;
}
object IEnumerator.Current
{
get { return Current; }
}
public void Dispose()
{
}
}
}

Using :

using UnityEngine;

[Serializable] public class CustomDictionary : SerializableDictionary<string, bool> { }

public class Exemple : MonoBehaviour
{
  [SerializeField] private CustomDictionary myCustomDictionary = new CustomDictionary();
}

Render :

Render

@alexander-tolkachov
Copy link

Hello! Thank you for the code. I just installed this script in my project. For some reason OnValidate() not fires when changing values of Custom Dictionary in the inspector. Is there a workaround?

@Marsgames
Copy link
Author

Hello! Thank you for the code. I just installed this script in my project. For some reason OnValidate() not fires when changing values of Custom Dictionary in the inspector. Is there a workaround?

Hey,

You can add these lines in DictionaryDrawer.cs, in the if (EditorGUI.EndChangeCheck()) (around line 107)

MonoBehaviour target = property.serializedObject.targetObject as MonoBehaviour;
if (target)
{
    target.SendMessage("OnValidate", null, SendMessageOptions.DontRequireReceiver);
}

This will force call OnValidate after changes.
It will show an error, but it should still work.

I'm looking for a better way to do this (without the error)

@BestStream
Copy link

Hey @Marsgames! Thanks for your project. It works in the normal case. But it doesn't work when you need a list

public Collection[] Collections;
[System.Serializable]
public class Collection
{
    [SerializeField] private CustomDictionary myCustomDictionary2 = new CustomDictionary();
}

@WondernautStudio
Copy link

Hey @Marsgames do you can use that in if(EditorGUI.EndChangeCheck()), at the final of the function before the break:

if (EditorGUI.EndChangeCheck())
{
        //Your Code...

        EditorUtility.SetDirty(property.serializedObject.targetObject);
        AssetDatabase.SaveAssets();
        break;
}

Hope this helps!

@wahyucandra30
Copy link

wahyucandra30 commented Mar 6, 2023

Hi @Marsgames thanks for the code, it's been real useful! One slight annoyance, I get this error when I try to use this for an enum and float dictionary.
image

The drawer still works fine though, but this one error pops up every time the drawer is shown in the inspector and the value doesn't save. I don't know how to fix this. I made sure my SerializableDictionary class has the same types as seen below

[Serializable]
public class AccuracyDictionary : SerializedDictionary<MoveState, float> { }
[CustomPropertyDrawer(typeof(AccuracyDictionary))]
public class AccuracyDictionaryDrawer : DictionaryDrawer<MoveState, float>{ }

Any help would be greatly appreciated cause I'm super confused lol

@DelovoyDrew
Copy link

DelovoyDrew commented May 31, 2023

Hi @Marsgames , is it possible to put [Serializable] class or struct as value in dictionary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment