FishPlayer

一个喜欢摸鱼的废物

0%

Unity Inspector 绘制非序列化字段

在MonoBehaviour里给字段打上 [SerializedField] 特性标记即可把这个字段显示在 Inspector 上,同时还会把这个字段序列化到资源里。
在实际开发项目的时候,有些字段我们只是想从 Inspector 上看到数值以方便差错,并不想序列化这个字段(浪费空间)。在 Unity 商城里有 Odin Inspector, Naughty Attributes 之类的编辑器拓展插件可以完成这个功能。

最近在座的项目里使用了 Mirror Networking 和 Naughty Attributes,但是我发现 Naughty Attributes 的 [ShowInInspector] 没法用在 Mirror.NerworkBehaviour 上。只能照着 Naughty Attributes 源码里的方式把 [ShowInInspector] 的功能给 Mirror.NerworkBehaviour 在实现一次,于是有了今天这篇笔记。

基本思路

为什么 Mirror.NerworkBehaviour 无法使用另一个编辑器插件里的 [ShowInInspector] 呢?我们通过查看源码得知, Naughty Attributes 编写了一个应用于 UnityObject 的 Inspector 编辑器脚本,用于在里面调用自己 Attibutes 的相关绘制方法,并且设置为应用于子类。而 Mirror.NerworkBehaviour 正好也有一个自己专有的 Inspector 绘制脚本,并且要应用于子类。所以继承 Mirror.NerworkBehaviour 的脚本就使用了 Mirror 准备的编辑器绘制脚本,无法再使用 Naughty Attributes,而项目里其他 MonoBehaviour 还仍然使用 Naughty Attributes 的编辑器绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

CanEditMultipleObjects]
[CustomEditor(typeof(UnityEngine.Object), true)]
public class NaughtyInspector : UnityEditor.Editor
{
// ......
}

[CustomEditor(typeof(NetworkBehaviour), true)]
[CanEditMultipleObjects]
public partial class NetworkBehaviourInspector : Editor
{
// ......
}

那么我的思路很简单:

  1. 在 Mirror 中也定义一个 [ShowInInspector] 类似的特性
  2. Mirror.NerworkBehaviour 的Inspector绘制脚本中对 [Mirror.TempShowInInspector] 进行检测, 收集有此特性的字段
  3. Mirror.NerworkBehaviour 的Inspector原本的绘制方法结束后,对有 [Mirror.TempShowInInspector] 特性的字段进行绘制

代码

为了让代码看起来干净整洁,下面放出的代码是用于在空的Unity工程中实现 [ShowInInspector]

先定义特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

namespace Core
{
public class TempShowInInspectorAttribute : Attribute
{
public bool IsReadonlyField { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="isReadonly">If this field is readonly in inspector.
/// Set it to readonly would save some performance</param>
public TempShowInInspectorAttribute(bool isReadonly = true)
{
IsReadonlyField = isReadonly;
}
}
}

然后编写一个 Helper 类。在这个 Helper 类中,要编写方法获取一个 UnityObject 中的字段。
同时还要编写方法去绘制字段。绘制字段的函数非常庞大,由于在实际项目中会有 Struct 嵌套的情况,所以我写了一个可以递归调用的方法以处理嵌套。
但我只是粗暴的设置了允许嵌套的层数,因为我觉得实际使用情境中是不太会有太深层的嵌套了,因为此类数据一般都是从配置表中拿到或者是一些运行时的参数组合。

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336

namespace Core.Editor
{
public static class TempGUIHelper
{
public const int DRAW_DEPTH_LIMIT = 2;

public static void DrawTempShowInInspectorField(UnityObject target, FieldInfo field, bool isReadonly = false)
{
object value = field.GetValue(target);
string niceVariableName = ObjectNames.NicifyVariableName(field.Name);
if (field.IsStatic)
{
niceVariableName = $"{niceVariableName}(static) ";
}
if (null == value)
{
// TODO @Hiko if field is a class pack, better to spawn one for it; But why do ppl make a data pack into class, it should just be struct;
return;
}
if (TryDrawField(value, niceVariableName, field.FieldType, 0, out object nextValueObject, out bool setNextValue, isReadonly))
{
if (setNextValue && !isReadonly)
{
field.SetValue(target, nextValueObject);
}
}
}

public static bool TryDrawField(object value, string lable, Type expectedType, int depth, out object nextValue, out bool setNextValue, bool readonlyField = false)
{
nextValue = null;
setNextValue = false;
string showLableName = lable;
if (0 < depth) // HACK @Hiko pad left
{
showLableName = lable.PadLeft(lable.Length + depth * 4);
}
if (depth >= DRAW_DEPTH_LIMIT)
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"{showLableName}(OutOfDrawDepthLimit):");
string tempContent = value.ToString();
tempContent = tempContent.PadLeft(tempContent.Length + depth * 4);
EditorGUILayout.LabelField(tempContent);
EditorGUILayout.EndVertical();
return true;
}
using (new EditorGUI.DisabledScope(disabled: readonlyField))
{
bool isDrawn = true;
Type valueType = Equals(value, null) ? expectedType : value.GetType();
if (null == valueType)
{
return false;
}
if (valueType == typeof(bool))
{
bool prevBool = (bool)value;
bool nextBool = EditorGUILayout.Toggle(showLableName, prevBool);
if (setNextValue = prevBool != nextBool)
{
nextValue = nextBool;
}
}
else if (valueType == typeof(short))
{
short prevShort = (short)value;
short nextShort = (short)Mathf.Clamp(EditorGUILayout.IntField(showLableName, (int)prevShort), short.MinValue, short.MaxValue); // need clamp
if (setNextValue = prevShort != nextShort)
{
nextValue = nextShort;
}
}
else if (valueType == typeof(ushort))
{
ushort prevUshort = (ushort)value;
ushort nextUshort = (ushort)Mathf.Clamp(EditorGUILayout.IntField(showLableName, (ushort)value), ushort.MinValue, ushort.MaxValue); // need clamp
if (setNextValue = prevUshort != nextUshort)
{
nextValue = nextUshort;
}
}
else if (valueType == typeof(int))
{
int prevInt = (int)value;
int nextInt = EditorGUILayout.IntField(showLableName, prevInt);
if (setNextValue = prevInt != nextInt)
{
nextValue = nextInt;
}
}
else if (valueType == typeof(uint))
{
uint prevUint = (uint)value;
uint nextUint = (uint)Mathf.Clamp(EditorGUILayout.LongField(showLableName, prevUint), uint.MinValue, uint.MaxValue); // need clamp
if (setNextValue = prevUint != nextUint)
{
nextValue = nextUint;
}
}
else if (valueType == typeof(long))
{
long prevLong = (long)value;
long nextLong = EditorGUILayout.LongField(showLableName, prevLong);
if (setNextValue = prevLong != nextLong)
{
nextValue = nextLong;
}
}
else if (valueType == typeof(ulong))
{
ulong prevUlong = (ulong)value;
string nextUlongStr = EditorGUILayout.TextField(showLableName, prevUlong.ToString());
nextUlongStr = Regex.Replace(nextUlongStr, @"[^a-zA-Z0-9 ]", ""); // force convert all to number
if (ulong.TryParse(nextUlongStr, out ulong nextUlong) && (setNextValue = prevUlong != nextUlong))
{
nextValue = nextUlongStr;
}
}
else if (valueType == typeof(float))
{
float prevFloat = (float)value;
float nextFloat = EditorGUILayout.FloatField(showLableName, (float)value);
if (setNextValue = !Mathf.Approximately(prevFloat, nextFloat))
{
nextValue = nextFloat;
}
}
else if (valueType == typeof(double))
{
double prevDouble = (double)value;
double nextDouble = EditorGUILayout.DoubleField(showLableName, prevDouble);
if (setNextValue = prevDouble != nextDouble)
{
nextValue = nextDouble;
}
}
else if (valueType == typeof(string))
{
string prevString = (string)value;
string nextString = EditorGUILayout.TextField(showLableName, prevString);
if (setNextValue = prevString != nextString)
{
nextValue = nextString;
}
}
else if (valueType == typeof(Vector2))
{
Vector2 prevVector2 = (Vector2)value;
Vector2 nextVector2 = EditorGUILayout.Vector2Field(showLableName, prevVector2);
if (setNextValue = prevVector2 != nextVector2)
{
nextValue = prevVector2;
}
}
else if (valueType == typeof(Vector3))
{
Vector3 prevVector3 = (Vector3)value;
Vector3 nextVector3 = EditorGUILayout.Vector3Field(showLableName, prevVector3);
if (setNextValue = prevVector3 != nextVector3)
{
nextValue = nextVector3;
}
}
else if (valueType == typeof(Vector4))
{
Vector4 prevVector4 = (Vector4)value;
Vector4 nextVector4 = EditorGUILayout.Vector4Field(showLableName, prevVector4);
if (setNextValue = prevVector4 != nextVector4)
{
nextValue = nextVector4;
}
}
else if (valueType == typeof(Vector2Int))
{
Vector2Int prevV2int = (Vector2Int)value;
Vector2Int nextV2int = EditorGUILayout.Vector2IntField(showLableName, prevV2int);
if (setNextValue = prevV2int != nextV2int)
{
nextValue = nextV2int;
}
}
else if (valueType == typeof(Vector3Int))
{
Vector3Int prevV3int = (Vector3Int)value;
Vector3Int nextV3int = EditorGUILayout.Vector3IntField(showLableName, prevV3int);
if (setNextValue = prevV3int != nextV3int)
{
nextValue = nextV3int;
}
}
else if (valueType == typeof(Color))
{
Color prevColor = (Color)value;
Color nextColor = EditorGUILayout.ColorField(showLableName, prevColor);
if (setNextValue = prevColor != nextColor)
{
nextValue = nextColor;
}
}
else if (valueType == typeof(Bounds))
{
Bounds prevBounds = (Bounds)value;
Bounds nextBounds = EditorGUILayout.BoundsField(showLableName, prevBounds);
if (setNextValue = prevBounds != nextBounds)
{
nextValue = nextBounds;
}
}
else if (valueType == typeof(Rect))
{
Rect prevRect = (Rect)value;
Rect nextRect = EditorGUILayout.RectField(showLableName, prevRect);
if (setNextValue = prevRect != nextRect)
{
nextValue = nextRect;
}
}
else if (valueType == typeof(RectInt))
{
RectInt prevRectInt = (RectInt)value;
RectInt nextRectInt = EditorGUILayout.RectIntField(showLableName, prevRectInt);
if (setNextValue = !prevRectInt.Equals(nextRectInt))
{
nextValue = nextRectInt;
}
}
else if (typeof(UnityObject).IsAssignableFrom(valueType))
{
UnityObject prevUnityObj = (UnityObject)value;
UnityObject nextUnityObj = EditorGUILayout.ObjectField(showLableName, prevUnityObj, valueType, true);
if (setNextValue = prevUnityObj != nextUnityObj)
{
nextValue = nextUnityObj;
}
}
else if (valueType.BaseType == typeof(Enum))
{
Enum prevEnum = (Enum)value;
Enum nextEnum = EditorGUILayout.EnumPopup(showLableName, prevEnum);
if (setNextValue = prevEnum != nextEnum)
{
nextValue = nextEnum;
}
}
else if (valueType.BaseType == typeof(TypeInfo))
{
bool tempPrev = GUI.enabled;
GUI.enabled = tempPrev;
EditorGUILayout.TextField(showLableName, value.ToString());
GUI.enabled = tempPrev;
}
else
{
// TODO @Hiko add fold/unfold to save some space
bool isSerializable = null != valueType.GetCustomAttribute<SerializableAttribute>(); // check if target is Serializable.
if (isSerializable)
{
object structValueObject = value;
EditorGUILayout.LabelField(showLableName);
FieldInfo[] members = valueType.GetFields();
for (int i = 0, length = members.Length; i < length; i++)
{
FieldInfo memberInfo = members[i];
if (MemberTypes.Field == memberInfo.MemberType && !memberInfo.IsStatic) // I dun need static/const field
{
Type fieldType = memberInfo.FieldType; // better to pass this type in, cuz value could be null
object prevValueObj = memberInfo.GetValue(value);
if (TryDrawField(prevValueObj, memberInfo.Name, fieldType, depth + 1, out object nextValueObj, out bool setNext, readonlyField))
{
if (setNext)
{
setNextValue = true;
memberInfo.SetValue(structValueObject, nextValueObj);
}
}
isDrawn = true;
}
}
nextValue = structValueObject;
}
else
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"{showLableName}(NonSupprtType/NonSerializable):");
string valueStr = value.ToString();
valueStr = valueStr.PadLeft(valueStr.Length + (depth + 1) * 4);
EditorGUILayout.LabelField(valueStr);
EditorGUILayout.EndVertical();
isDrawn = true;
}
}
return isDrawn;
}
}

public static IEnumerable<FieldInfo> GetAllFields(object target, Func<FieldInfo, bool> predicate)
{
if (target == null)
{
Debug.LogError("The target object is null. Check for missing scripts.");
yield break;
}

List<Type> types = GetSelfAndBaseTypes(target);

for (int i = types.Count - 1; i >= 0; i--)
{
IEnumerable<FieldInfo> fieldInfos = types[i]
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly)
.Where(predicate);

foreach (var fieldInfo in fieldInfos)
{
yield return fieldInfo;
}
}
}

private static List<Type> GetSelfAndBaseTypes(object target)
{
List<Type> types = new List<Type>()
{
target.GetType()
};

while (types.Last().BaseType != null)
{
types.Add(types.Last().BaseType);
}

return types;
}
}
}

接下来就是编写一个 Inpector 脚本了。最后一步,非常简单。在默认的绘制之后再绘制我们捕捉到的需要绘制的非序列化字段。

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

namespace Core.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(UnityObject), true)]
public class TempInspector : UnityEditor.Editor
{
private IEnumerable<FieldInfo> m_nonSerializedFields;

protected virtual void OnEnable()
{
m_nonSerializedFields = TempGUIHelper.GetAllFields(target, f => f.GetCustomAttributes(typeof(TempShowInInspectorAttribute), true).Length > 0);
}

public override void OnInspectorGUI()
{
DrawDefaultInspector(); // draw default, make sure old stuff works
DrawNonSerializeFileds();
}

protected void DrawNonSerializeFileds()
{
if (m_nonSerializedFields.Any())
{
foreach (var field in m_nonSerializedFields)
{
TempShowInInspectorAttribute fieldAttribute = field.GetCustomAttribute<TempShowInInspectorAttribute>();
bool isReadonlyField = null == fieldAttribute || fieldAttribute.IsReadonlyField || field.IsStatic; // make static fields readonly cuz I just think we should not change them
TempGUIHelper.DrawTempShowInInspectorField(serializedObject.targetObject, field, isReadonlyField);
}
}
}

}
}

结尾

其实这个绘制的功能是我最开始工作的时候一直好奇且想去做的,那个时候也上网看了下论坛很快就明白了做法,但是因为项目组买了 Odin 所以就没必要我再做一次了。
没想到这段时间却因为项目中使用的两个插件冲突而有这个机会去依葫芦画瓢实现一次这个功能,还挺开心的。

P.S: Mirror 还定义了一个 Attribute 叫做 [Mirror.ShowInInspector],它的用处是展示 SyncList 里的内容。但是,它展示 SyncList 内容的方法是把每个元素都 ToSting。
这个坑我也是看了他的代码才知道,我一开始还觉得它画不出 SyncList<CustomType> 是我自己的问题,恁麻了。