FishPlayer

一个喜欢摸鱼的废物

0%

最近有在做手游。各个厂商手机屏幕的尺寸和比例都各不相同。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.

之前看了一些关于RectTransform的文章,但是最终记忆都不是很深刻。而且要用的时候重新找还挺麻烦的。
干脆把别人的笔记抄来一些好了,以后也方便就着自己的笔记和UXUI同学解释。

Pivot

中心点,是UI元素旋转/缩放的中心点。使用归一化Vector2表示。

Anchor

其实是由两个点组成的(AnchorMin, AnchorMax)。并使用归一化Vector2来表示。
数值代表了在父类X轴和Y轴方向的百分比。

绝对布局

当anchorMax与anchorMin相等时,Anchor呈现为一个点,称之为锚点
在使用锚点的情况下,anchoredPosition是元素Pivot到Anchor的距离

此时会有4个重要的属性。

  • PosX, posY : 中心点到锚点的参数,实际像素值
  • Width, Height : UI 元素的尺寸

绝对布局的情况下无论分辨率是多少,父物体多大,该UI元素的大小是恒定的。

相对布局

当anchorMax与anchorMin不相等时,Anchor呈现为一个框,称之为锚框
在使用锚框的情况下,anchoredPosition是元素Pivot到锚框中心点的距离

这种情况下UI元素的四个角,距离四个对应的锚点的距离是不变的,在这种情况下RectTransform的属性又变为了Left,Top,Right,Bottom。

  • Left,Top,Right,Bottom : 四个点的数值分别是(Left,Top,Right,Bottom)锚点到实际的rect的这4个位置的点的距离。

SizeDelta

OffsetMin/OffsetMax

min是实际UI原素相对于AnchorMin的偏移,另外一个不言而喻.

sizeDelta就是offsetMax - offsetMin的值。

所以这个属性之所以叫做sizeDelta,是因为在锚点情况下其表征的是size(大小),在锚框的情况下其表征的是Delta(UI元素实际的属性值与锚框的差值)

Rect

rect中的属性,不与UI元素所在的位置有关,只和其自身属性相关。根据rect中提供的width和height可以得到UI元素实际的尺寸大小。

rect.position指的是以Pivot为原点,UI元素左下角的坐标。(right,up 为正方向)

参考资料

https://zhuanlan.zhihu.com/p/194317677
https://blog.csdn.net/jmu201521121014/article/details/105725175

顺便推荐一下在线画图小工具,不方便贴图的时候用这个还挺不错的。
https://asciiflow.com/#/
https://textik.com/

这周赶版本,接到了一个前同事留下来的bug。是说UI上的教程视频一直不播放。
我们的视频资源是在 StreamingAssets 下的,用path进行加载。
播放相关的代码如下。

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

void OnEnable()
{
m_player.prepareCompleted += OnPrepareCompleted;
}

void OnPrepareCompleted(VideoPlayer source)
{
Debug.Log("OnPrepareCompleted");
m_player.Play();
}

public void SetVideo(string path)
{
m_player.source = VideoSource.Url;
// I guess this process may not be finished now.
m_player.url = path;
m_player.Prepare();
}

查了一下,发现在调用了 SetVideo 之后,OnPrepareCompleted没有被调用,意思是 VideoPlayer的prepareCompleted事件没有触发。
我猜发生这个情况的原因是,当我们使用url来设置视频资源的那一帧,VideoPlayer还没能获取到视频资源的一些属性,或是上一个视频资源还在占用着当前的播放器,所以这个时候如果调用PrePare,其实是没有用的。

所以我使用的Hacky Fix是在设置完资源的URL之后,等一帧再调用Prepare,然后等待 prepareCompleted 事件。

顺便在网上查了一下,也有些人遇到类似的问题,说是赋值URL之后不知道视频资源什么时候才能正确播放。
论坛里有个人给出的解决方法是去检查 VideoPlayer.isPrepared 的这个属性。但赋值URL之后这个值并不会在某一时刻变成true。