XGundam05 发表于 2022-2-20 14:32

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

字典作为最重要的数据结构之一,在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 { }


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

   
    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.Key] = i;
      }
      return dictionary;
    }

    public void OnBeforeSerialize() { }

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

    #region IDictionary<TKey, TValue>

    public TValue this {
      get => list].Value;
      set {
            var pair = new SerializableKeyValuePair(key, value);
            if (KeyPositions.ContainsKey(key)) {
                list] = pair;
            }
            else {
                KeyPositions = 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 = 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.Key] = i;
            
            return true;
      }
      else
            return false;
    }

    public bool TryGetValue(TKey key, out TValue value) {
      if (KeyPositions.TryGetValue(key, out var index)) {
            value = list.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 = 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
}


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);
    }
}

zt3ff3n 发表于 2022-2-20 14:36

unity2019.4.x测试无法使用

量子计算9 发表于 2022-2-20 14:43

我是在2020.3.14上使用的,老版本可能用不了

量子计算9 发表于 2022-2-20 14:45

嗯,我改出了一个2019的版本
页: [1]
查看完整版本: 关于Unity中Inspector序列化显示Dictionary的方法