找回密码
 立即注册
查看: 403|回复: 3

关于Unity中Inspector序列化显示Dictionary的方法

[复制链接]
发表于 2022-2-20 14:32 | 显示全部楼层 |阅读模式
字典作为最重要的数据结构之一,在Unity的inspector中竟然无法序列化显示。通过Google搜索,我找到了几种方法。


  • 使用List模拟字典的键值对,在Awake或Start方法中将键值复制到字典中。


原文链接:https://forum.unity.com/threads/adding-public-dictionary-as-parameter.692701/
缺点:每使用一个字典都需要写一段foreach;只能在开始时初始化字典,无法在运行中和字典绑定。

2. 使用两个List分别保存字典的键和值,通过继承ISerializationCallbackReceiver接口定义序列化行为。


原文链接:Serializing Dictionaries | Odin Inspector for Unity
缺点:每使用一个类型的字典都需要新建一个类,不够方便。

3. 使用Odin Serializer


原文链接:Odin Serializer Quick Start | Odin Inspector for Unity
缺点:需要继承SerializedMonoBehaviour代替MonoBehaviour,侵入性的接口。

4. 使用List模拟字典的键值对,内部使用一个字典存储键的索引,在运行时更改字典可将修改反映到List(Inspector)上,并使用ReorderableList和PropertyDrawer自定义绘制方法。


原文链接:SerializableDictionary - Unify Community Wiki
缺点:只能单行显示,无法用于复杂数据结构。
<hr/>方法4应该可以满足大多数人的需求,但没有满足我的需求。
我的需求如下:

  • Dictionary的键和值可以是复杂的可序列化数据结构
  • 在inspector中拥有类似List的GUI(如添加和删除按钮)
  • 使用时编码简单,接口非侵入性
因此我只要在方法4的基础上改动自定义绘制方法即可。
复杂数据结构含有嵌套,GUI上需要是可伸缩的,而原文使用的ReorderableList的行高是固定的,不同元素的高度不一样的话会有显示问题,所以我只能弃用它。而Unity原生的List可以有动态的行高,那我直接把自定义绘制方法去掉,并用上复杂数据结构试试。
public class Example : MonoBehaviour {
    public SerializableDictionary<string, VelocityState> velocityStates;
    public SerializableDictionary<string, List<GameObject>> gameObjects;
    public SerializableDictionary<string, SerializableDictionary<string, Color>> colors;
}
(其中VelocityState是我自定义的一个结构体)


去掉原文的自定义绘制方法可以直接用于复杂数据结构!
但是有一个问题,会多一层List嵌套。
通过简单的自定义绘制方法可以解决它。解决后效果如图:


大功告成!完结撒花~
感谢方法4原文作者给予我代码上的启发。
<hr/>附录:
SerializableDictionary.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class SerializableDictionary { }

[Serializable]
public class SerializableDictionary<TKey, TValue> :
    SerializableDictionary,
    ISerializationCallbackReceiver,
    IDictionary<TKey, TValue> {
    [SerializeField] private List<SerializableKeyValuePair> list = new List<SerializableKeyValuePair>();

    [Serializable]
    private struct SerializableKeyValuePair {
        public TKey Key;
        public TValue Value;

        public SerializableKeyValuePair(TKey key, TValue value) {
            Key = key;
            Value = value;
        }
    }

    private Dictionary<TKey, int> KeyPositions => _keyPositions.Value;
    private Lazy<Dictionary<TKey, int>> _keyPositions;

    public SerializableDictionary() {
        _keyPositions = new Lazy<Dictionary<TKey, int>>(MakeKeyPositions);
    }

    private Dictionary<TKey, int> MakeKeyPositions() {
        var dictionary = new Dictionary<TKey, int>(list.Count);
        for (var i = 0; i < list.Count; i++) {
            dictionary[list.Key] = i;
        }
        return dictionary;
    }

    public void OnBeforeSerialize() { }

    public void OnAfterDeserialize() {
        _keyPositions = new Lazy<Dictionary<TKey, int>>(MakeKeyPositions);
    }

    #region IDictionary<TKey, TValue>

    public TValue this[TKey key] {
        get => list[KeyPositions[key]].Value;
        set {
            var pair = new SerializableKeyValuePair(key, value);
            if (KeyPositions.ContainsKey(key)) {
                list[KeyPositions[key]] = pair;
            }
            else {
                KeyPositions[key] = list.Count;
                list.Add(pair);
            }
        }
    }

    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] = 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 var index)) {
            KeyPositions.Remove(key);

            list.RemoveAt(index);
            for (var i = index; i < list.Count; i++)
                KeyPositions[list.Key] = i;
            
            return true;
        }
        else
            return false;
    }

    public bool TryGetValue(TKey key, out TValue value) {
        if (KeyPositions.TryGetValue(key, out var index)) {
            value = list[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 (var i = 0; i < numKeys; i++, arrayIndex++) {
            var entry = list;
            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();

        static KeyValuePair<TKey, TValue> ToKeyValuePair(SerializableKeyValuePair skvp) {
            return new KeyValuePair<TKey, TValue>(skvp.Key, skvp.Value);
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    #endregion
}

[CustomPropertyDrawer(typeof(SerializableDictionary), true)]
public class SerializableDictionaryDrawer : PropertyDrawer {
    private SerializedProperty listProperty;

    private SerializedProperty getListProperty(SerializedProperty property) =>
        listProperty ??= property.FindPropertyRelative("list");

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        EditorGUI.PropertyField(position, getListProperty(property), label, true);
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
        return EditorGUI.GetPropertyHeight(getListProperty(property), true);
    }
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-2-20 14:36 | 显示全部楼层
unity2019.4.x测试无法使用
发表于 2022-2-20 14:43 | 显示全部楼层
我是在2020.3.14上使用的,老版本可能用不了
发表于 2022-2-20 14:45 | 显示全部楼层
嗯,我改出了一个2019的版本
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-28 17:27 , Processed in 0.092587 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表