抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

众所周知,Unity 无法在 Inspector 面板上序列化显示字典的内容,因此我们需要用别的方法曲线救国。

Unity 是可以序列化显示 List 的,因此我们的目标就是用 List 的方式来序列化字典。

原理

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

序列化字典

简单来说就是把字典分为两个部分:

  • 新的序列化字典类内部的 List。
  • 一个是 List 内模拟键值对的类。

然后 List 的类型设为模拟键值对的类。这样我们就得到了一个基本容器。

为了方便拓展,我把原文模拟键值对的类提取出来:

SKeyValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

[Serializable]
public struct SKeyValue<TKey,TValue>
{
public TKey Key;
public TValue Value;

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

该类标记为可序列化,里面的内容就可以在 Inspector 面板上显示。

然后我们在序列化字典类中定义 List:

1
[SerializeField] private List<SKeyValue<TKey,TValue>> list = new List<SKeyValue<TKey, TValue>>();

然后我们需要额外定义一个字典来存储 Key 在 List 中的索引。

1
2
3
4
//属性
private Dictionary<TKey, int> KeyPositions => _keyPositions.Value;
//懒字典
private Lazy<Dictionary<TKey, int>> _keyPositions;

这样我们对于字典的操作就可以分为两个步骤:

  1. 获取 Key 在 List 中的索引。
  2. 获取对应索引的 Value。

其他操作就和普通的字典一样了。

面板绘制

如果使用默认的绘制,Value 值如果是嵌套的列表或字典,就会额外多出一个 List 标识。

要去掉 List,我们就需要自己重新绘制面板。

利用 EditorGUI.PropertyField 方法,我们可以绘制出对应类型的面板。

我们还要重写 GetPropertyHeight 方法,使其能够递归的获取属性的高度。

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

最后的效果就是这样:

番外

Unity 的 List 在嵌套的情况下也无法在面板上进行序列化,因此我们可以仿照序列化字典的做法定义序列化 List。

定义值类:

1
2
3
4
5
6
7
8
9
10
11
12
using System;

[Serializable]
public struct SValue<TValue>
{
public TValue Value;

public SValue(TValue value)
{
Value = value;
}
}

序列化 List 不需要存储 Key 索引,因此我们只需要 [SerializeField] private List<SValue<TValue>> list = new List<SValue<TValue>>();

注意

  • 如果你不需要对面板进行额外拓展的话,可以直接使用原文的代码。
  • 需要序列化复杂的数据不要用 Unity 原生的 Dictionary 和 List 作为类型传递。

代码

标记为 Editor 的需要放在 Editor 文件夹下。

SKeyValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

[Serializable]
public struct SKeyValue<TKey,TValue>
{
public TKey Key;
public TValue Value;

public SKeyValue(TKey key, TValue value)
{
Key = key;
Value = value;
}
}
SDictionary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class SDictionary { }

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


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

public SDictionary()
{
_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[i].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 SKeyValue<TKey,TValue>(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 SKeyValue<TKey, TValue>(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[i].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[i];
array[arrayIndex] = new KeyValuePair<TKey, TValue>(entry.Key, entry.Value);
}
}

public bool Remove(KeyValuePair<TKey, TValue> kvp) => Remove(kvp.Key);

public Dictionary<TKey, TValue> ToDictionary()
{
Dictionary<TKey, TValue> dic = new Dictionary<TKey, TValue>();
foreach(var kvp in list)
{
dic.Add(kvp.Key, kvp.Value);
}
return dic;
}

#endregion

#region IEnumerable <KeyValuePair<TKey, TValue>>

public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return list.Select(ToKeyValuePair).GetEnumerator();

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

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

#endregion
}

Editor

SDictionaryDrawer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SDictionary<,>))]
public class SDictionaryDrawer : 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);
}
}
SValue
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

[Serializable]
public struct SValue<TValue>
{
public TValue Value;

public SValue(TValue value)
{
Value = value;
}
}

SList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEngine;

[Serializable]
public class SList<TValue> : ISerializationCallbackReceiver, IList<TValue>
{
[SerializeField] private List<SValue<TValue>> list = new List<SValue<TValue>>();


public SList()
{

}

public void OnBeforeSerialize() { }

public void OnAfterDeserialize()
{

}



public int Count => list.Count;
public bool IsReadOnly => false;

public TValue this[int index]
{
get => list[index].Value;
set
{
if (index < list.Count)
{
SValue<TValue> v = list[index];
v.Value = value;
list[index] = v;
}
else
{
list.Add(new SValue<TValue>(value));
}
}
}


#region IEnumerable <KeyValuePair<TKey, TValue>>

public IEnumerator<TValue> GetEnumerator()
{
return list.Select(ToKeyValuePair).GetEnumerator();

static TValue ToKeyValuePair(SValue<TValue> sb)
{
return sb.Value;
}
}

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

#endregion

public int IndexOf(TValue item)
{
for (int i = 0; i < list.Count; i++)
{
if (list[i].Value.Equals(item))
{
return i;
}
}
return -1;
}

public void Insert(int index, TValue item)
{
list.Insert(index, new SValue<TValue>(item));
}

public void RemoveAt(int index)
{
list.RemoveAt(index);
}

public void Add(TValue item)
{
list.Add(new SValue<TValue>(item));
}

public void Clear()
{
list.Clear();
}

public bool Contains(TValue item)
{
int res = IndexOf(item);
return res != -1;
}

public void CopyTo(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[i];
array[arrayIndex] = entry.value;
}
}

public bool Remove(TValue item)
{
int res = IndexOf(item);
if (res != -1)
{
list.RemoveAt(res);
return true;
}
return false;
}

public List<TValue> ToList()
{
List<TValue> l = new List<TValue>();
foreach (var item in list)
{
l.Add(item.Value);
}
return l;
}
}

Editor

SListDrawer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SList<>))]
public class SListDrawer : 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);
}
}

项目工程

更新日志

2024-03-22

  1. 修复 SList 方法 CopyTo 错误。

2024-03-11

  1. 新增基本内容。

评论