FishPlayer

一个喜欢摸鱼的废物

0%

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。
说实在的觉得是我的测试方法有问题。而且我跑在桌面端,这样测试也不严谨。

在实际制作UI的时候,为了完成一些排版设计,我们会经常用到Unity的布局(Layout)组件。甚至有些复杂的排版,我会用嵌套布局来实现。
UI美术人经常希望在这些布局上加上一些进场出场动画(甚至包括spacing变动),但布局组件上的数值是没法在动画中修改的,怎么办呢?

方法一

在布局中添加一些空物体或是在布局外面套空物体。然后再使用Tween动画来完成UI美术们想要的那些进出场动画。
美术人不太喜欢的方法,因为有那么一点点不太方便和Animation直接一起预览,但如果整体进出场动画不依赖UnityAnimation且不是很复杂,这种方法完全够用。

方法二

用通过动画控制脚本上的可序列化的数值,再传递到布局组件上。
个人感觉这个方法也还不错,还能支持预览,UI美术更偏向于这一个解决方法。

但是在动画窗口里里看到的curve是锯齿状的,很奇怪。之后还需要看下如何才能像其他属性一样拉正常的曲线。

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

using UnityEngine;
using UnityEngine.UI;

[ExecuteAlways]
public class LayoutGroupTempAnimationParam : MonoBehaviour
{
[SerializeField]
private LayoutGroup _target;

[SerializeField]
private int _left;
[SerializeField]
private int _right;
[SerializeField]
private int _top;
[SerializeField]
private int _bottom;

[Header("GridLayout must use GridSpacing")]
[SerializeField]
private bool _alsoControlSpacing;
[SerializeField]
private int _normalSpacing = 0;
[SerializeField]
private Vector2 _gridSpacing = Vector2.zero;

private RectOffset m_targetPadding;

public void OnDidApplyAnimationProperties()
{
if (_target == null)
{
return;
}
ApplyPadding();
if (_alsoControlSpacing)
{
ApplySpacing();
}
// need to notify refresh
RectTransform rectTransform = this.transform as RectTransform;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}

private void ApplyPadding()
{
m_targetPadding = _target.padding;
if (m_targetPadding != null)
{
m_targetPadding.left = _left;
m_targetPadding.right = _right;
m_targetPadding.top = _top;
m_targetPadding.bottom = _bottom;
}
}

private void ApplySpacing()
{
if (_target is HorizontalOrVerticalLayoutGroup layoutGroup)
{
layoutGroup.spacing = _normalSpacing;
}
else if (_target is GridLayoutGroup gridLayoutGroup)
{
gridLayoutGroup.spacing = _gridSpacing;
}
}

#if UNITY_EDITOR

private void OnValidate()
{
ApplyPadding();
if (_alsoControlSpacing)
{
ApplySpacing();
}
if (!Application.isPlaying)
{
// need to notify refresh
RectTransform rectTransform = this.transform as RectTransform;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
}

private void Reset()
{
_target = this.GetComponent<LayoutGroup>();
m_targetPadding = _target.padding;
}

#endif
}

项目引入Wwise以后,相信蛮多人都和我一样,平时不想听游戏声音就想听歌捶码修BUG的。但是Unity原本的mute按钮没法直接直接对Wwise那边做操作。
一开始不知道是搜索引擎的使用姿势不对还是咋的,查了好久都没查出来。导师研究了一下弄了一个版,大家用了感觉都很不错,决定分享出来。

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

public static class EditorWwiseManager
{
static bool m_isPaused = false;

static EditorWwiseManager()
{
EditorApplication.update += OnUpdate;
EditorApplication.pauseStateChanged += OnPauseStateChanged;
}

private static void OnPauseStateChanged(PauseState state)
{
switch (state)
{
case PauseState.Paused:
m_isPaused = true;
break;
case PauseState.Unpaused:
m_isPaused = false;
break;
}
}

static void OnUpdate()
{
if (EditorUtility.audioMasterMute && !m_isPaused)
{
AkSoundEngine.Suspend(true);
m_isPaused = true;
}
else if (!EditorUtility.audioMasterMute && m_isPaused)
{
AkSoundEngine.WakeupFromSuspend();
m_isPaused = false;
}
}
}

这样就可以通过Editor SceneView上的mute按钮把Wwise静音,超级方便凹。

现在依旧是在项目里负责一些UI业务的编写。前段时间需要需要给游戏中的 popup提示 做一些简单的重构,正好发现了我一直以来都误解的一个小点,决定记下来。

业务的要求就是做那种会弹出来一会儿再消失的提示,比如道具获取之类的UI。

我的做法就是用一个list去存着当前的提示,并 tick 检查它们是否结束,然后移除已经结束的相关数据。
大概的用法如下。

但其实啊,这个做法在 tick 是非常不好的。同事很快地通过 JetBrainsRider 查看 IL Code ,并告诉我,直接这样传入会每次都 new obj 。有比较大的消耗。
所以在这种需要经常 tick 并且条件比较固定的情况下。我们可以创建好 predicate 然后直接重复使用(可以存在成员变量里,这里演示就直接存在本地变量里了)。

最后附上测试代码和结果 测试 count 为 100000

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

public class PredicateTempTest
{
public struct TempPack
{
public int tempValue;
private static Random s_random = new Random(DateTime.Now.Millisecond);
public static TempPack CreateTempPack()
{
TempPack pack = default;
if (s_random == null)
{
s_random = new Random(DateTime.Now.Millisecond);
}
pack.tempValue = s_random.Next(0, 10);
return pack;
}
}

private static List<List<TempPack>> s_tempListContainer = new List<List<TempPack>>(10000);

public static void DoTest(int tempCount)
{
FillTestData(tempCount);
DoTestLambda();
DoTestPassMethod();
DoTestPassExistPredicate();
}

private static void FillTestData(int tempCount)
{
s_tempListContainer.Clear();
s_tempListContainer.Capacity = tempCount;
for (int i = 0; i < tempCount; i++)
{
List<TempPack> tempList = new List<TempPack>(tempCount);
s_tempListContainer.Add(tempList);
for (int j = 0; j < tempCount; j++)
{
tempList.Add(TempPack.CreateTempPack());
}
}
}

private static void DoTestLambda()
{
Console.WriteLine("");
Console.WriteLine("TestLambda start");
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
for (int i = 0, containerSize = s_tempListContainer.Count; i < containerSize; i++)
{
List<TempPack> tempList = s_tempListContainer[i];
tempList.Find((existPack) => existPack.tempValue % 5 == 0);
}
stopWatch.Stop();
Console.WriteLine($"TestLambda end, result_{stopWatch.ElapsedMilliseconds}ms");
}

private static void DoTestPassMethod()
{
Console.WriteLine("");
Console.WriteLine("PassMethod start");
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
for (int i = 0, containerSize = s_tempListContainer.Count; i < containerSize; i++)
{
List<TempPack> tempList = s_tempListContainer[i];
tempList.Find(CheckPack);
}
stopWatch.Stop();
Console.WriteLine($"PassMethod end, result_{stopWatch.ElapsedMilliseconds}ms");
}

private static void DoTestPassExistPredicate()
{
Console.WriteLine("");
Console.WriteLine("PassExistPredicate start");
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
Predicate<TempPack> predicate = new Predicate<TempPack>(CheckPack);
for (int i = 0, containerSize = s_tempListContainer.Count; i < containerSize; i++)
{
List<TempPack> tempList = s_tempListContainer[i];
tempList.Find(predicate);
}
stopWatch.Stop();
Console.WriteLine($"PassExistPredicate end, result_{stopWatch.ElapsedMilliseconds}ms");
}

private static bool CheckPack(TempPack pack)
{
return pack.tempValue % 5 == 0;
}

}

最近在遇到了一个需求,要点击某个道具UI之后,显示一个小的关于该道具的信息框。
大概是入下图这个样子的。

还好之前做过,于是乎直接修修补补腾过去。至于为什么要修补,是因为在之前的项目里虽然写了一些和RectTransform相关的一些utils,但都因为懒和菜,没有去维护。有重复的方法还有错误的方法,麻了。
这次说是修补,也稍微重写了一些和RectTransform相关的一些utils,也准备带进项目里看看有没有地方可以用的。

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

using UnityEngine;
using UnityEngine.UI;

namespace Temp
{
public enum RectPositionType
{
Center = 0,

Top = 1,
Bottom = 2,
Left = 3,
Right = 4,

TopLeft = 5,
TopRight = 6,

BottomLeft = 7,
BottomRight = 8,
}

public class UIElementPositionSetter : MonoBehaviour
{
[Header("Target setting")]
[SerializeField]
private RectPositionType _targetRectPosition = RectPositionType.Center;
[SerializeField]
private Vector2 _targetPivot = Vector2.zero;
[SerializeField]
private Vector2 _extraOffset = Vector2.zero;
[SerializeField]
private RectOffset _adaptionOffset;
[SerializeField]
private bool _doScreenAdaption;

[Header("Other setting")]
[SerializeField]
private RectTransform _defaultBoundsCanvasTransform;
[SerializeField]
private bool _adjustHorizontal = true;
[SerializeField]
private bool _adjustVertical = true;

public void SetPosition(RectTransform target)
{
RectTransform self = transform as RectTransform;
Vector3 spawnPos = GetLocalRectPosition(self, _targetRectPosition);
spawnPos += new Vector3(_extraOffset.x, _extraOffset.y, 0f);
spawnPos = self.localToWorldMatrix.MultiplyPoint(spawnPos);
target.pivot = _targetPivot;
target.position = spawnPos;
if (_doScreenAdaption)
{
// HACK @Hiko force rebuild to make sure the size is correct
LayoutRebuilder.ForceRebuildLayoutImmediate(target);
DoAdaption(target);
}
}

public void DoAdaption(RectTransform targetTransform)
{
if (_adjustHorizontal)
{
AdjustHorizontal(targetTransform, _defaultBoundsCanvasTransform);
}
if (_adjustVertical)
{
AdjustVertical(targetTransform, _defaultBoundsCanvasTransform);
}
}

public void AdjustHorizontal(RectTransform target, RectTransform outterBoundRectTransform)
{
Vector2 targetSize = target.rect.size;
Matrix4x4 targetLocalToWorld = target.localToWorldMatrix;
Matrix4x4 boundRectWorldToLocal = outterBoundRectTransform.worldToLocalMatrix;
Vector2 topLeftPos = GetLocalRectPosition(target, RectPositionType.TopLeft);

Vector3 checkWorldPoint = targetLocalToWorld.MultiplyPoint(topLeftPos + Vector2.down * targetSize.y * 0.5f + Vector2.left * _adaptionOffset.left);
Vector2 leftCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 leftCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Left);
bool leftOut = leftCheckLocalPoint.x < leftCheckBoundPoint.x;

checkWorldPoint = targetLocalToWorld.MultiplyPoint(topLeftPos + Vector2.down * targetSize.y * 0.5f + Vector2.right * targetSize.x + Vector2.right * _adaptionOffset.right);
Vector2 rightCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 rightCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Right);
bool rightOut = rightCheckLocalPoint.x > rightCheckBoundPoint.x;

if (leftOut && rightOut)
{
// outter bound is too small
return;
}

if (leftOut)
{
float xDelta = leftCheckLocalPoint.x - leftCheckBoundPoint.x;
Vector3 tempPos = target.localPosition;
tempPos.x -= xDelta;
target.localPosition = tempPos;
}
if (rightOut)
{
float xDelta = rightCheckLocalPoint.x - rightCheckBoundPoint.x;
Vector3 tempPos = target.localPosition;
tempPos.x -= xDelta;
target.localPosition = tempPos;
}
if (leftOut || rightOut)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(target);
}
}

public void AdjustVertical(RectTransform target, RectTransform outterBoundRectTransform)
{
Vector2 targetSize = target.rect.size;
Matrix4x4 targetLocalToWorld = target.localToWorldMatrix;
Matrix4x4 boundRectWorldToLocal = outterBoundRectTransform.worldToLocalMatrix;
Vector2 topLeftPos = GetLocalRectPosition(target, RectPositionType.TopLeft);

Vector3 checkWorldPoint = targetLocalToWorld.MultiplyPoint(topLeftPos + Vector2.right * targetSize.x * 0.5f + Vector2.up * _adaptionOffset.top);
Vector2 topCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 topCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Top);
bool topOut = topCheckLocalPoint.y > topCheckBoundPoint.y;

checkWorldPoint = targetLocalToWorld.MultiplyPoint(topLeftPos + Vector2.down * targetSize.y + Vector2.right * targetSize.x * 0.5f + Vector2.down * _adaptionOffset.bottom);
Vector2 bottomCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 bottomCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Bottom);
bool bottomOut = bottomCheckLocalPoint.y < bottomCheckBoundPoint.y;

if (topOut && bottomOut)
{
// outter bound is too small
return;
}

if (topOut)
{
float yDelta = topCheckLocalPoint.y - topCheckBoundPoint.y;
Vector3 tempPos = target.localPosition;
tempPos.y -= yDelta;
target.localPosition = tempPos;
}
if (bottomOut)
{
float yDelta = bottomCheckLocalPoint.y - bottomCheckBoundPoint.y;
Vector3 tempPos = target.localPosition;
tempPos.y -= yDelta;
target.localPosition = tempPos;
}
if (topOut || bottomOut)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(target);
}
}

#region position util

public static Vector2 GetLocalRectPosition(RectTransform target, RectPositionType offsetType)
{
return GetLocalRectPosition(target, offsetType, Vector2.zero);
}

public static Vector2 GetLocalRectPosition(RectTransform target, RectPositionType offsetType, Vector2 offset)
{
return GetLocalRectPosition(target.rect.size, target.pivot, offsetType, offset);
}

public static Vector2 GetLocalRectPosition(Vector2 targetSize, Vector2 targetPivot, RectPositionType offsetType, Vector2 offset)
{
Vector2 bottomLeft = Vector2.zero;
bottomLeft.x -= targetPivot.x * targetSize.x;
bottomLeft.y -= targetPivot.y * targetSize.y;
Vector2 result = bottomLeft;
switch (offsetType)
{
case RectPositionType.Top:
result += Vector2.right * 0.5f * targetSize.x + Vector2.up * targetSize.y;
break;
case RectPositionType.Bottom:
result += Vector2.right * 0.5f * targetSize.x;
break;
case RectPositionType.Left:
result += Vector2.up * 0.5f * targetSize.y;
break;
case RectPositionType.Right:
result += Vector2.right * targetSize.x + Vector2.up * 0.5f * targetSize.y;
break;
case RectPositionType.TopLeft:
result += Vector2.up * targetSize.y;
break;
case RectPositionType.TopRight:
result += Vector2.right * targetSize.x + Vector2.up * targetSize.y;
break;
case RectPositionType.BottomRight:
result += Vector2.right * targetSize.x;
break;
case RectPositionType.BottomLeft:
default:
break;
}
return result + offset;
}

public static Vector3 RectPositionToWorld(RectTransform rect, RectPositionType offsetType)
{
return RectPositionToWorld(rect, offsetType, Vector2.zero);
}

public static Vector3 RectPositionToWorld(RectTransform rect, RectPositionType offsetType, Vector2 offset)
{
var localPosition = GetLocalRectPosition(rect, offsetType, offset);
return rect.TransformPoint(localPosition);
}

private Vector2 GetTargetRectPositionInLocalReference(RectPositionType offsetType)
{
RectTransform self = transform as RectTransform;
Vector2 targetPivotPos = GetLocalRectPosition(self, _targetRectPosition, _extraOffset);
Vector2 virtualCenterPos = targetPivotPos;
virtualCenterPos += GetLocalRectPosition(_targetSize, _targetPivot, offsetType, Vector2.zero);
return virtualCenterPos;
}

#endregion

#if UNITY_EDITOR

[Header("Preview setting")]
[SerializeField]
private Vector2 _targetSize = Vector2.one * 128f;
[SerializeField]
private Color _previewColor = Color.white;
[SerializeField]
private bool _doDrawAdaptionOffsetRect = false;

[System.Serializable]
private struct TempCheckResult
{
public bool previewLeftOut;
public bool previewRightOut;
public bool previewTopOut;
public bool previewBottomOut;
}

[SerializeField]
private TempCheckResult _checkResult;

private bool _previewLeftOut;
private bool _previewRightOut;
private bool _previewTopOut;
private bool _previewBottomOut;

private void OnDrawGizmosSelected()
{
// draw A box to show the position of target
RectTransform self = transform as RectTransform;
Vector2 localSpawnPos = GetLocalRectPosition(self, _targetRectPosition);
localSpawnPos += _extraOffset;

Vector2 pivot = _targetPivot;
Vector2 topLeftPos = localSpawnPos;
topLeftPos.x -= pivot.x * _targetSize.x;
topLeftPos.y -= (pivot.y - 1f) * _targetSize.y;

if (_doScreenAdaption)
{
// do adaption
if (_defaultBoundsCanvasTransform != null) // if there is no bound, we can take the screen as the bound
{
Matrix4x4 localToWorld = self.localToWorldMatrix;
RectTransform outterBoundRectTransform = _defaultBoundsCanvasTransform;
Matrix4x4 boundRectWorldToLocal = outterBoundRectTransform.worldToLocalMatrix;

Vector3 checkWorldPoint = localToWorld.MultiplyPoint(topLeftPos + Vector2.down * _targetSize.y * 0.5f + Vector2.left * _adaptionOffset.left);
Vector2 leftCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 leftCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Left);
_previewLeftOut = leftCheckLocalPoint.x < leftCheckBoundPoint.x;

checkWorldPoint = localToWorld.MultiplyPoint(topLeftPos + Vector2.down * _targetSize.y * 0.5f + Vector2.right * _targetSize.x + Vector2.right * _adaptionOffset.right);
Vector2 rightCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 rightCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Right);
_previewRightOut = rightCheckLocalPoint.x > rightCheckBoundPoint.x;

checkWorldPoint = localToWorld.MultiplyPoint(topLeftPos + Vector2.right * _targetSize.x * 0.5f + Vector2.up * _adaptionOffset.top);
Vector2 topCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 topCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Top);
_previewTopOut = topCheckLocalPoint.y > topCheckBoundPoint.y;

checkWorldPoint = localToWorld.MultiplyPoint(topLeftPos + Vector2.down * _targetSize.y + Vector2.right * _targetSize.x * 0.5f + Vector2.down * _adaptionOffset.bottom);
Vector2 bottomCheckLocalPoint = boundRectWorldToLocal.MultiplyPoint(checkWorldPoint);
Vector2 bottomCheckBoundPoint = GetLocalRectPosition(outterBoundRectTransform, RectPositionType.Bottom);
_previewBottomOut = bottomCheckLocalPoint.y < bottomCheckBoundPoint.y;

if (_adjustHorizontal)
{
if (_previewLeftOut && _previewRightOut)
{
// bound too small
}
else
{
if (_previewLeftOut)
{
float xDelta = leftCheckLocalPoint.x - leftCheckBoundPoint.x;
Vector3 tempPos = topLeftPos;
tempPos.x -= xDelta;
topLeftPos = tempPos;
}
if (_previewRightOut)
{
float xDelta = rightCheckLocalPoint.x - rightCheckBoundPoint.x;
Vector3 tempPos = topLeftPos;
tempPos.x -= xDelta;
topLeftPos = tempPos;
}
}
}

if (_adjustVertical)
{
if (_previewTopOut && _previewBottomOut)
{
// bound too small
}
else
{
if (_previewTopOut)
{
float yDelta = topCheckLocalPoint.y - topCheckBoundPoint.y;
Vector3 tempPos = topLeftPos;
tempPos.y -= yDelta;
topLeftPos = tempPos;
}
if (_previewBottomOut)
{
float yDelta = bottomCheckLocalPoint.y - bottomCheckBoundPoint.y;
Vector3 tempPos = topLeftPos;
tempPos.y -= yDelta;
topLeftPos = tempPos;
}
}
}
}
}

_checkResult.previewLeftOut = _previewLeftOut;
_checkResult.previewRightOut = _previewRightOut;
_checkResult.previewTopOut = _previewTopOut;
_checkResult.previewBottomOut = _previewBottomOut;

if (_doDrawAdaptionOffsetRect && _defaultBoundsCanvasTransform != null)
{
Vector2 innerBoundLeftPos = GetLocalRectPosition(_defaultBoundsCanvasTransform, RectPositionType.TopLeft, new Vector2(_adaptionOffset.left, -_adaptionOffset.top));
Vector2 size = _defaultBoundsCanvasTransform.rect.size;
size.x -= _adaptionOffset.horizontal;
size.y -= _adaptionOffset.vertical;
DrawBox(_defaultBoundsCanvasTransform, innerBoundLeftPos, size, Color.blue);
}
DrawBox(self, topLeftPos, _targetSize, _previewColor);
}

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

#endif

}
}

还是像以前一样,先在debug draw里把功能完成再给移到实际的物体上。
但这次还是很可惜没有把功能很好抽出成比较单独的数学运算,而且感觉要用到的参数好多也没法拆分,最后留了一个多参的入口。

When I searching some articles about MVC and MVVM, I see this talk.
Basiclly they does same stuff as my current project, but we still have some differences on our implementation.
I’d like to share it and take some notes :D

Here is the talk abour UGUI implementation.
video.
ppt.
temp.

Main note

Architecture in < Lost Survivor >

  • Artist works directly in Unity (Creating prefabs, setup aniamtions; well that’s what I my current project does, so I guess they did the same)
  • Dev works on GameLogic and UILogic
  • Dev/Art work decoupled
  • Dev/Art work parallel (This happens a lot in iteration)

The Pattern used in < Lost Survivor >

MVCVM (they choose this)

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
                                                                                                                               
+---------------------------------------+
| data binding |
| <-> |
| |
| |
+------------------------------+ | |
| | +----------------------|-------+ +------------------------------+ +-----------------------+
----------Controller------------ | Model | | View Model | | View (UGUI View) |
| | -------------------------------- -------------------------------- -------------------------
| void GainXP(); | | int XP; | | | | Text XP; |
| void LevelUp(); | | int Level; | | | | Text Attribute; |
| | | | | | | Button ReAllocate; |
| | +--------------/---------------+ +---------------\--------------+ | int WeaponATK; |
+---------------\--------------+ /- -\ | |
\ /- -\ +-----------/-----------+
-\ /- \ |
\ /- -\ /
+---------------------------------------------------------------------------------------------------|-------+
| |
| Message Bus |
+-----------------------------------------------------|-----------------------------------------------------+
|
|
|
+-------------|------------+
| |
| server backend |
| |
+--------------------------+

This is the pattern they use for their project.
The one thing that I cant understand is that the View will receive events from MessageBus. IDK why they did like this. For me, I prefer to let view only receive direct events from VM, bind to many stuff to MessageBus will make it slow.

MVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                                                                                                                                                                                                   
+------------------------------+
| Controller |
+---------------------------------------------------------------------------------------------+
| user action -> | void GainXp(); | update -> |
| | Void LevelUp(); | |
| | int GetAttribute(type); | |
| | | |
| | | |
| +-------|--------------|-------+ |
| | | |
|------------------------------+ | | +----------------------|-------+
| Character View | | | | Model |
-------------------------------- | | --------------------------------
| Text XP; | | | | int AmountXp; |
| Text Attribute; ------------+ | | int AttributeTypeA; |
| Button ReAllocate; | <- update | | int AttributeTypeB; |
| | +------------- |
| | <- notify change? | |
+------------------------------+ +------------------------------+

I’m sure this pattern is kind of useful. I use this a lot when I did the first version of some UI pages. But later the Controller become a giant class.
And also I think the View and Controller in this pattern is not very reusable.

MVVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                                                                                                                            
+------------------------------+
| Model Character |
+-----------------------------------------------
| | int AmountXp; |
| | int AttributeTypeA; |
| | int AttributeTypeB; |
| | |
| | |
| +------------------------------+
+------------------------------+ +----------------------|-------+
| Character View | data binding <-> | View Model |
-------------------------------- notifications <- --------------------------------
| Text XP; | command -> |int AmountXP; |
| Text Attribute; ----------------------+int WeaponATK; |
| Button ReAllocate; | |int WeaponLevel; | +------------------------------+
| int WeaponATK; | | | | Model Weapon |
| | | | --------------------------------
+------------------------------+ +----------------------|-------+ | int ATK; |
| | int Level; |
+---------------- |
+------------------------------+
  • ViewModel serves the View
  • One ViewModel per View
  • Based on Data Binding

After a few iterations, I find this pattern is also a good solutions. And I feel that we can even use CompositePattern in ViewModel to create big ViewModel for some big views(resuable).

In my current project, for the UI-View, we create some basic view that can receive similar types of data. And the good thing from this pattern is that the VM can send notificaiton to View to update, then I dun need to have lots of update method from Controller to update different data (cuz some data update need aniamtion, I cant just update all).

Problems

Previously it says that separating UIPrefabs creation and UILogic code may solve “Not my problem attitude”. But actually not.
In production state, everyone is busy, and we are iterating View-Scripts and VM-Scripts together. Sometimes it is diffcult to tell that is code issues or setup issues. Unless we have solid View-Scripts.

Summary

This talk has lots of similiar content as the DivisionUI talk. And I also realise that the game development has lots of unknow shit. It’s very difficult to have some resuable components that always works. Even the thougts are similar, the implementation can be different. A solution can be that having some solid, simple, basic and resuable compoents.