SerializableDictionary

From Unify Community Wiki
(Difference between revisions)
Jump to: navigation, search
(C# - SerializableDictionaryDrawer.cs)
(Added Notice 2)
Line 1: Line 1:
 
Author: Fredrik Ludvigsen ([[:User:Steinbitglis|Steinbitglis]])
 
Author: Fredrik Ludvigsen ([[:User:Steinbitglis|Steinbitglis]])
  
== Notice ==
+
== Notice 1 ==
  
 
This collection has been updated (as of Mar. 2021).
 
This collection has been updated (as of Mar. 2021).
  
 
Check the document history for the old version.
 
Check the document history for the old version.
 +
 +
== Notice 2 ==
 +
 +
A much more advanced version is now available at [https://office.rain-games.com:4873/-/web/detail/com.beetlecircus.serializabledictionary Rain Games office server].
 +
 +
This can be added as a scoped registry in the package manager.
 +
 +
The package name is '''com.beetlecircus.serializabledictionary'''
 +
 +
The registry url is '''https://office.rain-games.com:4873'''
 +
 +
Instructions for adding registries [https://docs.unity3d.com/Manual/upm-scoped.html here]
  
 
== Description ==
 
== Description ==

Revision as of 23:02, 2 April 2021

Author: Fredrik Ludvigsen (Steinbitglis)

Contents

Notice 1

This collection has been updated (as of Mar. 2021).

Check the document history for the old version.

Notice 2

A much more advanced version is now available at Rain Games office server.

This can be added as a scoped registry in the package manager.

The package name is com.beetlecircus.serializabledictionary

The registry url is https://office.rain-games.com:4873

Instructions for adding registries here

Description

This script lets you use serializable dictionaries in Unity with no boilerplate per new type.

Usage looks like this.

SerializableDictionary.png

And you get an editor that looks like this.

SerializableDictionary inspector.png

All files as a zip archive

File:SerializableDictionary.zip

Usage

  • Copy and paste all files except the drawer into Assets.
  • Copy the drawer into Assets/Editor
  • Put Example.cs on a new game object.

C# - Example.cs

using System.Collections.Generic;
using System.Linq;
 
using UnityEngine;
 
public class Example : MonoBehaviour
{
    public SerializableDictionary<string, string>     names;
    public SerializableDictionary<string, GameObject> gameObjects;
    public SerializableDictionary<string, Color>      colors;
}

C# - SerializableDictionary.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
 
using UnityEngine;
using UnityEngine.Serialization;
 
public class SerializableDictionary {}
 
[Serializable]
public class SerializableDictionary<TKey, TValue> : SerializableDictionary, ISerializationCallbackReceiver, IDictionary<TKey, TValue> {
 
    [SerializeField]
    private List<SerializableKeyValuePair> list = new List<SerializableKeyValuePair>();
 
    [Serializable]
    public struct SerializableKeyValuePair {
        public TKey   Key;
        public TValue Value;
 
        public SerializableKeyValuePair (TKey key, TValue value) {
            Key   = key;
            Value = value;
        }
 
        public void SetValue (TValue value) {
            Value = value;
        }
    }
 
 
    private Dictionary<TKey, uint> KeyPositions => _keyPositions.Value;
    private Lazy<Dictionary<TKey, uint>> _keyPositions;
 
    public SerializableDictionary() {
        _keyPositions = new Lazy<Dictionary<TKey, uint>>(MakeKeyPositions);
    }
 
    private Dictionary<TKey, uint> MakeKeyPositions () {
        var numEntries = list.Count;
        var result = new Dictionary<TKey, uint>( numEntries );
        for (int i = 0; i < numEntries; i++)
            result[list[i].Key] = (uint) i;
        return result;
    }
 
    public void OnBeforeSerialize() {}
    public void OnAfterDeserialize() {
        // After deserialization, the key positions might be changed
        _keyPositions = new Lazy<Dictionary<TKey, uint>>(MakeKeyPositions);
    }
 
#region IDictionary<TKey, TValue>
    public TValue this[TKey key] {
        get => list[(int) KeyPositions[key]].Value;
        set {
            if (KeyPositions.TryGetValue(key, out uint index))
                list[(int) index].SetValue(value);
            else {
                KeyPositions[key] = (uint) list.Count;
                list.Add( new SerializableKeyValuePair( key, value ) );
            }
        }
    }
 
    public ICollection<TKey>   Keys   => list.Select( tuple => tuple.Key ).ToArray();
    public ICollection<TValue> Values => list.Select( tuple => tuple.Value ).ToArray();
 
    public void Add(TKey key, TValue value) {
        if (KeyPositions.ContainsKey(key))
            throw new ArgumentException("An element with the same key already exists in the dictionary.");
        else {
            KeyPositions[key] = (uint) list.Count;
            list.Add( new SerializableKeyValuePair( key, value ) );
        }
    }
 
    public bool ContainsKey(TKey key) => KeyPositions.ContainsKey(key);
 
 
    public bool Remove (TKey key) {
        if (KeyPositions.TryGetValue(key, out uint index)) {
            var kp = KeyPositions;
            kp.Remove(key);
 
            var numEntries = list.Count;
 
            list.RemoveAt( (int) index );
            for (uint i = index; i < numEntries; i++)
                kp[list[(int) i].Key] = i;
 
            return true;
        }
        else
            return false;
    }
 
    public bool TryGetValue (TKey key, out TValue value) {
        if (KeyPositions.TryGetValue(key, out uint index)) {
            value = list[(int) index].Value;
            return true;
        }
        else {
            value = default;
            return false;
        }
    }
#endregion
 
 
#region ICollection <KeyValuePair<TKey, TValue>>
    public int  Count      => list.Count;
    public bool IsReadOnly => false;
 
    public void Add (KeyValuePair<TKey, TValue> kvp) => Add( kvp.Key, kvp.Value );
 
    public void Clear    ()                               => list.Clear();
    public bool Contains (KeyValuePair<TKey, TValue> kvp) => KeyPositions.ContainsKey(kvp.Key);
 
    public void CopyTo (KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
        var numKeys = list.Count;
        if (array.Length - arrayIndex < numKeys)
            throw new ArgumentException("arrayIndex");
        for (int i = 0; i < numKeys; i++, arrayIndex++) {
            var entry = list[i];
            array[arrayIndex] = new KeyValuePair<TKey, TValue>( entry.Key, entry.Value );
        }
    }
 
    public bool Remove(KeyValuePair<TKey, TValue> kvp) => Remove(kvp.Key);
#endregion
 
 
#region IEnumerable <KeyValuePair<TKey, TValue>>
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator () {
        return list.Select(ToKeyValuePair).GetEnumerator();
 
        KeyValuePair<TKey, TValue> ToKeyValuePair (SerializableKeyValuePair skvp) {
            return new KeyValuePair<TKey, TValue>( skvp.Key, skvp.Value );
        }
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
 
 
}

C# - SerializableDictionaryDrawer.cs

using System;
using System.Reflection;
 
using UnityEngine;
 
using UnityEditor;
using UnityEditorInternal;
 
[CustomPropertyDrawer(typeof(SerializableDictionary), true)]
public class SerializableDictionaryDrawer : PropertyDrawer {
 
    private ReorderableList list;
 
    private Func<Rect> VisibleRect;
 
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
        if (list == null) {
            var listProp = property.FindPropertyRelative("list");
            list = new ReorderableList(property.serializedObject, listProp, true, false, true, true);
            list.drawElementCallback = DrawListItems;
        }
 
        var firstLine = position;
        firstLine.height = EditorGUIUtility.singleLineHeight;
        EditorGUI.PropertyField(firstLine, property, label);
 
        if (property.isExpanded) {
            position.y += firstLine.height;
 
            if (VisibleRect == null) {
                 var tyGUIClip = System.Type.GetType("UnityEngine.GUIClip,UnityEngine");
                 if (tyGUIClip != null) {
                    var piVisibleRect = tyGUIClip.GetProperty("visibleRect", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
                    if (piVisibleRect != null) {
 
                        var getMethod = piVisibleRect.GetGetMethod(true) ?? piVisibleRect.GetGetMethod(false);
                        VisibleRect = (Func<Rect>)Delegate.CreateDelegate(typeof(Func<Rect>), getMethod);
                    }
                 }
            }
 
            var vRect = VisibleRect();
            vRect.y -= position.y;
 
            if (elementIndex == null)
                elementIndex = new GUIContent();
 
            list.DoList(position, vRect);
        }
    }
 
    private static GUIContent[] pairElementLabels => s_pairElementLabels ?? (s_pairElementLabels = new[] {new GUIContent("Key"), new GUIContent ("=>")});
    private static GUIContent[] s_pairElementLabels;
 
    private static GUIContent elementIndex;
 
    void DrawListItems(Rect rect, int index, bool isActive, bool isFocused) {
        SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index); // The element in the list
 
        var keyProp   = element.FindPropertyRelative("Key");
        var valueProp = element.FindPropertyRelative("Value");
 
        elementIndex.text = $"Element {index}";
        /*var label =*/ EditorGUI.BeginProperty(rect, elementIndex, element);
 
        var prevLabelWidth = EditorGUIUtility.labelWidth;
 
        EditorGUIUtility.labelWidth = 75;
 
        var rect0 = rect; //EditorGUI.PrefixLabel(rect, GUIUtility.GetControlID(FocusType.Passive), label);
 
        var halfWidth = rect0.width / 2f;
        rect0.width = halfWidth;
        rect0.y += 1f;
        rect0.height -= 2f;
 
 
        EditorGUIUtility.labelWidth = 40;
 
        EditorGUI.BeginChangeCheck();
        EditorGUI.PropertyField(rect0, keyProp);
 
        rect0.x += halfWidth + 4f;
 
        EditorGUI.PropertyField(rect0, valueProp);
 
        EditorGUIUtility.labelWidth = prevLabelWidth;
 
        EditorGUI.EndProperty();
    }
 
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
        if (property.isExpanded) {
            var listProp = property.FindPropertyRelative("list");
            if (listProp.arraySize < 2)
                return EditorGUIUtility.singleLineHeight + 52f;
            else
                return EditorGUIUtility.singleLineHeight + 23f * listProp.arraySize + 29;
        }
        else
            return EditorGUIUtility.singleLineHeight;
    }
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox