Finally, a serializable dictionary for Unity! (extracted from System.Collections.Generic)
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
[Serializable, DebuggerDisplay("Count = {Count}")]
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)
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++;
//if (num3 > 100 && HashHelpers.IsWellKnownEqualityComparer(comparer))
//{
// comparer = (IEqualityComparer<TKey>)HashHelpers.GetRandomizedEqualityComparer(comparer);
// Resize(entries.Length, true);
//}
}
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(KeyValuePair<TKey, TValue>);
_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(KeyValuePair<TKey, TValue>);
}
object IEnumerator.Current
{
get { return Current; }
}
public void Dispose()
{
}
}
}
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityObject = UnityEngine.Object;
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 GUIContent iconToolbarMinus = EditorGUIUtility.IconContent("Toolbar Minus", "Remove selection from list");
static GUIContent iconToolbarPlus = EditorGUIUtility.IconContent("Toolbar Plus", "Add to list");
static GUIStyle preButton = "RL FooterButton";
static 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)
{
Undo.RecordObject(property.serializedObject.targetObject, "Undo Dictionary Change");
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);
EditorUtility.SetDirty(property.serializedObject.targetObject);
}
position.xMin += kMargin;
position.xMax -= kMargin;
var buttonRect = position;
buttonRect.xMin = position.xMax - kButtonWidth;
//buttonRect.x -= 5;
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);
EditorUtility.SetDirty(property.serializedObject.targetObject);
}
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;
EditorUtility.SetDirty(property.serializedObject.targetObject);
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)
{
Func<Rect, object, object> field;
if (_Fields.TryGetValue(type, out 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(TK);
var value = default(TV);
try
{
_Dictionary.Add(key, value);
}
catch (Exception e)
{
Debug.Log(e.Message);
}
}
}
// This one is for your custom-made dictionaries.
[CustomPropertyDrawer(typeof(VideoClipDictionary))]
public class VideoClipDictionaryDrawer : DictionaryDrawer<string, UnityEngine.Video.VideoClip> { }
[CustomPropertyDrawer(typeof(AudioClipDictionary))]
public class AudioClipDictionaryDrawer : DictionaryDrawer<string, AudioClip> { }
[CustomPropertyDrawer(typeof(TextureDictionary))]
public class TextureDictionaryDrawer : DictionaryDrawer<string, Texture> { }
引用参考:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]public class VideoClipDictionary : SerializableDictionary<string, UnityEngine.Video.VideoClip>{ }
using System;
using UnityEngine;
[Serializable]public class AudioClipDictionary : SerializableDictionary<string, AudioClip>{ }
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable] public class TextureDictionary : SerializableDictionary<string, Texture> { }
ScriptableObject 配置文件:
using UnityEngine;
using UnityEngine.Video;
[CreateAssetMenu(fileName = "Cfg_AppVideoDictionary", menuName = "Configures/Cfg_Videos", order = 1)]
public class VideoDictionaryConfig : ScriptableObject
{
public VideoClipDictionary VideoPool;
}