FishPlayer

一个喜欢摸鱼的废物

0%

在实际制作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.

之前我用Unity自带的API去查询一些物体的引用,结果当然是超超超超超级慢,对于那些把项目工程放在辣鸡机械盘上的同事,这个工具根本是没法好好用的。而且因为要用Unity的APi,自然是没办法使用async的。

最近导师拿到QA那边提供的一个类似的工具,改了一下,做了一个超级快的版本。

思路

思路其实是非常简单的。我们知道Unity Asset会有一个唯一的GUID,那我们就可以用这个GUID去查询其引用。
查询的时候我们也不需要使用Unity API去检查引用,而是把我们的各种Asset都当成文本文件直接读取,然后直接在这些文本里去匹配我们需要查询的GUID。

代码

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

// Analyze asset as text file

public interface IFileAnalysis<T>
{
public int FileCount { get; }
public int Scanned { get; }
public Task<T> Task { get; }
public float PercentCompletion { get; }
public string CurrentOperationMessage { get; }
public bool Running { get; }
public void Cancel();
}

public static class FileAnalyzer
{
public class FileAnalysis<T> : IFileAnalysis<T>
{
CancellationTokenSource m_tokenSource = new CancellationTokenSource
public Task<T> Task { get; set; }
public float PercentCompletion { get; set; }
public string CurrentOperationMessage { get; set; }
public int FileCount { get; set; }
public int Scanned { get; set; }
public bool Running => Task.Status switch
{
TaskStatus.RanToCompletion => false,
TaskStatus.Faulted => false,
TaskStatus.Canceled => false,
_ => true
};
public CancellationToken Token => m_tokenSource.Token
public void Cancel()
{
m_tokenSource.Cancel();
}
}

public static IFileAnalysis<string[]> AnalyzeFolders(string[] folders, System.Predicate<string> fileFilter, Regex content)
{
FileAnalysis<string[]> operation = new FileAnalysis<string[]>();
operation.Task = Task.Factory.StartNew<string[]>(() => CheckFolders(folders, fileFilter, content, operation), TaskCreationOptions.LongRunning);
return operation;
}

private static string[] CheckFolders(string[] folders, System.Predicate<string> fileFilter, Regex fileContent, FileAnalysis<string[]> operationStatus)
{
string[] files = folders.SelectMany(folder => Directory.GetFiles(folder, "*", SearchOption.AllDirectories)).ToArray();
List<string> result = new List<string>(200);
int count = files.Length;
operationStatus.FileCount = count;
operationStatus.Scanned = 0;
operationStatus.PercentCompletion = 0f;
for (int i = 0; i < count; i++)
{
operationStatus.Token.ThrowIfCancellationRequested();
string filePath = files[i];
if (fileFilter(filePath))
{
operationStatus.CurrentOperationMessage = filePath;
if (CheckFile(filePath, fileContent))
{
result.Add(filePath);
}
operationStatus.Scanned++;
operationStatus.PercentCompletion = (i / (float)count) * 100f;
}
}
return result.ToArray();
}

private static bool CheckFile(string filepath, Regex fileContent)
{
string assetText = File.ReadAllText(filepath);
MatchCollection matches = fileContent.Matches(assetText);
bool result = matches.Count > 0;
return result;
}
}

// Editor window

public class AssetUsageSearchWindow : EditorWindow
{
[Flags]
private enum AssetFilter
{
Asset = 1 << 0,
Material = 1 << 1,
Prefab = 1 << 2,
Scene = 1 << 3
}

[Serializable]
private struct SearchRequest
{
public bool searchTextOnly;
public string targetString
public UnityObject Asset
{
get
{
string path = AssetPath;
if (string.IsNullOrEmpty(path))
{
return null;
}
return AssetDatabase.LoadAssetAtPath<UnityObject>(path);
}
set
{
if (null != value && AssetDatabase.TryGetGUIDAndLocalFileIdentifier<UnityObject>(value, out string guid, out _))
{
targetString = guid;
}
}
}

public string AssetPath => searchTextOnly ? string.Empty : AssetDatabase.GUIDToAssetPath(targetString
public long FileID
{
get
{
UnityObject asset = Asset;
if (null != asset && AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out _, out long fileId))
{
return fileId;
}
return 0;
}
}
}

[System.Serializable]
private struct SearchResult
{
public string path;
public UnityObject asset;
/// <summary>
/// used to ping the actual asset dat has the direct reference
/// </summary>
public UnityObject subAssetIfNeed;
}

/// <summary>
/// only search specify text from asset
/// </summary>
private bool m_searchTextOnly = false;
private AssetFilter m_filter = (AssetFilter)~0; // Everything by default
private List<string> m_searchFolders = new List<string>();
private SearchRequest m_search;

private IFileAnalysis<string[]> m_findTask = null;
private bool m_waitForResult = false;
private SearchResult[] m_findResults = null;
private Regex m_pathFilterRegex;

private Vector2 m_scrollPos;
private Stopwatch m_processTime;

private bool IsBusy => null != m_findTask && m_findTask.Running;

[MenuItem("Muy Tools/Asset Usage Search", priority = 4)]
private static void Init()
{
AssetUsageSearchWindow window = CreateWindow<AssetUsageSearchWindow>();
window.Show();
window.titleContent = new GUIContent("Asset Usage Search");
}

private void OnGUI()
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
// switch search mode
m_searchTextOnly = EditorGUILayout.Toggle("Search Text Only", m_searchTextOnly
// draw search info
string searchLable = m_searchTextOnly ? "Search for string (Text)" : "Search for Asset (GUID)";
SearchRequest request = m_search;
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PrefixLabel(searchLable);
if (m_searchTextOnly)
{
request.targetString = EditorGUILayout.TextField(request.targetString);
}
else
{
GUI.enabled = false;
request.targetString = EditorGUILayout.TextField(request.targetString);
GUI.enabled = true;
request.Asset = EditorGUILayout.ObjectField(request.Asset, typeof(UnityObject), false);
using (new EditorGUI.DisabledGroupScope(true))
{
EditorGUILayout.TextField("FileID: ", request.FileID.ToString());
}
}
}
m_search = request
m_filter = (AssetFilter)EditorGUILayout.EnumFlagsField("Search Filter", m_filter);
EditorGUILayout.LabelField("Search folders:");
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
for (int i = 0; i < m_searchFolders.Count; i++)
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("X", EditorStyles.miniButton, GUILayout.Width(26)))
{
m_searchFolders.RemoveAt(i);
i--;
continue;
}
string folderResult = DrawSelectFolder(m_searchFolders[i]);
m_searchFolders[i] = string.IsNullOrEmpty(folderResult) ? m_searchFolders[i] : folderResult;
}

if (GUILayout.Button("Add Folder"))
{
m_searchFolders.Add(Application.dataPath);
}
}
}

// search button
using (new EditorGUI.DisabledGroupScope(IsBusy))
{
if (GUILayout.Button($"Search References for {m_search.targetString}"))
{
if (!string.IsNullOrEmpty(m_search.targetString))
{
DoFind();
}
}

bool isSearching = null != m_findTask && m_findTask.Task.Status == TaskStatus.Running;
bool isFaulted = null != m_findTask && m_findTask.Task.Status == TaskStatus.Faulted
if (isSearching) SearchingGUI();
else if (isFaulted) EditorGUILayout.HelpBox($"Error while running search:\n{m_findTask.Task.Exception}", MessageType.Error);
else if (m_findResults != null) SearchResultGUI();
}

private string DrawSelectFolder(string folderPath)
{
using (new EditorGUILayout.HorizontalScope())
{
GUI.enabled = false;
EditorGUILayout.TextField(folderPath);
GUI.enabled = true;
if (GUILayout.Button("Select"))
{
folderPath = EditorUtility.OpenFolderPanel("Select search folder", Application.dataPath, "");
}
}
return folderPath;
}

private void DoFind()
{
m_findResults = null;
m_waitForResult = true;
m_processTime = Stopwatch.StartNew();
RefreshPathFilter();
m_findTask = FileAnalyzer.AnalyzeFolders(m_searchFolders.Count > 0 ? m_searchFolders.ToArray() : new string[] { Application.dataPath }, FilterPath, new Regex(m_search.targetString));
m_findTask.Task.ContinueWith(task =>
{
if (task.Status == TaskStatus.RanToCompletion)
{
string[] result = task.Result;
}
});
}

private bool FilterPath(string path)
{
if (null == m_pathFilterRegex)
{
return false;
}
return m_pathFilterRegex.IsMatch(path);
}

private void SearchingGUI()
{
EditorGUILayout.LabelField($"Searching: {m_findTask.PercentCompletion}%");
EditorGUILayout.LabelField(m_findTask.CurrentOperationMessage);
if (GUILayout.Button("Cancel"))
{
m_findTask.Cancel();
m_findTask = null;
}

private void SearchResultGUI()
{
EditorGUILayout.LabelField($"Found {m_findResults.Length} assets:");
if (null != m_processTime)
{
m_processTime.Stop();
EditorGUILayout.LabelField($"Search time {m_processTime.ElapsedMilliseconds}ms, {m_findTask.Scanned}/{m_findTask.FileCount} files scanned");
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Copy list to clipboard", EditorStyles.miniButton))
{
GUIUtility.systemCopyBuffer = string.Join("\n", m_findResults.Select(rst => rst.path));
}
GUILayout.FlexibleSpace();
}
// since we dun have tons of result, we dun need to apply recycle scroll view here
using (EditorGUILayout.ScrollViewScope scrollview = new EditorGUILayout.ScrollViewScope(m_scrollPos, EditorStyles.helpBox))
{
m_scrollPos = scrollview.scrollPosition;
using (new EditorGUILayout.VerticalScope())
{
for (int i = 0; i < m_findResults.Length; i++)
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
{
SearchResult result = m_findResults[i];
Rect foldoutRect = GUILayoutUtility.GetRect(20, EditorGUIUtility.singleLineHeight, GUILayout.Width(20));
EditorGUILayout.ObjectField(result.asset, typeof(UnityObject), false);
EditorGUILayout.LabelField(new GUIContent(result.path, result.path));
m_findResults[i] = result;
EditorGUILayout.EndFoldoutHeaderGroup();
}
}
}
}
}

private void Update()
{
if (null != m_findTask)
{
if (m_findTask.Task.Status == TaskStatus.Running)
{
Repaint();
}
else if (m_findTask.Task.Status == TaskStatus.RanToCompletion && m_waitForResult)
{
// Result filling is done here because it needs to run on main thread
m_waitForResult = false;
string[] result = m_findTask.Task.Result;
int count = result.Length;
m_findResults = new SearchResult[count];
for (int i = 0; i < count; i++)
{
string assetPath = $"Assets{result[i].Substring(Application.dataPath.Length)}";
UnityObject asset = AssetDatabase.LoadAssetAtPath<UnityObject>(assetPath);
m_findResults[i] = new SearchResult
{
path = result[i],
asset = asset,
};
}
Repaint();
}
}
}

private void RefreshPathFilter()
{
string extensionFilter = GetExtensionFilter(m_filter);
m_pathFilterRegex = new Regex(string.Format(@"^.*\.({0})$", extensionFilter));
}

private string GetExtensionFilter(AssetFilter filter)
{
List<string> extensions = new List<string>();
if (filter.HasFlag(AssetFilter.Asset))
{
extensions.Add("asset");
}
if (filter.HasFlag(AssetFilter.Material))
{
extensions.Add("mat");
}
if (filter.HasFlag(AssetFilter.Prefab))
{
extensions.Add("prefab");
}
if (filter.HasFlag(AssetFilter.Scene))
{
extensions.Add("unity");
}
string extensionFilter = string.Join("|", extensions);
return extensionFilter;
}
}

关键代码和Unity API毫无关系,可以避免长时间的卡死,而且我们也能加入取消搜索的功能之类的。如果在乎性能的话可以还可以在读文件检查这边做优化。不过感觉这样已经比之前快了许多了。

之前看了一些关于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。

Well just ignore dat weird tag. :D

Here is muy notes dat I learn from the Divsion UI talk.

The key is iteration, threading and data.

Iterate your stuff to make it better.

Threading and data driven make the work efficient.

video.

Snow drop UI

  • No canvas
  • Immediate Mode
    • Constant cost (maybe it’s more statble?).
  • Vector Graphics
    • Shapes(scaling friendly, dun rely on resolution) over textures masks.
    • Save GPU load by only rendering the pixels gonna be used.
    • Artist can play with it.

Game Data & Node graph

  • Lots of data getter (get pure data?) (in graph node), used in UI and also game play.

  • Auto conversion to save some performance.

  • custom UI logic components.

  • custom reusable compounds.

Iteration

For coder

Reduce complie time

  • Blob builds(idk what is this).
  • Header file reduction.
  • Keep refactoring code to make it efficient.
  • Remove nolonger used code.

Reduce startup time and Load time

  • Only load needed stuff.
  • UI preview with live data for easy iteration.
  • Well some debug UI will also help :).

(for my unity project)

  • Have a UI preview scene (UI only, we can preview states and transitions).
  • Have simple a gym dat provide gameplay with enough content.

Coder to Artist

  • Coder work with artist (react fast to feedbacks).

  • Resable compounds, arist can directly use.

Drawback of iteration

  • Hard to stop.
  • Set clear goals and deadlines. (do not waste time)
  • Greate for prototyping, risk for delivering. (need a lead to shut down iteration, make the decision)

Building blocks of UI

To let arist and designers to have full(or more) control?

Widget and Graphics

Widget define space, accept input. Graphic improve widget, relative size, also accept input?

5 Basic widgets with a few variations.

Use basic widgets, and use them to build functionalities.

  • Window

    • 2D and 3D (anchoring functionalities, pixels or percentage; prepare some options for different auto scaling and apply to different UI widgets/graphic and situations).
    • Vertical Stack (stack the (children)widgets)
    • Layering
  • Text

    • JIT font generaion (wtf, keep memory down)
    • Fich text formatting lib to build customize text(icon, color …).
    • Auto clip (no idea how to do it)
  • Image

    • Use color/gradient more. (looks good with vector graphic)
    • Texture and loose image(ikd this, no border image?) into sprite sheet.
  • Stack container

    • For sorting elements.
    • Spacing and padding (how to do it with stack)
    • Invert for quick right to left language fixes (wtf)
  • Scroll Box

    • Fixed size(viewport size) with a virtual inner space(content).

7 Graphics (nodes)

  • Text with effects.
  • Images(basic) with more shapes, flat or curved.
  • Lines(strait, curved list?, 3d)
  • Points, single point or large squares(for effects?)
  • Shapes
  • Sector (why? for their art style?)
  • Custom graphics

Simply Ui workflow

  • Complex and powerful (lots of resuable compounds, and threading stuff)
  • Documentation and examples(I guess examples are better and save time)

For coder

  • Simple code; Less code, less bugs
  • Simple Tools; Do not overthink. Simple base, complex behaviour.

For UI artist/designer

  • Communicate to avoid merge conflicts, get ahead of it (cuz yaml merge wont help u all the time).

最近在做红点功能,path是用string挂在UI物体上的。UI同学那边想要一个按钮可以切换path在inspector是可编辑或不可编辑的状态。
最开始毫无思路,因为本身path是一个string,我自然不可以它身上记录这个编辑与否的状态。后来想着能不能把这个状态存在path的protperty drawer上。问了下导师,可行,但是没法直接存。

思路

创建一个object存储这个状态,这个object存入editor的state object里。
在Property drawer进行绘制的时候获取/更新这个object。

代码

代码很简单

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

public class UITagDrawer : PropertyDrawer
{
// the object for string state
class EditingState
{
public bool IsEditing = false;
}

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
/***/

// get state object
EditingState state = GUIUtility.GetStateObject(typeof(EditingState), GUIUtility.GetControlID(FocusType.Passive)) as EditingState;

using (new EditorGUILayout.HorizontalScope(style))
{
Rect editButtonRect = new Rect(position.x, position.y, 16f, 16f);
if (GUI.Button(editButtonRect, "E"))
{
// well since it's a class we dun need to set it back :>
state.IsEditing = !state.IsEditing;
}

if (GUILayout.Button("X", GUILayout.Width(18)))
{
state.IsEditing = !state.IsEditing;
}
if (state.IsEditing)
{
// show text field
tagStr = EditorGUILayout.TextField(tagStr);
}
else
{
// show lable
EditorGUILayout.LabelField(tagStr);
}
}

}
}