FishPlayer

一个喜欢摸鱼的废物

0%

本周接到了强制引导的功能需求,本周就要做好。我之前没有做过这个需求,说实在的不知道那个图上挖洞的功能怎么做。

因为时间紧迫,用现有的功能糊弄了一个,思路比较蠢: 挖洞区域(父节点,带Mask) -> 被挖洞的目标背景图(子节点,使用网上的mask反转组件)
这个做法有个巨麻烦的点就是当需要聚焦UI空间的时候,需要先让父节点对目标UI,然后让子物体调整尺寸撑满屏幕。预览起来也很麻烦。

最近稍微看了下 UI Mask 的工作原理,想着要不自己做一个方便一些的。于是决定先参考 RectMask2D 的工作原理做一个。

思路

RectMask2D 的工作方法是 给 MaskableGraphic 设置剪裁区域(SetClipRect),区域外的部分都会被裁切不显示。

1
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);

那么我只要稍微反一下,就可以变成区域内的不显示了。

1
color.a *= 1 - UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);

所以最后我们需要一个 UI Shader 赋给被裁切的物体,和一个去调整裁切区域的脚本。

UI Shader (和UI Default基本没区别,就是裁切区域那边反了一下)

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

Shader "UI/CutTarget"
{
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)

_StencilComp("Stencil Comparison", Float) = 8
_Stencil("Stencil ID", Float) = 0
_StencilOp("Stencil Operation", Float) = 0
_StencilWriteMask("Stencil Write Mask", Float) = 255
_StencilReadMask("Stencil Read Mask", Float) = 255

_ColorMask("Color Mask", Float) = 15

[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
}

SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}

Stencil
{
Ref[_Stencil]
Comp[_StencilComp]
Pass[_StencilOp]
ReadMask[_StencilReadMask]
WriteMask[_StencilWriteMask]
}

Cull Off
Lighting Off
ZWrite Off
ZTest[unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask[_ColorMask]

Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP

struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};

sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;

v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

OUT.color = v.color * _Color;
return OUT;
}

fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

#ifdef UNITY_UI_CLIP_RECT
// only edit this line :D
color.a *= 1 - UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif

#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif

return color;
}
ENDCG
}
}
}


//#ifndef UNITY_UI_INCLUDED
//#define UNITY_UI_INCLUDED
//
//inline float UnityGet2DClipping(in float2 position, in float4 clipRect)
//{
// float2 inside = step(clipRect.xy, position.xy) * step(position.xy, clipRect.zw);
// return inside.x * inside.y;
//}
//
//inline fixed4 UnityGetUIDiffuseColor(in float2 position, in sampler2D mainTexture, in sampler2D alphaTexture, fixed4 textureSampleAdd)
//{
// return fixed4(tex2D(mainTexture, position).rgb + textureSampleAdd.rgb, tex2D(alphaTexture, position).r + textureSampleAdd.a);
//}
//#endif

调整裁切区域的脚本可以参考 RectMask2D 来写,我只是浅浅试一试。

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

[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(RectTransform))]
public class CutRectSetter : UIBehaviour, IClipper
{
[SerializeField]
private Canvas _canvas;
[SerializeField]
private MaskableGraphic _clipTarget;

private bool m_shouldRecalculateClipRects = false;
private Rect m_prevClipRect = default;
private bool m_hasClipped = false;

public void PerformClipping()
{
// IDK why unity use ReferenceEquals, I just copied it from RectMask2D.cs :(
if (ReferenceEquals(_canvas, null) || null == _clipTarget)
{
return;
}

RectTransform selfRectTransform = transform as RectTransform;
RectTransform targetTectTransform = _clipTarget.transform as RectTransform;
if (!IsInsideRect(selfRectTransform, targetTectTransform))
{
CancelClip();
m_prevClipRect = default;
m_shouldRecalculateClipRects = false;
return;
}

Rect currentRect = selfRectTransform.rect;
Vector2 selfPivot = selfRectTransform.pivot;
// get pivot position
currentRect.position = SwitchToRectTransform(selfRectTransform, targetTectTransform);
// rect's position is bottom left
Vector2 delta = new Vector2(-currentRect.width * selfPivot.x, -currentRect.height * selfPivot.y);
currentRect.position += delta;

m_shouldRecalculateClipRects = m_shouldRecalculateClipRects || m_prevClipRect != currentRect;
if (m_shouldRecalculateClipRects)
{
_clipTarget.SetClipRect(default, true);
_clipTarget.SetClipRect(currentRect, true);
_clipTarget.Cull(currentRect, true);
m_prevClipRect = currentRect;
m_shouldRecalculateClipRects = false;
}
}

[ContextMenu(nameof(CancelClip))]
private void CancelClip()
{
_clipTarget.SetClipRect(default, true);
m_hasClipped = false;
}

protected override void OnEnable()
{
base.OnEnable();
m_shouldRecalculateClipRects = true;
ClipperRegistry.Register(this);
MaskUtilities.Notify2DMaskStateChanged(this);
}

protected override void OnDisable()
{
base.OnDisable();
ClipperRegistry.Unregister(this);
MaskUtilities.Notify2DMaskStateChanged(this);
CancelClip();
}

protected override void OnTransformParentChanged()
{
m_shouldRecalculateClipRects = true;
}

protected override void OnCanvasHierarchyChanged()
{
m_shouldRecalculateClipRects = true;
}

#region some utils

private readonly Vector2 RECT_TOP = new Vector2(0.5f, 1f);
private readonly Vector2 RECT_BOTTOM = new Vector2(0.5f, 0f);
private readonly Vector2 RECT_LEFT = new Vector2(0f, 0.5f);
private readonly Vector2 RECT_RIGHT = new Vector2(1f, 0.5f);
private readonly Vector2 RECT_CENTER = new Vector2(0.5f, 0.5f);

private readonly Vector2 RECT_BOTTOM_LEFT = new Vector2(0f, 0f);
private readonly Vector2 RECT_BOTTOM_RIGHT = new Vector2(1f, 0f);
private readonly Vector2 RECT_TOP_LEFT = new Vector2(0f, 1f);
private readonly Vector2 RECT_TOP_RIGHT = new Vector2(1f, 1f);

private Vector2 SwitchToRectTransform(RectTransform from, RectTransform to)
{
Vector2 screenP = RectTransformUtility.WorldToScreenPoint(null, from.position);
RectTransformUtility.ScreenPointToLocalPointInRectangle(to, screenP, null, out Vector2 localPoint);
return localPoint;
}

private bool IsInsideRect(RectTransform rectTransform, RectTransform outterRect)
{
Vector3 checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_BOTTOM_LEFT);
bool bottomLeftOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);
checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_BOTTOM_RIGHT);
bool bottomRightOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);
checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_TOP_LEFT);
bool topLeftOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);
checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_TOP_RIGHT);
bool topRightOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);

return !(bottomLeftOut || bottomRightOut || topLeftOut || topRightOut);
}

private Vector3 RectOffsetToWorld(RectTransform rectTransform, Vector2 normalizedRectPosition)
{
Rect selfRect = rectTransform.rect;
Vector2 localposition = Vector2.zero;
if (RECT_CENTER == normalizedRectPosition) // RectPositionType.Center
{
localposition = selfRect.center;
}
else if (RECT_TOP == normalizedRectPosition) // RectPositionType.Top
{
localposition = new Vector2(selfRect.center.x, selfRect.yMax);
}
else if (RECT_BOTTOM == normalizedRectPosition) // RectPositionType.Bottom
{
localposition = new Vector2(selfRect.center.x, selfRect.yMin);
}
else if (RECT_LEFT == normalizedRectPosition) // RectPositionType.Left
{
localposition = new Vector2(selfRect.xMin, selfRect.center.y);
}
else if (RECT_RIGHT == normalizedRectPosition) // RectPositionType.Right
{
localposition = new Vector2(selfRect.xMax, selfRect.center.y);
}
else if (RECT_TOP_LEFT == normalizedRectPosition) // RectPositionType.TopLeft
{
localposition = new Vector2(selfRect.xMin, selfRect.yMax);
}
else if (RECT_TOP_RIGHT == normalizedRectPosition) // RectPositionType.TopRight
{
localposition = selfRect.max;
}
else if (RECT_BOTTOM_LEFT == normalizedRectPosition) // RectPositionType.BottomLeft
{
localposition = selfRect.min;
}
else if (RECT_BOTTOM_RIGHT == normalizedRectPosition) // RectPositionType.BottomRight
{
localposition = new Vector2(selfRect.xMax, selfRect.yMin);
}
else
{
Debug.LogError($"{normalizedRectPosition} is not a normalized rect position");
}

return rectTransform.TransformPoint(localposition);
}

#endregion

}

试了一下感觉功能可用,调整挖口大小的参考物体无需作为被挖物体的父或子,效果预览非常方便。

但这个做法有两个比较可惜的点
1 此做法只能挖放行的缺口
2 当前只挖缺口,没有做柔软处理,估计还得照着 RectMask2D 的思路去做软化

最近做的新项目没有使用 DOTween 了,转而使用性能比较高的 PrimeTween。这是一个比较新的插件,看了下大概是前两年上架的。
比起历史悠久的 DOTween 相比, PrimeTween 虽然性能很高,但是不像 DOTween 有丰富的特性以及一些额外的其它插件的联动支持。

具体的就说 UniTask 吧,他有一个专门的拓展类给予 DOTwee 异步相关的支持。这里面有我现在马上就需要用到的 “使用 CancellationToken 取消对 Tween 的异步等待” 的功能。

PrimeTween 没有这个支持,没关系,我只要照着这个拓展类抄一个就好,虽然这俩Tween的实现不太一样,但是核心的行为还是非常相似的。

思路

稍微点F12跳两下就发现,其实这个功能的实现思路就是通过方法 CancellationToken.Register 去给C ancellationToken 收到取消请求的时机注册一个回调。在这个回调中去处理关于Tween的各种处理。

DOTween 的拓展处理中会把原本 Tween 的正常播放完成相关回调拿出来然后缓存在自己的结构中并换上自己的响应动画的正常完成。PrimeTween 和 DOTween 不一样的地方在于 PrimeTween 的回调不能重复绑定,所以照着抄的时候为了保证代码的一致性,我选择用 PrimeTween 插件中的 Sequence 把原本的 Tween/Sequence 包装起来,似乎这个应该是他插件本身就推荐的做法。当然包起来以后就不可以再编辑原 Tween 的一些参数了。

PrimeTween 高性能的原因似乎是他把实际执行 Tween 行为的实例都用对象池管理起来了,不像 DOTween 需要用户自己管理动画实例对象。而且 DOTween 的用于存放相关动画参数的数据结构 TweenSetting 好像也是类,这点其实让我有点讨厌。

代码

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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455

namespace Cysharp.Threading.Tasks
{
public enum TweenCancelBehaviour
{
KillAndCancelAwait,
// TODO @Hiko finish those cancel behaviour if really need :)
// Kill,
//KillWithCompleteCallback,
}

public static class PrimeTweenAsyncExtensions
{
public struct TweenAwaiter : ICriticalNotifyCompletion
{
private readonly Tween tween;
private readonly Sequence warppedSequence;

// killed(non active) as completed.
public bool IsCompleted => !tween.isAlive;

public TweenAwaiter(Tween tween)
{
this.tween = tween;
warppedSequence = Sequence.Create(this.tween);
}

public TweenAwaiter GetAwaiter()
{
return this;
}

public void GetResult() { }

public void OnCompleted(Action continuation)
{
UnsafeOnCompleted(continuation);
}

public void UnsafeOnCompleted(Action continuation)
{
// onKill is called after OnCompleted, both Complete(false/true) and Kill(false/true).
warppedSequence.OnComplete(PooledPrimeTweenCallback.Create(continuation));
}
}

public static TweenAwaiter GetAwaiter(this Tween tween)
{
return new TweenAwaiter(tween);
}

public static UniTask WithCancellation(in this Tween tween, CancellationToken cancellationToken)
{
if (!tween.isAlive) return UniTask.CompletedTask;
return new UniTask(TweenConfiguredSource.Create(tween, TweenCancelBehaviour.KillAndCancelAwait, cancellationToken, out var token, out _), token);
}

public static UniTask WithCancellation(in this Tween tween, CancellationToken cancellationToken, out Sequence warppedTween)
{
warppedTween = default;
if (!tween.isAlive) return UniTask.CompletedTask;
return new UniTask(TweenConfiguredSource.Create(tween, TweenCancelBehaviour.KillAndCancelAwait, cancellationToken, out var token, out warppedTween), token);
}

private sealed class TweenConfiguredSource : IUniTaskSource, ITaskPoolNode<TweenConfiguredSource>
{
private static TaskPool<TweenConfiguredSource> s_pool;

private TweenConfiguredSource m_nextNode;

private Tween m_tween;
// use sequence to warp the original tween cuz original tween may already bind a completion callback
private Sequence m_warppedSequence;

private TweenCancelBehaviour m_cancelBehaviour;
private CancellationToken m_cancellationToken;
private CancellationTokenRegistration m_cancellationRegistration;
bool m_isCanceled;

private UniTaskCompletionSourceCore<AsyncUnit> m_core;

/// <summary>
/// event will be triggered when the tween completes normally
/// </summary>
private readonly Action m_tweenCompleteNotifyCallback;

public ref TweenConfiguredSource NextNode => ref m_nextNode;

static TweenConfiguredSource()
{
TaskPool.RegisterSizeGetter(typeof(TweenConfiguredSource), () => s_pool.Size);
}

private TweenConfiguredSource()
{
m_tweenCompleteNotifyCallback = OnTweenCompleteCallbackDelegate;
}

public static IUniTaskSource Create(Tween tween, TweenCancelBehaviour cancelBehaviour, CancellationToken cancellationToken, out short token)
{
return Create(tween, cancelBehaviour, cancellationToken, out token, out _);
}

public static IUniTaskSource Create(Tween tween, TweenCancelBehaviour cancelBehaviour, CancellationToken cancellationToken, out short token, out Sequence warppedTween)
{
if (cancellationToken.IsCancellationRequested)
{
DoCancelBeforeCreate(tween, cancelBehaviour);
warppedTween = default;
return AutoResetUniTaskCompletionSource.CreateFromCanceled(cancellationToken, out token);
}

if (!s_pool.TryPop(out var result))
{
result = new TweenConfiguredSource();
}

result.m_tween = tween;
result.m_cancelBehaviour = cancelBehaviour;
result.m_cancellationToken = cancellationToken;
result.m_isCanceled = false;
// warp the original tween and bind our comlete callback
result.m_warppedSequence = Sequence.Create(result.m_tween);
result.m_warppedSequence.OnComplete(result.m_tweenCompleteNotifyCallback);

if (cancellationToken.CanBeCanceled)
{
result.m_cancellationRegistration = cancellationToken.RegisterWithoutCaptureExecutionContext(x =>
{
TweenConfiguredSource source = (TweenConfiguredSource)x;
switch (source.m_cancelBehaviour)
{
case TweenCancelBehaviour.KillAndCancelAwait:
default:
// token canceled stop tween
source.m_isCanceled = true;
source.m_warppedSequence.Stop();
source.OnTweenCompleteCallbackDelegate();
break;
}
}, result);
}

TaskTracker.TrackActiveTask(result, 3);
token = result.m_core.Version;
warppedTween = result.m_warppedSequence;
return result;
}

private static void DoCancelBeforeCreate(Tween tween, TweenCancelBehaviour tweenCancelBehaviour)
{
switch (tweenCancelBehaviour)
{
case TweenCancelBehaviour.KillAndCancelAwait:
default:
tween.Stop();
break;
}
}

public UniTaskStatus UnsafeGetStatus()
{
return m_core.UnsafeGetStatus();
}

public void OnCompleted(Action<object> continuation, object state, short token)
{
m_core.OnCompleted(continuation, state, token);
}

public void GetResult(short token)
{
try
{
m_core.GetResult(token);
}
finally
{
TryReturnSelfToPool();
}
}

public UniTaskStatus GetStatus(short token)
{
return m_core.GetStatus(token);
}

private bool TryReturnSelfToPool()
{
TaskTracker.RemoveTracking(this);
m_core.Reset();
m_cancellationRegistration.Dispose();

m_tween = default;
m_warppedSequence = default;
m_cancellationToken = default;
return s_pool.TryPush(this);
}

/// <summary>
/// Tween completes normally
/// </summary>
private void OnTweenCompleteCallbackDelegate()
{
if (m_isCanceled)
{
m_core.TrySetCanceled(m_cancellationToken);
}
else
{
m_core.TrySetResult(AsyncUnit.Default);
}
}
}

//----------------------------------

public struct SequenceAwaiter : ICriticalNotifyCompletion
{
private readonly Sequence originSequence;
private readonly Sequence warppedSequence;

// killed(non active) as completed.
public bool IsCompleted => !originSequence.isAlive;

public SequenceAwaiter(Sequence sequence)
{
originSequence = sequence;
warppedSequence = Sequence.Create();
warppedSequence.Insert(0f, sequence);
}

public SequenceAwaiter GetAwaiter()
{
return this;
}

public void GetResult() { }

public void OnCompleted(Action continuation)
{
UnsafeOnCompleted(continuation);
}

public void UnsafeOnCompleted(Action continuation)
{
// onKill is called after OnCompleted, both Complete(false/true) and Kill(false/true).
warppedSequence.OnComplete(PooledPrimeTweenCallback.Create(continuation));
}
}

public static SequenceAwaiter GetAwaiter(this Sequence tween)
{
return new SequenceAwaiter(tween);
}

public static UniTask WithCancellation(in this Sequence sequence, CancellationToken cancellationToken)
{
if (!sequence.isAlive) return UniTask.CompletedTask;
return new UniTask(SequenceConfiguredSource.Create(sequence, TweenCancelBehaviour.KillAndCancelAwait, cancellationToken, out var token, out _), token);
}

public static UniTask WithCancellation(in this Sequence sequence, CancellationToken cancellationToken, out Sequence warppedTween)
{
warppedTween = default;
if (!sequence.isAlive) return UniTask.CompletedTask;
return new UniTask(SequenceConfiguredSource.Create(sequence, TweenCancelBehaviour.KillAndCancelAwait, cancellationToken, out var token, out warppedTween), token);
}

private sealed class SequenceConfiguredSource : IUniTaskSource, ITaskPoolNode<SequenceConfiguredSource>
{
private static TaskPool<SequenceConfiguredSource> s_pool;

private SequenceConfiguredSource m_nextNode;

private Sequence m_originSequence;
// use sequence to warp the original sequence cuz original tween may already bind a completion callback
private Sequence m_warppedSequence;

private TweenCancelBehaviour m_cancelBehaviour;
private CancellationToken m_cancellationToken;
private CancellationTokenRegistration m_cancellationRegistration;
bool m_isCanceled;

private UniTaskCompletionSourceCore<AsyncUnit> m_core;

private readonly Action m_SequenceCompleteNotifyCallback;

public ref SequenceConfiguredSource NextNode => ref m_nextNode;

static SequenceConfiguredSource()
{
TaskPool.RegisterSizeGetter(typeof(SequenceConfiguredSource), () => s_pool.Size);
}

private SequenceConfiguredSource()
{
m_SequenceCompleteNotifyCallback = OnSequenceCompleteCallbackDelegate;
}

public static IUniTaskSource Create(Sequence Sequence, TweenCancelBehaviour cancelBehaviour, CancellationToken cancellationToken, out short token)
{
return Create(Sequence, cancelBehaviour, cancellationToken, out token, out _);
}

public static IUniTaskSource Create(Sequence Sequence, TweenCancelBehaviour cancelBehaviour, CancellationToken cancellationToken, out short token, out Sequence warppedSequence)
{
if (cancellationToken.IsCancellationRequested)
{
DoCancelBeforeCreate(Sequence, cancelBehaviour);
warppedSequence = default;
return AutoResetUniTaskCompletionSource.CreateFromCanceled(cancellationToken, out token);
}

if (!s_pool.TryPop(out var result))
{
result = new SequenceConfiguredSource();
}

result.m_originSequence = Sequence;
result.m_cancelBehaviour = cancelBehaviour;
result.m_cancellationToken = cancellationToken;
result.m_isCanceled = false;

result.m_warppedSequence = Sequence.Create();
result.m_warppedSequence.Insert(0f, result.m_originSequence);
result.m_warppedSequence.OnComplete(result.m_SequenceCompleteNotifyCallback);

if (cancellationToken.CanBeCanceled)
{
result.m_cancellationRegistration = cancellationToken.RegisterWithoutCaptureExecutionContext(x =>
{
SequenceConfiguredSource source = (SequenceConfiguredSource)x;
switch (source.m_cancelBehaviour)
{
case TweenCancelBehaviour.KillAndCancelAwait:
source.m_isCanceled = true;
source.m_warppedSequence.Stop();
source.OnSequenceCompleteCallbackDelegate();
break;
default:
break;
}
}, result);
}

TaskTracker.TrackActiveTask(result, 3);
token = result.m_core.Version;
warppedSequence = result.m_warppedSequence;
return result;
}

private static void DoCancelBeforeCreate(Sequence Sequence, TweenCancelBehaviour tweenCancelBehaviour)
{
switch (tweenCancelBehaviour)
{
case TweenCancelBehaviour.KillAndCancelAwait:
default:
Sequence.Stop();
break;
}
}

public UniTaskStatus UnsafeGetStatus()
{
return m_core.UnsafeGetStatus();
}

public void OnCompleted(Action<object> continuation, object state, short token)
{
m_core.OnCompleted(continuation, state, token);
}

public void GetResult(short token)
{
try
{
m_core.GetResult(token);
}
finally
{
TryReturnSelfToPool();
}
}

public UniTaskStatus GetStatus(short token)
{
return m_core.GetStatus(token);
}

private bool TryReturnSelfToPool()
{
TaskTracker.RemoveTracking(this);
m_core.Reset();
m_cancellationRegistration.Dispose();

m_originSequence = default;
m_warppedSequence = default;
m_cancellationToken = default;
return s_pool.TryPush(this);
}

private void OnSequenceCompleteCallbackDelegate()
{
if (m_isCanceled)
{
m_core.TrySetCanceled(m_cancellationToken);
}
else
{
m_core.TrySetResult(AsyncUnit.Default);
}
}
}

}

public sealed class PooledPrimeTweenCallback
{
private static readonly ConcurrentQueue<PooledPrimeTweenCallback> s_pool = new ConcurrentQueue<PooledPrimeTweenCallback>();

private readonly Action m_runDelegate;
private Action m_continuation;

private PooledPrimeTweenCallback()
{
m_runDelegate = Run;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Action Create(Action continuation)
{
if (!s_pool.TryDequeue(out PooledPrimeTweenCallback item))
{
item = new PooledPrimeTweenCallback();
}

item.m_continuation = continuation;
return item.m_runDelegate;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Run()
{
Action call = m_continuation;
m_continuation = null;
if (call != null)
{
s_pool.Enqueue(this);
call.Invoke();
}
}

}
}

序列化(serialization)在计算机科学的资料处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

序列化的发生

当我们用(UnityEngine.Object.Instantiate())此方法去生成一个 UnityObject 时候,就经历了序列化和反序列化的操作。

Original Object —serialize—> mid-data —deserialize—> Spawned Object

序列化使用场景

保存相关资产

我们的许多 UnityObject 资产 scene, prefab, material 都是以文本文件(YAML)的形式保存在项目中。编辑器时,会在 Library 中存放’中间物体’。
我猜测,编辑器时会使用这些’中间物体’以减少实际序列化的步骤来加速运行。

当我们将磁盘里的 Asset 加载到游戏的运行时里的 UnityObject 的过程,是一个反序列化操作,把YAML反序列化成 UnityObject。

在编辑器时,我们会修改脚本,改变一些数据类型。把字段名称修改后,原本序列化的值丢失了。我猜是因为项目在 Refresh 的时候,会先把所有必要的数据都序列化存入’中间物体’,然后等代码编译完,生成了新的类型,再尝试把原本的’中间物体’用新的类型反序列化生成新物体并在序列化生成新的’中间物体’。这个过程中由于是使用新类型进行反序列化了,自然会抛弃新类型中没有的(不同名且类型不匹配的)数据。引擎C#层重载 HotReloading 会报错但是有可能凑合能跑估计也是这个原因,所以 HotReloading 实际上是为了让我们快速修改业务逻辑看看有无修好的。

FormerlySerializedAs(PROPERTY_NAME) 这个特性可以解决上面说的这个问题,但其实就是要求这个字段做序列化相关操作时沿用旧的字段名(PROPERTY_NAME)。这样在 prefab 中序列化下来记录的字段名一直都是那个旧的字段名。

编辑器的字段绘制

当我们对字段做做编辑器绘制的时候,也会有序列化以及反序列化的操作。

Inspector —update changes—> Serialized Properties —deserialize—> Spawned Object
Inspector <—update view— Serialized Properties —serialize—> Spawned Object

UnityObject 中的字段值会被序列化到 Serialized Properties 集合中,然后编辑器绘制相关的代码拿到序列化后得到的数据集合,再把他们提出来并根据数据类型画不同的样式。
下面是一个 PropertuDrawer 的例子,在绘制方法的参数中有 SerializedProperty,这个就是用于存放 UnityObject 中被序列化字段的 wrapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
[CustomPropertyDrawer(typeof(NormalizedCurve))]
public class NormalizedCurvePropertyDrawer : PropertyDrawer
{
const string CURVE_FIELD = "_curve";
static readonly Rect RANGES = new Rect(0.0f, 0.0f, 1.0f, 1.0f);

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
var curveProperty = property.FindPropertyRelative(CURVE_FIELD);
// ....
}
}

对 UnityObject 引用的序列化

哦我的老兄,这个大家都很熟悉了。以文本的形式打开我们的 prefab 康康。摘一段

UnityScriptClassType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--- !u!114 &7941049597127225971 // !u!CLASS_TYPE &OBJECT_FILE_ID
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3753066996938937369}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 43f865a8433367346843b29a2464c1a7, type: 3} // guid point to this object instance's asset source
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}

首行表明了该 UnityObject 的类型以及它在这个 Prefab 中的唯一标识ID。
m_Script 这一行表明了这个物体是个 .cs 脚本。

我们还能发现有些字段只记录 { FileID }, 有些却要记录三个参数 {fileID, guid, type} 。记录三个参数的表明的是这个 prefab 内的物体的外部来源。听起来很别扭,我说详细点吧,就好比说一个 prefab A 中包含了一个 prefab B,那么 B 物体就是需要在 prefab A 中以如上方式记录。 其中 guid 是 prefab B 所在(定义处)的资源文件的 guid, FileID 则是这个物体在资源内的唯一ID,type 是这个物体的资源类型。

所以 UnityObject 引用的序列化是依靠以文本的形式记录ID完成的。

对空引用(null)的支持

空引用只支持 UnityObject 捏,如果是我们自定义的 C# class, 序列化时会生成默认的给他。这个时候如果有疯狂嵌套的 C# class就会爆炸了,因为他会一直往里层生成默认值给这些可序列化的 C# Class object。

Library 目录下有大量的缓存文件。其中包含着导入进项目里的资源文件以及一些导入资源后产生的用于文件(可能是用于记录关系或是一些关于资源的额外的缓存数据?)。

每当有资源导入/刷新修改时,Library 都会有文件发生变更(增删改)。

Artifacts 目录

Unity 提供了一个工具可以把 Artifacts 里的文件转成人话

C:\Program Files\Unity\Hub\Editor\UNITY_VERSION\Editor\Data\Tools\binary2text.exe

Artifacts 目录下存放各个资源在导入阶段后期生成的缓存文件。似乎这些文件由资产的哈希值作为键来命名。这些文件在 Unity 内部被称为 producedFiles (在 Log 中使用改名字称呼这些文件;但在AssetV2中似乎已经没有这个东西)。

为什么 Libraray 目录会比 Asset 大很多,其中一个原因是这样的。以脚本文件举例,在这个缓存文件中,不仅仅有该脚本的代码文本,同时还以文本的形式记录着该资产相关的一些ID,以及资产的存储类型?。

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
ID: 3 (ClassID: 1035) MonoImporter
m_ObjectHideFlags 0 (unsigned int)
m_CorrespondingSourceObject (PPtr<EditorExtension>)
m_FileID 0 (int)
m_PathID 0 (SInt64) // signed long?
m_PrefabInstance (PPtr<PrefabInstance>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_PrefabAsset (PPtr<Prefab>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_Name "" (string)
m_ExternalObjects (map)
size 0 (int)

m_UsedFileIDs (set)
size 1 (int)
data (SInt64) #0: 11500000
m_DefaultReferences (vector)
size 0 (int)

executionOrder 0 (SInt16)
icon (PPtr<Texture2D>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_UserData "" (string)
m_AssetBundleName "" (string)
m_AssetBundleVariant "" (string)


ID: 11500000 (ClassID: 115) MonoScript
m_ObjectHideFlags 0 (unsigned int)
m_CorrespondingSourceObject (PPtr<EditorExtension>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_PrefabInstance (PPtr<PrefabInstance>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_PrefabAsset (PPtr<Prefab>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_Name "IntEqualBehaviour" (string)
m_Script "using UnityEngine;

" (string) // 资产的存储类型?
m_DefaultReferences (map)
size 0 (int)

m_Icon (PPtr<Object>)
m_FileID 0 (int)
m_PathID 0 (SInt64)
m_ExecutionOrder 0 (int)
m_ClassName "IntEqualBehaviour" (string)
m_Namespace "Core.UI" (string)

这是一条导入的Log.

[Worker0] Start importing Assets/Script/UI/Core/PrimitiveBehaviour/IntEqualBehaviour.cs using Guid(ce9400962fc00544a89e0e69c863b84c) Importer(815301076,1909f56bfc062723c751e8b465ee728b) [Worker0] -> (artifact id: ‘840ab621654cd480e1699784fa88cf93’) in 0.005725 seconds

‘ce9400962fc00544a89e0e69c863b84c’ 是该脚本作为资产的GUID,和meta文件中的GUID一致。
Importer(815301076,1909f56bfc062723c751e8b465ee728b) 猜测为指向倒入该资源使用的导入器类型,以及该导入器作为 Asset 时候的GUID。

如下代码用于追踪某个资产在 Artifacts 中的键名和位置。

1
2
3
string guidString = AssetDatabase.AssetPathToGUID(assetPath);
string hash = AssetDatabaseExperimental.GetArtifactHash(guidString);
AssetDatabaseExperimental.GetArtifactPaths(hash, out var paths);

缓存损坏处理

这个问题造成的原因很多,但是基本就是 Artifacts 里的关于资产的缓存文件坏了。其表现就是那个资产的图标变成白纸,然后无法使用了。
但是有时候对着这个资产点击 Reimport 并不能解决问题。

我认为正确的思路就是把这个坏的缓存杀掉,再生成一个新的

  1. 只有几个资产的缓存坏掉的情况,可以把这几个资产从项目中拿出来。刷新项目,再把资产放回去,重新生成缓存。
  2. 有多个资产缓存爆炸的情况,需要先收集资产列表,然后用上面的代码追溯所有坏掉的缓存并杀掉,再重新导入这些资产。(只是猜测,并未实践过。因为这个情况很少见,感觉不如直接把Library杀了全部重刷下班吃饭)

资料来源

网上找到的活爹

在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> 是我自己的问题,恁麻了。

Temp

自从开始加班以后就没什么干劲了。这段时间重头工作就是接用户SDK然后补以前落下的一些细节需求。

比较蚌埠住的是,这段时间发现了自己写的很多重复的UI组件代码。而且在这个时候已经不方便再整合替换了,整合替换可能会导致爆炸。

为了尽可能杜绝此类事情发生,决定先把用到的一些基础代码总结一下写在这里,方便下次直接过来抄而不是再手搓了。

上代码

PrimitiveBehaviour(基本结构)

首先定义表现件的最基本的结构。实质就是一个接收值的更新。
此外还多了一个标志,可用于调整该组件是否在第一次收到值得时候使用快进动画。
因为该功能在实际使用的时候,是根据美术的要求来决定的,所以我觉得不写在代码里而是开放出来供直接在prefab更改会更方便。

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

namespace Core.UI
{
// where T : IComparable, IComparable<T>, IConvertible, IEquatable<T> 其实就是为了限制 T 必须是这些基础类型,但是感觉还是没必要这么写
public interface IBehaviourDispatcher<T> where T : IComparable, IComparable<T>, IConvertible, IEquatable<T>
{
void DispatchValueChange(T nextValue, bool instant);
}

public abstract class PrimitiveBehaviour<T> : MonoBehaviour where T : IComparable, IComparable<T>, IConvertible, IEquatable<T>
{
[SerializeField]
private bool m_instantApplyInitValue = true;

private bool m_hasInit = false;
private T m_currentValue;
private T m_previousValue;

public T currentValue => m_currentValue;
public T previousValue => m_previousValue;
public bool hasInited => m_hasInit;

/// <summary>
/// 持有数据的一方主动调用该方法,以对表现进行更新
/// </summary>
/// <param name="nextValue"></param>
/// <param name="instant">instant changing value can also used to fast forward anim</param>
public void ChangeValue(T nextValue, bool instant)
{
if (m_hasInit)
{
if (!instant && IsValueEqual(currentValue, nextValue))
{
return;
}
m_previousValue = m_currentValue;
m_currentValue = nextValue;
NotifyValueChanged(nextValue, instant);
return;
}

instant = instant || m_instantApplyInitValue;
NotifyValueChanged(nextValue, instant);
m_previousValue = m_currentValue = nextValue;
m_hasInit = true;
}

protected bool IsValueEqual(T l, T r) => 0 == l.CompareTo(r);

protected abstract void NotifyValueChanged(T nextValue, bool instant);

}

}

BoolBehaviour

需要下发数据的一方需要持有一个 BoolBehaviourDispatcher 变量,并通过其把数据变更下发出去。

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

namespace Core.UI
{
[Serializable]
public struct BoolBehaviourDispatcher : IBehaviourDispatcher<bool>
{
[SerializeField]
private BoolBehaviour[] m_behaviours;

public void DispatchValueChange(bool nextValue, bool instant)
{
int count = m_behaviours.Length;
for (int i = 0; i < count; i++)
{
BoolBehaviour behaviour = m_behaviours[i];
if (behaviour.gameObject.activeInHierarchy && behaviour.enabled)
{
behaviour.ChangeValue(nextValue, instant);
}
}
}

#if UNITY_EDITOR
[ContextMenu("FindInChildren")]
private void FindInChildren(GameObject source)
{
var behaviors = source.GetComponentsInChildren<BoolBehaviour>();
m_behaviours = behaviors;
}
#endif

}

public abstract class BoolBehaviour : PrimitiveBehaviour<bool>
{
#if UNITY_EDITOR
[Header("Editor preview"), NonReorderable]
public bool previewValue_editor = false;
public bool previrwInstant = true;

[ContextMenu("SetValuePreview")]
public void SetValuePreview()
{
if (Application.isPlaying)
{
NotifyValueChanged(previewValue_editor, previrwInstant);
return;
}
NotifyValueChanged(previewValue_editor, true);
}
#endif
}

}

FloatBehaviour

需要下发数据的一方需要持有一个 FloatBehaviourDispatcher 变量,并通过其把数据变更下发出去。

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

namespace Core.UI
{
[Serializable]
public struct FloatBehaviourDispatcher: IBehaviourDispatcher<float>
{
[SerializeField]
private FloatBehaviour[] m_behaviours;

public void DispatchValueChange(float nextValue, bool instant)
{
int count = m_behaviours.Length;
for (int i = 0; i < count; i++)
{
var behaviour = m_behaviours[i];
if (behaviour.gameObject.activeInHierarchy && behaviour.enabled)
{
behaviour.ChangeValue(nextValue, instant);
}
}
}

#if UNITY_EDITOR
[ContextMenu("FindInChildren")]
private void FindInChildren(GameObject source)
{
var behaviors = source.GetComponentsInChildren<FloatBehaviour>();
m_behaviours = behaviors;
}
#endif

}

public abstract class FloatBehaviour : PrimitiveBehaviour<float>
{
#if UNITY_EDITOR
[Header("Editor preview"), NonReorderable]
public float previewValue_editor = 1f;
public bool previrwInstant = true;

[ContextMenu("SetValuePreview")]
public void SetValuePreview()
{
if (Application.isPlaying)
{
NotifyValueChanged(previewValue_editor, previrwInstant);
return;
}
NotifyValueChanged(previewValue_editor, true);
}
#endif
}

}

IntBehaviour

需要下发数据的一方需要持有一个 IntBehaviourDispatcher 变量,并通过其把数据变更下发出去。
IntBehaviour 我定义成一个抽象类。因为实际使用的时候(我暂时遇到的情况),在UI表现上会需要有持有一个数值,数值相同不同时的表现,或是UI收到不同的数值再有不同的表现。
所以这边分做成了两个 Beheaviour 。

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

namespace Core.UI
{
[Serializable]
public struct IntBehaviourDispatcher : IBehaviourDispatcher<int>
{
[SerializeField]
private IntBehaviour[] m_behaviours;

public void DispatchValueChange(int nextValue, bool instant)
{
int count = m_behaviours.Length;
for (int i = 0; i < count; i++)
{
var behaviour = m_behaviours[i];
if (behaviour.gameObject.activeInHierarchy && behaviour.enabled)
{
behaviour.ChangeValue(nextValue, instant);
}
}
}

#if UNITY_EDITOR
[ContextMenu("FindInChildren")]
private void FindInChildren(GameObject source)
{
var behaviors = source.GetComponentsInChildren<IntBehaviour>();
m_behaviours = behaviors;
}
#endif

}

public abstract class IntBehaviour : PrimitiveBehaviour<int>
{
#if UNITY_EDITOR
[Header("Editor preview"), NonReorderable]
public int previewValue_editor = 0;
public bool previrwInstant = true;

[ContextMenu("SetValuePreview")]
public void SetValuePreview()
{
if (Application.isPlaying)
{
NotifyValueChanged(previewValue_editor, previrwInstant);
return;
}
NotifyValueChanged(previewValue_editor, true);
}
#endif
}

// 根据收到的值不同 而作不同表现
public abstract class IntSwitchBehaviour<T> : PrimitiveBehaviour<int>
{
[Serializable]
public struct IntPack
{
public int intValue;
public T targetValueDataPack;
}

[Header("Extra param")]
[SerializeField]
private bool m_hasDefaultPack = false;
[SerializeField]
private int m_defaultPackIndex = 0;
[SerializeField]
private bool m_unapplyPrevPack = true;
[SerializeField]
private IntPack[] m_packArray;

protected sealed override void NotifyValueChanged(int nextValue, bool instant)
{
int length = m_packArray.Length;
int meetIndex = -1;
IntPack pack;
for (int i = 0; i < length; i++)
{
pack = m_packArray[i];
if (pack.intValue == nextValue)
{
meetIndex = i;
continue;
}
else if (m_unapplyPrevPack)
{
UnApplyPack(pack.targetValueDataPack);
}
}
if (meetIndex > 0) // has meet
{
pack = m_packArray[meetIndex];
ApplyPack(pack.targetValueDataPack, instant);
return;
}
// apply default one
pack = m_packArray[m_defaultPackIndex];
ApplyPack(pack.targetValueDataPack, instant);
}

protected abstract void ApplyPack(T pack, bool instant);
protected abstract void UnApplyPack(T pack);
}

// 持有一个数值,判断传进来的值是否相等,再做表现
public abstract class IntEqualBehaviour : IntBehaviour
{
[SerializeField]
private int m_targetValue;

private bool m_isPreviousValueEqual = false;

protected sealed override void NotifyValueChanged(int nextValue, bool instant)
{
bool isEqual = IsValueEqual(nextValue, m_targetValue);
bool hasChanged = isEqual != m_isPreviousValueEqual;
if (hasInited)
{
if (hasChanged)
{
NotifyValueEqual(isEqual, instant);
}
return;
}
NotifyValueEqual(isEqual, instant);
}

protected abstract void NotifyValueEqual(bool equal, bool instant);

}
}

Sample

下面是一个 FloatBehaviour 和 BoolBehaviour 的例子。
只需要收到数值,FloatBehaviour 启动tween去修改 Image 的填充/ BoolBehaviour 去开关物体。其它还会用到的表现还有更改颜色,开关物体之类的,
当然实际使用的情况还得考虑 target 是否会无效,以及物体从来没开过,却调用 NotifyValueChanged 之类的细节问题的。

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

namespace Core.UI
{
[RequireComponent(typeof(Image))]
public class SlicesImageFill : FloatBehaviour
{
[Header("Fields")]
[SerializeField]
private Image m_target;
[SerializeField]
private bool m_reverse = false;
[SerializeField]
private Vector2 m_range = new Vector2(0, 1f);
[SerializeField]
private bool m_doTweenFill = false;
[SerializeField]
private TweenParameter m_tweenParameter;

private Tween m_currentTween;

protected override void NotifyValueChanged(float nextValue, bool instant)
{
m_currentTween?.Kill();
float targetFill = CalculateFillValue(nextValue);
if (instant || !m_doTweenFill)
{
m_target.fillAmount = targetFill;
return;
}
m_currentTween = m_target.DOFillAmount(targetFill, m_tweenParameter.duration);
m_currentTween.ApplyParam(m_tweenParameter);
}

private float CalculateFillValue(float rawValue)
{
return Mathf.Lerp(m_range.x, m_range.y, m_reverse ? 1 - rawValue : rawValue);
}

private void OnDestroy()
{
m_currentTween?.Kill();
}

#if UNITY_EDITOR
private void Reset()
{
m_target = GetComponent<Image>();
}
#endif
}

public class BoolReactorObjectActive : BoolBehaviour
{
[Serializable]
public struct ObjectActivePack
{
public GameObject target;
public bool inverse;
}

[Header("Extra param")]
[SerializeField]
private ObjectActivePack[] m_packs;

protected override void NotifyValueChanged(bool value, bool instant)
{
for (int i = 0, length = m_packs.Length; i < length; i++)
{
ObjectActivePack pack = m_packs[i];
pack.target.SetActive(pack.inverse ? !value : value);
}
}
}

}

总结

当前的项目里因为UI缺乏UI美术人,没有人力导图拼图,所以使用了PSD2UGUI插件。这个插件又依赖一个分析PSD的解析库psd-parser
这俩东西都已经有点老旧了,美术那边遇到了一个问题,他们做的智能对象图层不能正确导出。

直接用文字有点难描述,大概是这样的。他们做的智能对象大小就和图里这个黑底一样大,但实际有图案部分就像红框里的那样,不会占满整个智能对象的区域。
用工具导出Sprite的时候,只会导出红框的大小。


解决方法比较简单粗暴了,因为psd-parser似乎不能很方便拿到智能对象的实际的区域尺寸,我只能让美术多做一个和需要导出的只能对象尺寸大小一样的纯色空图层。
同时新增两个后缀规则,让这个纯色空图层和需要依照智能对象尺寸导出的图层分别使用。

PSD2UGUI中根据图层生成Texture的代码中添加后缀检查,检测到特殊规则的时候去去根据特殊的纯色图层的尺寸再去重新绘制Texture。同时在PSD2UGUI中根据图层创建他的ImageNode的时候有个Rect需要计算,这个Rect计算也要修改。

最近有在做手游。各个厂商手机屏幕的尺寸和比例都各不相同。UI适配可以说是一大难题叻。UI美术那边在制作的也有考虑到适配方面的问题,有许多元素是需要我们根据安全区来适配的。
但比较坑爹的一些情况是这样的,要求页面A下方有个按钮托盘,按钮托盘打开的时候要有一个大图背景去阻挡玩家点击原本页面A上的元素。这个按钮托盘还是在其他页面中也有用到的。为了解决这个问题,做了两个Rect适配的脚本。
还没完成,但是感觉基本能用,就先拉上来记一下。

这两个脚本都依赖与当前UI使用的相机和根Canvas。所以在实际使用的时候需要准备好这两个东西。

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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491

public interface UIAreaRect
{
Camera GetUICamera();
Canvas GetRootCanvas();
void Refresh();
}

[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
public class SafeAreaRect : UIBehaviour, ILayoutSelfController
{
private static readonly Vector2 s_specialPivot = Vector2.zero; // HACK @Hiko self pivot
[SerializeField]
private RectOffset _rectOffset = new RectOffset();

[SerializeField]
private Camera _uiCamera;
[SerializeField]
private Canvas _rootCanvas;

private RectTransform m_rect;
private DrivenRectTransformTracker m_tracker;

private RectTransform selfRectTransform
{
get
{
if (m_rect == null)
{
m_rect = GetComponent<RectTransform>();
}
return m_rect;
}
}

public void SetLayoutHorizontal()
{
Refresh();
}

public void SetLayoutVertical()
{
Refresh();
}

[ContextMenu("Refresh")]
public void Refresh()
{
#if UNITY_EDITOR
RebuildTracker();
#endif
Canvas rootCanvas = GetRootCanvas();
Camera uiCamera = GetUICamera();
RectTransform rectTransform = selfRectTransform, rootCanvasRectTransform = rootCanvas.transform as RectTransform;
RectTransform parent = rectTransform.transform as RectTransform;
if (parent == null)
{
Debug.LogError("This behaviour can not to be used on root recttransform");
}
if (rootCanvas == null || uiCamera == null)
{
return;
}
Rect safeRect = CalcultateCurrentSafeAreaRect();
rectTransform.pivot = s_specialPivot;
Vector3 worldUIPos;
Vector2 localPos = rootCanvasRectTransform.GetLocalRectPosition(RectPositionType.BottomLeft);
localPos.x += safeRect.x + _rectOffset.left;
float deltaX = _rectOffset.horizontal;
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, safeRect.width - deltaX);
localPos.y += safeRect.y + _rectOffset.bottom;
float deltaY = _rectOffset.vertical;
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, safeRect.height - deltaY);
worldUIPos = rootCanvasRectTransform.TransformPoint(localPos);
rectTransform.position = worldUIPos;
}

public Camera GetUICamera()
{
// TODO implement the actual code to get camera while in game or in editor
return _uiCamera;
}

public Canvas GetRootCanvas()
{
// TODO implement the actual code to get root canvas while in game or in editor
return _rootCanvas;
}

private Rect CalcultateCurrentSafeAreaRect()
{
Rect safeRect;
Canvas rootCanvas = GetRootCanvas();
RectTransform rootRectTransform = rootCanvas.transform as RectTransform;
Rect rootCanvasRect = rootRectTransform.rect;
float rootCanvasWidth = rootCanvasRect.width;
float rootCanvasHeight = rootCanvasRect.height;

// temp pretend it is ip12
float horizontalGap = 0.055f;
float verticalGap = 0.053f;
switch (SystemInfo.deviceType)
{
case DeviceType.Handheld:
Rect deviceSafeRect = Screen.safeArea;
if (Mathf.Approximately(deviceSafeRect.x, 0f))
{
// a device with saferect as fullrect
safeRect = new Rect(new Vector2(rootCanvasWidth * horizontalGap, 0f), new Vector2(rootCanvasWidth * (1f - 2 * horizontalGap), rootCanvasHeight * (1f - verticalGap)));
}
else
{
Resolution resolution = Screen.currentResolution;
float scale = rootCanvasWidth / resolution.width;
safeRect = new Rect(deviceSafeRect.x * scale, deviceSafeRect.y * scale, deviceSafeRect.width * scale, deviceSafeRect.height * scale);
}
break;
default:
// HACK @Hiko it's on PC, create a temp saferect and pretend it is ip12
safeRect = new Rect(new Vector2(rootCanvasWidth * horizontalGap, 0f), new Vector2(rootCanvasWidth * (1f - 2 * horizontalGap), rootCanvasHeight * (1f - verticalGap)));
break;
}
return safeRect;
}

private void RebuildTracker()
{
m_tracker.Clear();
m_tracker.Add(this, selfRectTransform, DrivenTransformProperties.All);
}

protected override void OnEnable()
{
base.OnEnable();
RebuildTracker();
SetDirty();
if (Application.isPlaying)
{
Refresh();
}
}

protected override void OnDisable()
{
m_tracker.Clear();
LayoutRebuilder.MarkLayoutForRebuild(selfRectTransform);
base.OnDisable();
}

private void SetDirty()
{
if (!IsActive())
{
return;
}
LayoutRebuilder.MarkLayoutForRebuild(selfRectTransform);
}

#if UNITY_EDITOR

[Header("Editor readonly debug stuff")]
[SerializeField]
private Rect _editorSafeRect;
[SerializeField]
private Vector2 _editorCurrentResolution;
[SerializeField]
private Vector2 _editorScreenSize;
[SerializeField]
private float _editorScreenFactor;
[SerializeField]
private float _editorScreenDpi;
[SerializeField]
private float _editorCanvasFactor;
[SerializeField]
private Rect _editorRootCanvasRect;

private void OnDrawGizmosSelected()
{
UpdateEditorPreviewData();
Color color = Color.blue;
Rect safeRect = CalcultateCurrentSafeAreaRect();
Canvas rootCanvas = GetRootCanvas();
RectTransform rootRectTransform = rootCanvas.transform as RectTransform;
Vector2 bottomLeft = rootRectTransform.GetLocalRectPosition(RectPositionType.BottomLeft);
bottomLeft.x += safeRect.x;
bottomLeft.y += safeRect.y;
DrawBox(rootRectTransform, bottomLeft, safeRect.size, color);
// draw safe area end

if (Mathf.Approximately(_rectOffset.horizontal, 0f) && Mathf.Approximately(_rectOffset.vertical, 0f))
{
return;
}
Rect nextRect = safeRect;
nextRect.width -= _rectOffset.horizontal;
nextRect.height -= _rectOffset.vertical;
nextRect.x += _rectOffset.left;
nextRect.y += _rectOffset.bottom;
bottomLeft = rootRectTransform.GetLocalRectPosition(RectPositionType.BottomLeft);
bottomLeft.x += nextRect.x;
bottomLeft.y += nextRect.y;
color.a *= 0.8f;
DrawBox(rootRectTransform, bottomLeft, nextRect.size, color);
// draw actual safe area end
}

private void DrawBox(RectTransform reference, Vector2 bottomLeftLocalPos, Vector2 size, Color color)
{
Color cachedColor = Gizmos.color;
Gizmos.color = color;
if (reference != null)
{
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos), reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x));
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos), reference.TransformPoint(bottomLeftLocalPos + Vector2.up * size.y));
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x), reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x + Vector2.up * size.y));
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos + Vector2.up * size.y), reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x + Vector2.up * size.y));
}
Gizmos.color = cachedColor;
}

private void UpdateEditorPreviewData()
{
_editorSafeRect = CalcultateCurrentSafeAreaRect();
_editorScreenSize = new Vector2(Screen.width, Screen.height);
_editorCurrentResolution = new Vector2(Screen.currentResolution.width, Screen.currentResolution.height);
_editorScreenFactor = Screen.currentResolution.width / Screen.width;
_editorScreenDpi = Screen.dpi;
Canvas rootCanvas = GetRootCanvas();
if (rootCanvas != null)
{
_editorCanvasFactor = rootCanvas.scaleFactor;
_editorRootCanvasRect = (rootCanvas.transform as RectTransform).rect;
}
}

#endif

}

[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
public class ScreenRawRect : UIBehaviour, ILayoutSelfController, UIAreaRect
{
private static readonly Vector2 s_specialPivot = Vector2.zero; // HACK @Hiko self pivot

[SerializeField]
private RectOffset _rectOffset = new RectOffset();

private RectTransform m_rect;

private RectTransform selfRectTransform
{
get
{
if (m_rect == null)
{
m_rect = GetComponent<RectTransform>();
}
return m_rect;
}
}

// temp solution for testing
[SerializeField]
private Camera m_uiCamera;
[SerializeField]
private Canvas m_rootCanvas;

private DrivenRectTransformTracker m_tracker;
private RectTransform m_rectTransform;

private RectTransform rectTransform
{
get
{
if (m_rectTransform == null)
m_rectTransform = GetComponent<RectTransform>();
return m_rectTransform;
}
}

public void SetLayoutHorizontal()
{
Refresh();
}

public void SetLayoutVertical()
{
Refresh();
}

[ContextMenu("Refresh")]
public void Refresh()
{
Camera uiCamera = GetUICamera();
Canvas rootCanvas = GetRootCanvas();
RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
Rect sizeRect = rootCanvasRectTransform.rect;
RectTransform selfRectTransform = rectTransform;
selfRectTransform.pivot = s_specialPivot;
Vector3 worldUIPos = RectTransformEx.ScreenToWorld(Vector2.zero, rootCanvas, uiCamera);
selfRectTransform.position = worldUIPos;
Vector2 localPos = selfRectTransform.localPosition;
float leftOffset = _rectOffset.left;
localPos.x += leftOffset;
float deltaX = _rectOffset.horizontal;
selfRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, sizeRect.width - deltaX);
float bottomOffset = _rectOffset.bottom;
localPos.y += bottomOffset;
float deltaY = _rectOffset.vertical;
selfRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, sizeRect.height - deltaY);
selfRectTransform.localPosition = localPos;
}

public Camera GetUICamera()
{
return m_uiCamera;
}

public Canvas GetRootCanvas()
{
return m_rootCanvas;
}

private void SetDirty()
{
if (!IsActive())
{
return;
}
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}

private void RebuildTracker()
{
m_tracker.Clear();
m_tracker.Add(this, selfRectTransform, DrivenTransformProperties.All);
}

protected override void OnEnable()
{
base.OnEnable();
RebuildTracker();
SetDirty();
}

protected override void OnDisable()
{
m_tracker.Clear();
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
base.OnDisable();
}

#if UNITY_EDITOR

[Header("Editor readonly debug stuff")]
[SerializeField]
private Vector2 _editorCurrentResolution;
[SerializeField]
private Vector2 _editorScreenSize;
[SerializeField]
private float _editorScreenFactor;
[SerializeField]
private float _editorScreenDpi;
[SerializeField]
private float _editorCanvasFactor;
[SerializeField]
private Rect _editorRootCanvasRect;

private void OnDrawGizmosSelected()
{
Color color = Color.blue;
UpdateEditorPreviewData();
Canvas rootCanvas = GetRootCanvas();
RectTransform rootRectTransform = rootCanvas.transform as RectTransform;
Rect rect = rootRectTransform.rect;
Vector2 bottomLeft = rootRectTransform.GetLocalRectPosition(RectPositionType.BottomLeft);
DrawBox(rootRectTransform, bottomLeft, new Vector2(rect.width, rect.height), color);
// draw raw rect

if (Mathf.Approximately(_rectOffset.horizontal, 0f) && Mathf.Approximately(_rectOffset.vertical, 0f))
{
return;
}
rect.width -= _rectOffset.horizontal;
rect.height -= _rectOffset.vertical;
bottomLeft.x += _rectOffset.left;
bottomLeft.y += _rectOffset.bottom;
DrawBox(rootRectTransform, bottomLeft, new Vector2(rect.width, rect.height), color);
}

private void DrawBox(RectTransform reference, Vector2 bottomLeftLocalPos, Vector2 size, Color color)
{
Color cachedColor = Gizmos.color;
Gizmos.color = color;
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos), reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x));
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos), reference.TransformPoint(bottomLeftLocalPos + Vector2.up * size.y));
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x), reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x + Vector2.up * size.y));
Gizmos.DrawLine(reference.TransformPoint(bottomLeftLocalPos + Vector2.up * size.y), reference.TransformPoint(bottomLeftLocalPos + Vector2.right * size.x + Vector2.up * size.y));
Gizmos.color = cachedColor;
}

private void UpdateEditorPreviewData()
{
_editorScreenSize = new Vector2(Screen.width, Screen.height);
_editorCurrentResolution = new Vector2(Screen.currentResolution.width, Screen.currentResolution.height);
_editorScreenFactor = Screen.currentResolution.width / Screen.width;
_editorScreenDpi = Screen.dpi;
Canvas rootCanvas = GetRootCanvas();
if (rootCanvas != null)
{
_editorCanvasFactor = rootCanvas.scaleFactor;
_editorRootCanvasRect = (rootCanvas.transform as RectTransform).rect;
}
}

#endif
}

public static class RectTransformEx
{
public static Vector3 GetLocalRectPosition(this RectTransform target, RectPositionType offsetType)
{
Vector3 result = Vector3.zero;
Vector2 targetSize = target.rect.size;
Vector2 targetPivot = target.pivot;

Vector2 pivotOffset = Vector2.one * 0.5f - targetPivot;
pivotOffset.x *= targetSize.x;
pivotOffset.y *= targetSize.y;

result += (Vector3)pivotOffset;

switch (offsetType)
{
case RectPositionType.Top:
result.y += targetSize.y * 0.5f;
break;
case RectPositionType.Bottom:
result.y -= targetSize.y * 0.5f;
break;
case RectPositionType.Left:
result.x -= targetSize.x * 0.5f;
break;
case RectPositionType.Right:
result.x += targetSize.x * 0.5f;
break;

case RectPositionType.TopLeft:
result.y += targetSize.y * 0.5f;
result.x -= targetSize.x * 0.5f;
break;
case RectPositionType.TopRight:
result.y += targetSize.y * 0.5f;
result.x += targetSize.x * 0.5f;
break;

case RectPositionType.BottomLeft:
result.y -= targetSize.y * 0.5f;
result.x -= targetSize.x * 0.5f;
break;

case RectPositionType.BottomRight:
result.y -= targetSize.y * 0.5f;
result.x += targetSize.x * 0.5f;
break;

default:
break;
}

return result;
}

public static Vector3 ScreenToWorld(Vector2 screenPoint, Canvas rootCanvas, Camera uiCamera)
{
switch (rootCanvas.renderMode)
{
case RenderMode.ScreenSpaceOverlay:
return screenPoint;
case RenderMode.ScreenSpaceCamera:
RectTransformUtility.ScreenPointToWorldPointInRectangle((RectTransform)rootCanvas.transform, screenPoint, uiCamera, out Vector3 worldPoint);
return worldPoint;
case RenderMode.WorldSpace:
break;
}
throw new System.NotImplementedException();
}

}

最开始做的时候我很粗心的直接把 Screen.SafeArea 的大小直接给应用到 RectTransform 上了。后来发现这样是不对的,Screen.SafeArea 获取到的是实际的物理尺寸的安全区。
后来我是算出物理尺寸和根Canvas尺寸的笔直,再去计算出实际的RectTransform大小。

基本上可以凑合使用了。还比较遗憾的一点是,我没有能够让其支持只适配四个方向中的任1或者任2方向。比较简单的解决方法就是让不同适配的RectTransform互相套一下。

UI同学在调整动效以及物体布局的时候,会用Unity中的Device Simulator尝试切换不同的机型来查看不同设备上的UI 预览。这个时候可能需要有一个编辑器的监听器去监听设备变更的事件,然后发起全部UI尺寸刷新的请求。
这样会更方便他们做预览。

项目里的UI框架有非常方便实用的数据绑定功能。各种UI表现件也可以根据数据集里的某个int改变UI表现。但是比较逆天的是我们经常把enum转int然后再到表现件上去调对应的各种状态。
对着magic number调配置确实很让人苦恼。

思路就是再需要根据int来调偶配置的component上添加一个编辑器里专用的Type对象,然后根据这个对象类型去画int。基于Odin做的,但我觉得这个思路应该还不错。

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

// use a string to temply store the type

[AttributeUsage(AttributeTargets.Field)]
public class EnumTypeQualifiedName: Attribute { }

[DrawerPriority(DrawerPriorityLevel.WrapperPriority)]
public class EnumTypeQualifiedNameAttributeDrawer : OdinAttributeDrawer<EnumTypeQualifiedName, string>
{
class EditingState
{
public static IEnumerable<Type> tempQuery;

public bool wannaSet = false;
public Type selectedType = null;
}

protected override void DrawPropertyLayout(GUIContent label)
{
string currentName = this.ValueEntry.SmartValue;
Type enumType = string.IsNullOrEmpty(currentName) ? null : Type.GetType(currentName);
string enumTypeNiceName = enumType == null ? "invalid!!!" : enumType.GetNiceName();

int controlId = GUIUtility.GetControlID(FocusType.Passive);
EditingState state = GUIUtility.GetStateObject(typeof(EditingState), controlId) as EditingState;
if (state.wannaSet)
{
currentName = state.selectedType.AssemblyQualifiedName;
state.selectedType = null;
state.wannaSet = false;
this.ValueEntry.SmartValue = currentName;
}
EditorGUILayout.BeginVertical();
GUIStyle guiStyle = new GUIStyle(EditorStyles.label);
guiStyle.alignment = TextAnchor.MiddleLeft;
EditorGUILayout.LabelField(this.Property.NiceName, guiStyle);

guiStyle = new GUIStyle(EditorStyles.popup);
guiStyle.alignment = TextAnchor.MiddleLeft;
if (EditorGUILayout.DropdownButton(new GUIContent($"Preview Enum Type ({enumTypeNiceName})"), FocusType.Keyboard, guiStyle))
{
if (EditingState.tempQuery == null)
{
EditingState.tempQuery = GetTargetEnumTypes();
}

TypeSelector selector = new TypeSelector(EditingState.tempQuery, false);
selector.EnableSingleClickToSelect();
selector.ShowInPopup();
selector.SelectionConfirmed += selection =>
{
var resultTypeArray = selection.ToArray();
if (resultTypeArray.Length > 0)
{
Type selectedType = resultTypeArray.GetValue(0) as Type;
EditingState state = GUIUtility.GetStateObject(typeof(EditingState), controlId) as EditingState;
state.selectedType = selectedType;
state.wannaSet = true;
}
};
}
EditorGUILayout.EndVertical();
}

private static IEnumerable<Type> GetTargetEnumTypes()
{
// TODO @Hiko here to get actual enum types
TypeCache.TypeCollection cache = TypeCache.GetTypesDerivedFrom<Enum>();
var query = cache.Where(t => t.IsEnum && t.IsPublic);
return query;
}

}

// put temply data into a sturct

[Serializable]
public struct IntAsEnumEditorPack
{
public bool showIntAsEnumForTaggedField;
[EnumTypeQualifiedName]
public string enumTypeQualifiedName;
}

public interface IIntAsEnumPackHolder
{
#if UNITY_EDITOR
IntAsEnumEditorPack holderData { get; }
#endif
}

// draw enum popup for int

[AttributeUsage(AttributeTargets.Field)]
public class IntAsEnumAttribute : Attribute { }

[DrawerPriority(DrawerPriorityLevel.WrapperPriority)]
public class IntAsEnumAttributeDrawer : OdinAttributeDrawer<IntAsEnumAttribute, int>
{
protected override void DrawPropertyLayout(GUIContent label)
{
int nextValue, prevValue = this.ValueEntry.SmartValue;
InspectorProperty containerProterty = this.ValueEntry.Property;
Type holderType = typeof(IIntAsEnumPackHolder);
Type containerType = containerProterty.ParentType;
bool doContinue = true;
while (doContinue)
{
Type parentType = containerProterty.ParentType;
if (parentType == null)
{
break;
}
containerType = parentType;
containerProterty = containerProterty.Parent;
if (holderType.IsAssignableFrom(containerType))
{
break;
}
}

GUIStyle guiStyle = new GUIStyle(EditorStyles.label);
guiStyle.alignment = TextAnchor.MiddleLeft;
if (containerType != null && holderType.IsAssignableFrom(containerType))
{
IIntAsEnumPackHolder holder = containerProterty.ValueEntry.WeakSmartValue as IIntAsEnumPackHolder;
IntAsEnumEditorPack packData = holder.holderData;
if (packData.showIntAsEnumForTaggedField)
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(this.Property.NiceName, guiStyle);
Type enumType = string.IsNullOrEmpty(packData.enumTypeQualifiedName) ? null : Type.GetType(packData.enumTypeQualifiedName);
if (enumType != null && enumType.IsEnum)
{
Enum prevEnumValue;
if (!enumType.IsEnumDefined(prevValue))
{
prevEnumValue = Enum.GetValues(enumType).GetValue(0) as Enum;
}
else
{
prevEnumValue = (Enum)Enum.ToObject(enumType, prevValue);
}
guiStyle = new GUIStyle(EditorStyles.popup);
guiStyle.stretchWidth = false;
guiStyle.alignment = TextAnchor.MiddleLeft;
Enum nextEnumValue = EditorGUILayout.EnumPopup(prevEnumValue, guiStyle);
nextValue = Convert.ToInt32(nextEnumValue);
this.ValueEntry.SmartValue = nextValue;
guiStyle = new GUIStyle(EditorStyles.label);
guiStyle.alignment = TextAnchor.MiddleLeft;
guiStyle.stretchWidth = false;
EditorGUILayout.LabelField($"int value: ({nextValue})", guiStyle);
}
EditorGUILayout.EndVertical();
return;
}

}
// can not find the target preview enum type, draw normal int field
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(this.Property.NiceName);
nextValue = EditorGUILayout.IntField(prevValue);
this.ValueEntry.SmartValue = nextValue;
EditorGUILayout.EndHorizontal();
}
}

// test case
public class TesterA : MonoBehaviour, IIntAsEnumPackHolder
{
#if UNITY_EDITOR
[SerializeField]
private IntAsEnumEditorPack intAsEnumEditorPack;
public IntAsEnumEditorPack holderData => intAsEnumEditorPack;
#endif

[Serializable]
public struct TempStructA
{
[IntAsEnum]
public int tempInt;
public Color tempColor;
}

public TempStructA[] tempArray;

[IntAsEnum]
public int tempIntA;
}

效果如下

用起来是方便了,但是也引入了一个问题。这个做法会需要序列化一这两个小小的字段到物体上。导致Prefab变肥了,但感觉打包的时候打出来的prefab里应该不会包含这俩字段,应该还凑合啦。

今天看了下Unity官方2016年发的一个关于移动端小优化的一些分享视频。其中有一个关于把enum作为Dictionary中的key来使用时的boxing优化。

这个用法在我之前的项目里用得还挺多吧。直接上图看IL code。

Dictionary的增删查操作其实都会有用comparer.Equals()来判断是否相等的情况。
构建Dictionary时如果不传入指定的comparer,他会自己赋予一个’默认的’。这个默认的显然不适那么的友好。

我们创建一个comparer,然后直接看使用comparer和不使用的两种不同情况下的IL code。

很明显使用我们自己的comparer可以免去装箱的消耗。

后来我又拿到Unity里跑了一下看profiler。尝试跑了查询和添加,差距确实有,但是从Profiler中看结果,200万次的操作下,GC是差不多的,时间上使用comparer快了1300多ms。
说实在的觉得是我的测试方法有问题。而且我跑在桌面端,这样测试也不严谨。