Skip to content

Instantly share code, notes, and snippets.

@shane-harper
Last active May 30, 2019 21:33
Show Gist options
  • Save shane-harper/60a1c1ebcd4c7b8c020c98985ca51b3f to your computer and use it in GitHub Desktop.
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.
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
}
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);
}
}
@shane-harper
Copy link
Author

If I learned nothing else from this project, I learned that you can have multiple files in a gist.

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