Last active
May 30, 2019 21:33
-
-
Save shane-harper/60a1c1ebcd4c7b8c020c98985ca51b3f to your computer and use it in GitHub Desktop.
A serializable dictionary class for Unity that includes all the interfaces you find on the Dictionary class.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Threading; | |
using UnityEngine; | |
/// <summary> | |
/// Example implementation of the dictionary class | |
/// Unfortunately, Unity doesn't support serializing generic classes | |
/// </summary> | |
[Serializable] | |
public class ExampleDictionary : SerializableDictionary<string, Sprite> | |
{ | |
} | |
/// <summary> | |
/// Base classed used for identifying it to the property drawer | |
/// A lot of stuff could be transferred from the SerializableDictionary class to make this a working class of | |
/// it's own, but I didn't want to spend too much time on this | |
/// </summary> | |
[Serializable] | |
public abstract class BaseSerializableDictionary | |
{ | |
} | |
[Serializable] | |
public abstract class SerializableDictionary<TKey, TValue> : BaseSerializableDictionary, IDictionary<TKey, TValue>, | |
IDictionary, | |
IReadOnlyDictionary<TKey, TValue> | |
{ | |
#region Data | |
[SerializeField] private List<TKey> _keys = new List<TKey>(); | |
[SerializeField] private List<TValue> _values = new List<TValue>(); | |
public IEnumerable<TKey> Keys => _keys; | |
public IEnumerable<TValue> Values => _values; | |
ICollection<TKey> IDictionary<TKey, TValue>.Keys => _keys; | |
ICollection<TValue> IDictionary<TKey, TValue>.Values => _values; | |
ICollection IDictionary.Values => _values; | |
ICollection IDictionary.Keys => _keys; | |
public int Count => _keys.Count; | |
public bool IsReadOnly => false; | |
public bool IsFixedSize => false; | |
public bool TryGetValue(TKey key, out TValue value) | |
{ | |
var index = GetIndex(key); | |
if (index < 0) | |
{ | |
value = default(TValue); | |
return false; | |
} | |
value = _values[index]; | |
return true; | |
} | |
#endregion | |
#region Enumeration | |
public object this[object key] | |
{ | |
get | |
{ | |
if (key is TKey key1) return this[key1]; | |
return null; | |
} | |
set | |
{ | |
if (!(key is TKey key1) || !(value is TValue value1)) throw new ArgumentException(); | |
this[key1] = value1; | |
} | |
} | |
public TValue this[TKey key] | |
{ | |
get | |
{ | |
var index = GetIndex(key); | |
if (index < 0) throw new ArgumentOutOfRangeException(); | |
return _values[index]; | |
} | |
set | |
{ | |
var index = GetIndex(key); | |
if (index < 0) | |
{ | |
_keys.Add(key); | |
_values.Add(value); | |
} | |
else | |
{ | |
_values[index] = value; | |
} | |
} | |
} | |
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() | |
{ | |
return new Enumerator(_keys, _values, 2); | |
} | |
IDictionaryEnumerator IDictionary.GetEnumerator() | |
{ | |
return (IDictionaryEnumerator) GetEnumerator(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return GetEnumerator(); | |
} | |
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, IEnumerator, IDictionaryEnumerator | |
{ | |
private readonly List<TKey> _keys; | |
private readonly List<TValue> _values; | |
private readonly int _getEnumeratorRetType; | |
private KeyValuePair<TKey, TValue> _current; | |
private int _index; | |
public Enumerator(List<TKey> keys, List<TValue> values, int getEnumeratorRetType) : this() | |
{ | |
_keys = keys; | |
_values = values; | |
_getEnumeratorRetType = getEnumeratorRetType; | |
} | |
public bool MoveNext() | |
{ | |
for (; _index < _keys.Count; ++_index) | |
{ | |
_current = new KeyValuePair<TKey, TValue>(_keys[_index], _values[_index]); | |
++_index; | |
return true; | |
} | |
_index = _keys.Count + 1; | |
_current = new KeyValuePair<TKey, TValue>(); | |
return false; | |
} | |
public void Reset() | |
{ | |
_index = 0; | |
_current = new KeyValuePair<TKey, TValue>(); | |
} | |
KeyValuePair<TKey, TValue> IEnumerator<KeyValuePair<TKey, TValue>>.Current => _current; | |
public object Key => _current.Key; | |
public object Value => _current.Value; | |
public object Current | |
{ | |
get | |
{ | |
if (_index == 0 || _index == _keys.Count + 1) throw new InvalidOperationException(); | |
if (_getEnumeratorRetType == 1) return new DictionaryEntry(_current.Key, _current.Value); | |
return new KeyValuePair<TKey, TValue>(_current.Key, _current.Value); | |
} | |
} | |
public DictionaryEntry Entry => new DictionaryEntry(_current.Key, _current.Value); | |
public void Dispose() | |
{ | |
} | |
} | |
#endregion | |
#region Add and Remove | |
public void Add(KeyValuePair<TKey, TValue> item) | |
{ | |
Add(item.Key, item.Value); | |
} | |
public void Add(TKey key, TValue value) | |
{ | |
if (ContainsKey(key)) throw new ArgumentException("That key already exists"); | |
_keys.Add(key); | |
_values.Add(value); | |
} | |
public void Add(object key, object value) | |
{ | |
if (key == null) throw new ArgumentNullException(); | |
Add((TKey) key, (TValue) value); | |
} | |
public bool Remove(KeyValuePair<TKey, TValue> item) | |
{ | |
return Remove(item.Key); | |
} | |
public bool Remove(TKey key) | |
{ | |
var index = GetIndex(key); | |
if (index < 0) return false; | |
_keys.RemoveAt(index); | |
_values.RemoveAt(index); | |
return true; | |
} | |
public void Remove(object key) | |
{ | |
if (key is TKey key1) Remove(key1); | |
} | |
public void Clear() | |
{ | |
_keys.Clear(); | |
_values.Clear(); | |
} | |
#endregion | |
#region Contains | |
public bool Contains(object key) | |
{ | |
return key is TKey key1 && Contains(key1); | |
} | |
private int GetIndex(TKey key) | |
{ | |
for (var i = 0; i < _keys.Count; ++i) | |
if (Equals(_keys[i], key)) | |
return i; | |
return -1; | |
} | |
public bool Contains(KeyValuePair<TKey, TValue> item) | |
{ | |
var index = GetIndex(item.Key); | |
return index >= 0 && Equals(_values[index], item.Value); | |
} | |
public bool ContainsKey(TKey key) | |
{ | |
return _keys.Contains(key); | |
} | |
public bool ContainsValue(TValue value) | |
{ | |
return _values.Contains(value); | |
} | |
#endregion | |
#region Utilities | |
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int index) | |
{ | |
if (array == null || index < 0 || index > array.Length || array.Length - index < Count) | |
throw new IndexOutOfRangeException(); | |
for (var i = 0; i < Count; ++i) array[index++] = new KeyValuePair<TKey, TValue>(_keys[i], _values[i]); | |
} | |
public void CopyTo(Array array, int index) | |
{ | |
if (array == null || array.Rank != 1 || array.GetLowerBound(0) != 0 || | |
index < 0 || index > array.Length || array.Length - index < Count) | |
throw new ArgumentOutOfRangeException(); | |
if (array is TKey[] array1) | |
{ | |
CopyTo(array1, index); | |
return; | |
} | |
if (!(array is object[] objArray)) | |
throw new InvalidCastException(); | |
for (var i = 0; i < Count; ++i) objArray[index++] = _keys[i]; | |
} | |
public Dictionary<TKey, TValue> ToDictionary() | |
{ | |
var dictionary = new Dictionary<TKey, TValue>(Count); | |
for (var i = 0; i < Count; ++i) dictionary[_keys[i]] = _values[i]; | |
return dictionary; | |
} | |
#endregion | |
#region Sync | |
public bool IsSynchronized => false; | |
private object _syncRoot; | |
public object SyncRoot | |
{ | |
get | |
{ | |
if (_syncRoot == null) | |
Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null); | |
return _syncRoot; | |
} | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEditor; | |
using UnityEngine; | |
/// <summary> | |
/// Custom property drawer for the serializable dictionary. Make sure to put it in an Editor folder | |
/// Known issue: Right click > Duplicate/Delete Array Item does not work correctly, do not use it! | |
/// </summary> | |
[CustomPropertyDrawer(typeof(BaseSerializableDictionary), true)] | |
public class SerializableDictionaryDrawer : PropertyDrawer | |
{ | |
private bool _expanded = true; | |
private int _count; | |
private SerializedProperty _keysProperty; | |
private SerializedProperty _valuesProperty; | |
private SerializedProperty[] _keys; | |
private SerializedProperty[] _values; | |
private bool _initialized; | |
private static float VerticalLineCost => EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; | |
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) | |
{ | |
EditorGUI.BeginProperty(position, label, property); | |
// Draw label | |
var labelRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); | |
_expanded = EditorGUI.Foldout(labelRect, _expanded, label, true); | |
if (!_expanded) return; | |
// Get data on first run | |
if (!_initialized) GetData(property); | |
var indentLevel = EditorGUI.indentLevel + 1; | |
EditorGUI.indentLevel = 0; | |
var indent = 15f * indentLevel; | |
// Draw size parameter | |
var newCount = EditorGUI.IntField(new Rect(position.x + indent, position.y + VerticalLineCost, | |
position.width - indent, EditorGUIUtility.singleLineHeight), "Size", _count); | |
// If the size counts, set the new array sizes and reinitialize | |
if (newCount != _count) | |
{ | |
_keysProperty.arraySize = newCount; | |
_valuesProperty.arraySize = newCount; | |
GetData(property); | |
return; | |
} | |
// Calculate left and right rects | |
const float spacing = 2.5f; | |
var width = (position.width - indent) * 0.5f; | |
var left = new Rect(position.x + indent, position.y + VerticalLineCost, | |
width - spacing, EditorGUIUtility.singleLineHeight); | |
var right = new Rect(position.x + width + spacing + indent, position.y + VerticalLineCost, | |
width - spacing, EditorGUIUtility.singleLineHeight); | |
for (var i = 0; i < _count; ++i) | |
{ | |
left.y += VerticalLineCost; | |
right.y += VerticalLineCost; | |
EditorGUI.PropertyField(left, _keys[i], GUIContent.none, false); | |
EditorGUI.PropertyField(right, _values[i], GUIContent.none, false); | |
} | |
EditorGUI.indentLevel = indentLevel - 1; | |
EditorGUI.EndProperty(); | |
} | |
private void GetData(SerializedProperty property) | |
{ | |
// Get properties | |
_keysProperty = property.FindPropertyRelative("_keys"); | |
_valuesProperty = property.FindPropertyRelative("_values"); | |
_count = _keysProperty.arraySize; | |
// Initialize arrays | |
_keys = new SerializedProperty[_count]; | |
_values = new SerializedProperty[_count]; | |
// Populate arrays | |
for (var i = 0; i < _count; ++i) | |
{ | |
_keys[i] = _keysProperty.GetArrayElementAtIndex(i); | |
_values[i] = _valuesProperty.GetArrayElementAtIndex(i); | |
} | |
_initialized = true; | |
} | |
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) | |
{ | |
// Get base height | |
var baseValue = base.GetPropertyHeight(property, label); | |
if (_count == 0) _expanded = false; | |
// If expanded, add extra line for each item | |
if (!_expanded) return baseValue; | |
return baseValue + VerticalLineCost * (_count + 1); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If I learned nothing else from this project, I learned that you can have multiple files in a gist.