FishPlayer

一个喜欢摸鱼的废物

0%

之前做了一个自带简单 GridLayout 的循环滚动列表。但是基于继承的思路做的。导师指出了其不便指出,而且Unity其实是比较提倡大家使用组合模式的,所以我决定修改一下。

循环滚动

思路

和在网上能搜索到的大多数循环滚动列表一样,我有一个空的 Content,调整其尺寸来模拟列表的滚动,再根据滚动的当前位置在视口里把UI组件都摆放好。

我需要使用Unity自带的滚动视外,并且我还定义了简单的 ListElement, ListView, ScrollrectController 这三个类。

ListElement 就是实际需要被操作的列表组件,这个组件负责提供 RectTransform 和一个 index, 保证其能被我的 ScrollrectController 操作。

ListView 负责管理 ListElement,生成销魂都可以。同时还提供直接拿取所有 ListElement 的接口。

ScrollrectController 持有 ListView,并且根据用户传入的一个数据个数来进行循环滚动的管理。同时有事件告知每次的布局刷新的完成。

为了方便,我还做了一个比较简单的 GridLayout 给 ScrollrectController 控制。

至于视图内的最大可视条目数,老实说我感觉我没有特别好的方法去计算一个准确的值。所以就按着 (row + 2) (column 2) 来计算了,虽然有一点浪费内存,但在我当前设置的滚动速度下是够用的。

代码

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

public class TempListElementUI : MonoBehaviour
{
[Header("must have"), Tooltip("should inherit from ISetupable")]
[SerializeField]
Component m_dataReceiver;
[SerializeField, Tooltip("better to manual drag it in")]
RectTransform m_elementTransform;
[SerializeField, ReadOnly]
private int m_index = -1;
public int ElementIndex => m_index;
public RectTransform ElementRectTransform
{
get
{
if (m_elementTransform == null)
m_elementTransform = GetComponent<RectTransform>();
return m_elementTransform;
}
}

public void Setup<TData>(TData data)
{
SomeUtils.ISetup<Component, TData>(m_dataReceiver, data);
}

public void Show()
{
if (!this.gameObject.activeSelf)
this.gameObject.SetActive(true);
}

public void Hide()
{
if (this.gameObject.activeSelf)
this.gameObject.SetActive(false);
}

public void SetIndex(int index)
{
m_index = index;
}

private void Awake()
{
if (m_elementTransform == null)
m_elementTransform = this.transform as RectTransform;
}

#if UNITY_EDITOR
private void Reset()
{
m_elementTransform = this.transform as RectTransform;
}
#endif
}

public class TempListView : MonoBehaviour
{
#if UNITY_EDITOR
#endif
// check when prefab change
// [OnValueChanged("OnPrefabChanged")]
[SerializeField, Header("place holder, the listview should only contains one type of element")]
TempListElementUI m_elementPrefab;
RectTransform Container => transform as RectTransform;
[SerializeField, ReadOnly]
List<TempListElementUI> m_actualUsedComponents = new List<TempListElementUI>(0);

public int Count => m_actualUsedComponents.Count;
public TempListElementUI this[int index] => m_actualUsedComponents[index];
public IReadOnlyList<TempListElementUI> ElementList => m_actualUsedComponents;

public TempListElementUI Add()
{
TempListElementUI element = InternalAdd();
m_actualUsedComponents.Add(element);
element.Show();
return element;
}

public void Clear()
{
TempListElementUI element = null;
while (m_actualUsedComponents.Count > 0)
{
element = m_actualUsedComponents[m_actualUsedComponents.Count - 1];
m_actualUsedComponents.RemoveAt(m_actualUsedComponents.Count - 1);
InternalRemove(element);
}
}

public void Remove(TempListElementUI instance)
{
if (m_actualUsedComponents.Remove(instance))
{
instance.Hide();
}
else
{
Debug.LogError($"listview_{this.gameObject.name} does not contains {instance.ElementRectTransform.name}, remove failed");
}
}

public void RemoveAt(int index)
{
if (index < 0 || index >= m_actualUsedComponents.Count)
{
Debug.LogError($"{index} is invalid for SimpleListView", this.gameObject);
return;
}

TempListElementUI toRemove = m_actualUsedComponents[index];
m_actualUsedComponents.RemoveAt(index);
InternalRemove(toRemove);
}

public void InnerSwap(int indexA, int indexB)
{
if (indexA < 0 || indexA > m_actualUsedComponents.Count - 1 || indexB < 0 || indexB > m_actualUsedComponents.Count - 1)
{
return;
}

TempListElementUI temp = m_actualUsedComponents[indexA];
int transformIndexA = temp.ElementRectTransform.GetSiblingIndex();
int transformIndexB = m_actualUsedComponents[indexB].ElementRectTransform.GetSiblingIndex();
m_actualUsedComponents[indexA] = m_actualUsedComponents[indexB];
m_actualUsedComponents[indexB] = temp;
m_actualUsedComponents[indexA].ElementRectTransform.SetSiblingIndex(transformIndexA);
m_actualUsedComponents[indexB].ElementRectTransform.SetSiblingIndex(transformIndexB);
}

/// <summary>
///
/// </summary>
/// <returns>-1 means element is not in the list</returns>
public int IndexOf(TempListElementUI instance)
{
try
{
return m_actualUsedComponents.IndexOf(instance);
}
catch (ArgumentOutOfRangeException)
{
return -1;
}
}

protected virtual TempListElementUI InternalAdd()
{
// can apply object pool here
if (Application.isEditor && !Application.isPlaying)
return UnityEditor.PrefabUtility.InstantiatePrefab(m_elementPrefab, Container) as TempListElementUI;
return Instantiate(m_elementPrefab, Container);
}

protected virtual void InternalRemove(TempListElementUI element)
{
// can apply object pool here
if (element == null) return;
element.Hide();
if (Application.isEditor && !Application.isPlaying)
GameObject.DestroyImmediate(element.ElementRectTransform.gameObject);
else
GameObject.Destroy(element.ElementRectTransform.gameObject);
}

#if UNITY_EDITOR

private void OnTransformChildrenChanged()
{
if (Application.isEditor && !Application.isPlaying)
FindPrefabInstances();
}

private void OnPrefabChanged()
{
// remove pre objects
int amount = m_actualUsedComponents.Count;
for (int i = 0; i < m_actualUsedComponents.Count; i++)
{
GameObject.DestroyImmediate(m_actualUsedComponents[i].ElementRectTransform.gameObject);
}

if (m_elementPrefab != null)
{
for (int i = 0; i < amount; i++)
{
RectTransform rectTransform = (RectTransform)UnityEditor.PrefabUtility.InstantiatePrefab(m_elementPrefab, Container);
m_actualUsedComponents[i] = (rectTransform.GetComponent<TempListElementUI>());
}
}
else
{
m_actualUsedComponents.Clear();
}
}

/// <summary>
/// Retrieves prefab instances in the transform
/// </summary>
[ContextMenu("find prefab instances")]
private void FindPrefabInstances()
{
bool hasPrefab = !(m_elementPrefab == null);
TempListElementUI elementPrefab = m_elementPrefab.GetComponent<TempListElementUI>();
m_actualUsedComponents.Clear();
List<GameObject> toDeleteObjectList = new List<GameObject>();
foreach (Transform child in Container)
{
TempListElementUI childElement = child.GetComponent<TempListElementUI>();
if (childElement == null)
{
toDeleteObjectList.Add(child.gameObject);
continue;
}

if (hasPrefab)
{
GameObject detectPrefabGo = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(child.gameObject);
TempListElementUI detectPrefab = (detectPrefabGo == null) ? null : detectPrefabGo.GetComponent<TempListElementUI>();
if (elementPrefab == detectPrefab)
{
// same source prefab
m_actualUsedComponents.Add(childElement);
}
else
{
// different source prefab, delete this one
toDeleteObjectList.Add(child.gameObject);
}
}
else if (UnityEditor.PrefabUtility.IsAnyPrefabInstanceRoot(child.gameObject))
{
// find the first prefab
GameObject prefab = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(child.gameObject);
m_elementPrefab = prefab.GetComponent<TempListElementUI>();
m_actualUsedComponents.Add(childElement);
hasPrefab = true;
}
}

for (int i = 0; i < toDeleteObjectList.Count; i++)
{
if (Application.isPlaying)
GameObject.Destroy(toDeleteObjectList[i]);
else
GameObject.DestroyImmediate(toDeleteObjectList[i]);
}
}

[ContextMenu("editor time add")]
private void EditorTimeAdd()
{
if (Application.isPlaying) return;
if (m_elementPrefab == null)
{
Debug.LogError("listview is missing element prefab");
return;
}
TempListElementUI spawnObject = (TempListElementUI)UnityEditor.PrefabUtility.InstantiatePrefab(m_elementPrefab, Container);
m_actualUsedComponents.Add(spawnObject);
}

[ContextMenu("editor time clear")]
private void EditorTimeClear()
{
if (Application.isPlaying) return;
// remove pre objects
TempListElementUI[] preObjects = Container.GetComponentsInChildren<TempListElementUI>();
for (int i = 0; i < preObjects.Length; i++)
{
GameObject.DestroyImmediate(preObjects[i].ElementRectTransform.gameObject);
}
m_actualUsedComponents.Clear();
}

[ContextMenu("test print elements")]
private void TestPrintListElements()
{
if (m_actualUsedComponents != null)
{
StringBuilder printText = new StringBuilder($"temp list view children_{m_actualUsedComponents.Count} :\n");
for (int i = 0; i < m_actualUsedComponents.Count; i++)
{
printText.AppendLine(m_actualUsedComponents[i].ElementRectTransform.name);
}
Debug.Log(printText);
}
}

#endif
}

public class BoundlessScrollRectController : UIBehaviour
{
/*
too much code, I will just show the draw
*/

private void DrawContentItem()
{
IReadOnlyList<TempListElementUI> elementList = ElementList;
int dataCount = m_simulatedDataCount;
// TODO @Hiko use a general calculation later
bool test = m_content.anchorMin != Vector2.up || m_content.anchorMax != Vector2.up || m_content.pivot != Vector2.up;
if (test)
{
m_content.anchorMin = Vector2.up;
m_content.anchorMax = Vector2.up;
m_content.pivot = Vector2.up;
}
Vector3 dragContentAnchorPostion = m_content.anchoredPosition;
Vector3 contentMove = dragContentAnchorPostion - SomeUtils.GetOffsetLocalPosition(m_content, SomeUtils.UIOffsetType.TopLeft);
Vector2 itemSize = m_gridLayoutGroup.CellSize, spacing = m_gridLayoutGroup.Spacing;

RectOffset padding = null;
if (null != m_gridLayoutGroup)
padding = m_gridLayoutGroup.RectPadding;

// need to know the moving direction, then adjust it to prevent wrong draw
float xMove = contentMove.x < 0 ? (-contentMove.x - padding.horizontal) : 0;
xMove = Mathf.Clamp(xMove, 0.0f, Mathf.Abs(xMove));
float yMove = contentMove.y > 0 ? (contentMove.y - padding.vertical) : 0;
yMove = Mathf.Clamp(yMove, 0.0f, Mathf.Abs(yMove));

// the column index of the top left item
int tempColumnIndex = Mathf.FloorToInt((xMove + spacing.x) / (itemSize.x + spacing.x));
if (xMove % (itemSize.x + spacing.x) - itemSize.x > spacing.x)
tempColumnIndex = Mathf.Clamp(tempColumnIndex - 1, 0, tempColumnIndex);

// the row index of the top left item
int tempRowIndex = Mathf.FloorToInt((yMove + spacing.y) / (itemSize.y + spacing.y));
if (yMove % (itemSize.y + spacing.y) - itemSize.y > spacing.y)
tempRowIndex = Mathf.Clamp(tempRowIndex - 1, 0, tempRowIndex);

Vector2Int rowTopLeftItemIndex = new Vector2Int(tempRowIndex, tempColumnIndex);

int rowDataCount = 0, columnDataCount = 0;
if (BoundlessGridLayoutData.Constraint.FixedColumnCount == m_gridLayoutGroup.constraint)
{
rowDataCount = m_gridLayoutGroup.constraintCount;
columnDataCount = Mathf.CeilToInt((float)dataCount / rowDataCount);
}
else
{
columnDataCount = m_gridLayoutGroup.constraintCount;
rowDataCount = Mathf.CeilToInt((float)dataCount / columnDataCount);
}

// x -> element amount on horizontal axis
// y -> element amount on vertical axis
Vector2Int contentRowColumnSize = new Vector2Int(rowDataCount, columnDataCount);

// deal with content from left to right (simple case)
int dataIndex = 0, uiItemIndex = 0;
Vector3 rowTopLeftPosition = new Vector3(padding.left, -padding.top, 0.0f), itemTopLeftPosition = Vector3.zero;
for (int columnIndex = 0; columnIndex < m_viewItemCountInColumn; columnIndex++)
{
if (columnIndex + rowTopLeftItemIndex.x == columnDataCount)
break;

rowTopLeftPosition = new Vector3(padding.left, -padding.top, 0.0f) + Vector3.down * (columnIndex + rowTopLeftItemIndex.x) * (itemSize.y + spacing.y);
for (int rowIndex = 0; rowIndex < m_viewItemCountInRow; rowIndex++)
{
if (rowIndex + rowTopLeftItemIndex.y == rowDataCount)
break;

Vector2Int elementIndex = new Vector2Int(rowIndex + rowTopLeftItemIndex.y, columnIndex + rowTopLeftItemIndex.x);
dataIndex = CaculateDataIndex(elementIndex, contentRowColumnSize, GridLayoutData.startAxis, GridLayoutData.startCorner);
itemTopLeftPosition = rowTopLeftPosition + Vector3.right * (rowIndex + rowTopLeftItemIndex.y) * (itemSize.x + spacing.x);

// TODO @Hiko avoid overdraw
if (uiItemIndex > 0 && elementList[uiItemIndex - 1].ElementIndex == dataIndex)
continue; // over draw case
if (dataIndex > -1 && dataIndex < dataCount)
{
elementList[uiItemIndex].ElementRectTransform.localPosition = itemTopLeftPosition;
elementList[uiItemIndex].SetIndex(dataIndex);
elementList[uiItemIndex].Show();
uiItemIndex++;
}
}
}

while (uiItemIndex < elementList.Count)
{
elementList[uiItemIndex].SetIndex(-1);
elementList[uiItemIndex].Hide();
elementList[uiItemIndex].ElementRectTransform.position = Vector3.zero;
uiItemIndex++;
}

NotifyOnContentItemFinishDrawing();
}
}

这样写可以保证我的这个循环滚动的逻辑与实际的UI需要显示的逻辑尽可能地分开。需要使用循环滚动功能的时候,再添加这些组件即可。

配合实际显示组件使用

在滚动视图摆放好位置后,我们可以通过 ListElement 提供的索引知道其对应数据集里数据索引。所以需要使用这个滚动视图的UI,要同时管理 ListView 和 ScrollrectController。

我整了一个接口和一个扩展方法,用于显示数据的UI组件需要继承这个接口。因为按照我的写法,从 ListView 中只能拿到 ListElement 没法拿到实际用于表现的 UI 组件。但是通过这个接口和 ListElement 中的一个泛型方法就可以把内容传进去。虽然需要做一次类型检查,但也应该还好。毕竟不需要GetComponent也不需要单独维护一个关于实际显示组件的数组,用起来还是挺方便的呀。

除此之外比较坑的就是这样写导致 Unity 的 Component 都能调用这个Setup的方法,还挺危险的,所以加了个注释。实际使用的时候一定记得让用于表现的 UI 组件继承这个接口。

代码

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

// apply this on the target shows actual data
public interface ISetupable<TData>
{
void Setup(TData data);
}

/// <summary>
/// target component should inherit from ISetupable
/// </summary>
/// <param name="component"></param>
/// <param name="data"></param>
/// <typeparam name="TData"></typeparam>
public static void ISetup<TComponent, TData>(this Component component, TData data) where TComponent : Component
{
// I guess this check should be fine :(
if (component is ISetupable<TData> target)
{
target.Setup(data);
return;
}
UnityEngine.Debug.LogError($"component_{component.name} is not a setupable");
}

总结

其实这个功能丢尽项目里后用的地方不是特别多,所以都是隔几个月修一下,每次都要看自己写的垃圾代码好久,属实头痛了。
总算是用起来比以前方便不少了。接下来就把一些小问题修一修好了。

回头再看已经和自己做的那个初版完全不一样了,个人认为思路比以前那个好多了。

全部代码:
https://github.com/2C2C2C/MuyScrollRect

终于开始学习 Games101了。除了听课以外还想做他的作业多巩固一下。

但我实在是不想用虚拟机(菜,想调试),所以想整个windows这边的环境。
最开始按照一些教程配Cmake啥的,结果作业1编译就疯狂报错实在受不了。群友推荐了 vcpkg+vs 俺马上来试试。

vcpkg

https://github.com/microsoft/vcpkg/blob/master/README_zh_CN.md

安装并不难,找到了官方的简中手册,照着跑一轮就行。

安装作业依赖包

1
2
3
4
5
6
7

# install latest packages :D
.\vcpkg.exe install opencv:x64-windows
.\vcpkg.exe install eigen3:x64-windows

# install for all users
vcpkg integrate install

结果这个时候遇到了个天坑,eigen3这边的gitlab 仓库跑路了(不是)。

https://gitlab.com/libeigen/eigen/-/issues/2336

于是乎去暂存的仓库下载源码,然后放进 “vcpkg\downloads” 中,在执行之前的安装命令就好了

https://gitlab.com/cantonios/eigen/-/tree/3.3.9

作业框架设置

我是自己NEW了一个C++的空vs工程,把作业代码都丢进去。

目标平台选择x64

右键 项目 属性 -> c/c++ -> 语言 -> c++ 标准这边选择 c++17 (作业要求)

最近项目升级了2020.3,Graphic这边多给了一个Raycast Padding的设置。
我们有在用一些完全空白的图片来接受UI的点击响应,同时也有一些用图片接收点击的按钮。

但很坑爹的是,我发现无论怎么设置Raycast Padding的值,场景预览中都没有显示框框,所以我其实是完全不知道这玩意儿到底怎么用的。
搜索一下才知道原来是padding的数值是负数的时候才是往外扩张。

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

public class RaycastTarget : Graphic
{
// Additionally, If you need a Raycast target that is not a rectangle, you can implement bool Raycast(Vector2 sp, Camera eventCamera) method from Graphic.
public override void SetMaterialDirty() { return; }
public override void SetVerticesDirty() { return; }
/// Probably not necessary since the chain of calls `Rebuild()`->`UpdateGeometry()`->`DoMeshGeneration()`->`OnPopulateMesh()` won't happen; so here really just as a fail-safe.
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
return;
}

#if UNITY_EDITOR

private void OnDrawGizmos()
{
if (!this.enabled)
return
RectTransform rectTransform = this.transform as RectTransform;
Color wireColor = Color.yellow;
if (this.isActiveAndEnabled)
wireColor.a *= 0.7f
// Padding to be applied to the masking
// X = Left, Y = Bottom, Z = Right, W = Top
// if you wanna make it bigger, then the all value shouble be negative
Vector4 padding = this.raycastPadding * -1.0f;
Matrix4x4 localToWorld = rectTransform.localToWorldMatrix
Vector3 topLeft = SomeUtils.GetOffsetLocalPosition(rectTransform, SomeUtils.UIOffsetType.TopLeft);
Vector3 topRight = SomeUtils.GetOffsetLocalPosition(rectTransform, SomeUtils.UIOffsetType.TopRight);
Vector3 bottomLeft = SomeUtils.GetOffsetLocalPosition(rectTransform, SomeUtils.UIOffsetType.BottomLeft);
Vector3 bottomRight = SomeUtils.GetOffsetLocalPosition(rectTransform, SomeUtils.UIOffsetType.BottomRight)
topLeft = localToWorld.MultiplyPoint(topLeft + (Vector3.left * padding.x) + (Vector3.up * padding.w));
topRight = localToWorld.MultiplyPoint(topRight + (Vector3.right * padding.z) + (Vector3.up * padding.w));
bottomLeft = localToWorld.MultiplyPoint(bottomLeft + (Vector3.left * padding.x) + (Vector3.down * padding.y));
bottomRight = localToWorld.MultiplyPoint(bottomRight + (Vector3.right * padding.z) + (Vector3.down * padding.y))
Color tempColor = Gizmos.color;
Gizmos.color = wireColor
Gizmos.DrawLine(topLeft, topRight);
Gizmos.DrawLine(topLeft, bottomLeft);
Gizmos.DrawLine(bottomRight, topRight);
Gizmos.DrawLine(bottomRight, bottomLeft);
Gizmos.color = tempColor
}

#endif

}

空白raycast target的可以在Gizmo里画,但是Image本身的不是那么好画,因为无法直接覆写原本的Editor。
一个笨办法就是做一个自己的Iamge然后也在Gizmo里画。
但其实这个是Graphic的property 不知道怎么做通用解决方案。

问题出现

终于改来改去,俺做的这个循环滚动列表成功运用到了项目里的仓库UI上。但是还是被一个BUG给击垮了。

UX给到的设计稿是当玩家鼠标在其中一个物品上时,要有一个小弹窗显示这个物品的一些讯息。
BUG的表现是当我已经’选中’一个物品后,用鼠标滚轮滚到下一行,小弹窗显示的还是上一个物品的讯息。

BUG原因

我在UI脚本中写了关于鼠标移动进入和离开的处理,但其实当列表滚动时候,鼠标下面的物体还是上一个物体,但是物体内的数据已经改变,却没有刷新。

解决方案

其实也还算有个凑合的解决方案。导师之前重写了InputModule,所以我们决定在这边做改动。
当发生滚动时,我们需要重新做raycast。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void OnScrollWheel(InputAction.CallbackContext context)
{
if (IsActive())
{
PointerEventData eventData = GetPointerData();
eventData.button = PointerEventData.InputButton.Middle;
eventData.Reset();
eventData.scrollDelta = context.ReadValue<Vector2>()
GameObject testScrollObject = ExecuteEventExecuteHierarchy(eventData.pointerCurrentRaycasgameObject, eventData, ExecuteEvents.scrollHandler)
if (testScrollObject != null)
{
// to re-do the reycast cuz we may scroll a recyclscrollrect
HandlePointerExitAndEnter(eventData, null);
m_rayCastDelayFrame = SCROLL_RAYCAST_DELAY;
// SCROLL_RAYCAST_DELAY =3 or 5, depends....
}
}
}

我们先清楚了事件数据所认为的鼠标悬停的物体,然后过几帧再重新发射射线进行检测。
因为我的循环滚动列表依赖于Unity自带的滚动视图,并且其位置也是在LateUpdate中更新的,所以我们最好在下一帧或者接下来的几帧后再做射线检测。。

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

// will call in update
public override void Process()
{
/*
.....
*/
CheckAndDoDelayRaycast();
}

void CheckAndDoDelayRaycast()
{
if (m_rayCastDelayFrame < 0)
{
return;

if (--m_rayCastDelayFrame == 0)
{
var eventData = GetPointerData();
eventSystem.RaycastAll(eventData, m_RaycastResultCache);
var firstRaycast = FindFirstRaycast(m_RaycastResultCache)
eventData.pointerCurrentRaycast = firstRaycast;
m_RaycastResultCache.Clear()
var temp = eventData.pointerCurrentRaycast.gameObject;
HandlePointerExitAndEnter(eventData, temp);
m_rayCastDelayFrame = -1;
}
}

最近在拼UI,需要建立几个临时的场景在本地。自然不能上传,但是又想手贱 git add . 还是决定在本地手动设置忽略一些文件。

设置Git exclude

首先需要找到这个文件。

exclude 文件在 如图的目录中

1
\.git\info\

找到以后以文本文件的形式打开,用法和 gitignore 差不多,我还么有深入研究,但我差不多这样用。

把我需要忽略的本地目录填上去就可以了。
顺便一说,若使用这个方式忽略了某个目录,当把已经进行版本控制的文件丢进这个目录里时,Git中会生成一个delete的改动。

前几天打游戏太疯了,上班的时候脑子都不清醒,commit的时候输错了东西,push的时候被block了才发现。
结果还因为不会改commit消息,还错误地操作使得工作全没了。

修改当前最新的一条commit消息

1
2
3

# 修改当前最新的一条commit消息
git commit --amend

非常简单,一个命令就搞定了。然后就可以正常push了。

如果是需要修改已经推送到远端的commit,可以再这个命令之后尝试push —force。
不过我觉得这样的情景非常之少,都已经PUSH上去了为啥还需要改啊:(

修改往前第N条commit消息(还未推送到远端的)

如图,我需要修改图中选定的那一个commit。从我当前位置数,是第3个commit。
则X就应该是3。

1
2
3
4
5
6
7
8
9
10
11

git rebase -i HEAD~X
# X is the number of commits to go back
# Move to the line of your commit, change pick into edit,
# then change your commit message:
git commit --amend
# Finish the rebase with:
git rebase --continue

# :D
git push

执行rebase后,会让选择需要对哪条commit进行什么样的操作。

需要编辑消息则就把pick改成edit,然后保存,退出文本编辑。就可以使用 commit —amend 来修改消息了,然后再执行下一条命令完成rebase。
然后就是快乐push。

其实也可以同时edit多条commit。这样,在使用 commit —amend 时会从最老的commit开始编辑。完成一条的编辑就需要 rebase —continue。
之后才能继续编辑下一条。

如果修改了曾经push过的commit,就需要push —force。

JS脚本删除豆瓣广播

很久以前为了清理自己SPAM在微博的东西,找过批量删除微博的脚本。现在也想顺手清理一下豆瓣的,不过没找到比较好用的。
不过完全不会WEB开发,也只能自己拼凑一下。

直接上代码吧

代码

豆瓣有保护机制的,如果在短时间内疯狂发送请求,会在一定时间内被BAN IP(亲身经历)。
所以我们可以试着用随机等待的方式来避免这个问题。

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

// I steal this from somewhere
function sleepWait(waitMsec) {
var startMsec = new Date();
while (new Date() - startMsec < waitMsec);
}

function execTest() {
for (let step = 0; step < 10; step++) {
location.reload();
var waitDuration = (Math.floor(Math.random() * 10) + 1) * 10000;
console.log('test log, next exec in' + waitDuration + 'ms');
sleepWait(waitDuration);
}
}

删除的代码是我白嫖来的,在 我的广播 的页面里执行这个函数大概可以删除当前页面中的所有广播。
不过转发好像不一定能删除,之后还得找别的方法。

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

// I steal this from somewhere
function execDelTest() {
var a = function(c) { $.post_withck("/j/status/delete", { sid: c }) };

var b = function(c) { $.post_withck("/j/status/unreshare", { sid: c }) };

$("a[data-reshare-id]").each(function() {
b($(this).attr("data-reshare-id"));
$(this).hide()
});

$("div[data-sid]").each(function() {
a($(this).attr("data-sid"));
$(this).hide()
})
};

完整代码如下,不过真的怕被BAN IP 可以把随机等待的时间设定得长一些

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

// I steal this from somewhere
function sleepWait(waitMsec) {
var startMsec = new Date();
while (new Date() - startMsec < waitMsec);
}

// I steal this from somewhere
function execDelTest() {
var a = function(c) { $.post_withck("/j/status/delete", { sid: c }) };

var b = function(c) { $.post_withck("/j/status/unreshare", { sid: c }) };

$("a[data-reshare-id]").each(function() {
b($(this).attr("data-reshare-id"));
$(this).hide()
});

$("div[data-sid]").each(function() {
a($(this).attr("data-sid"));
$(this).hide()
})
};

function execDelLoop() {
for (let step = 0; step < 10; step++) {
location.reload();
var waitDuration = (Math.floor(Math.random() * 10) + 1) * 10000;
sleepWait(waitDuration);
}
}

// finally execute deletion :D
execDelLoop();

代码

感觉web这边可以做蛮多方便日常生活用的脚本,改天真的要开始学一学才行。

前言

上周把bug修得差不多了,这周稍微有点空闲时间了,就把以前想做的一些东西大概看了一下。
之前因为清理文件没有及时清除相关引用,结果项目跑起来的时候有各种沙雕报错。只能再一个个物体检查,好麻烦。
于是想试试能不能查到某个物体被其它哪些物体引用。

思路

好在 Unity提供了这个函数 AssetDatabase.GetDependencies() ,可以获得某个 asset 的依赖项。
AssetDatabase.GetDependencies()

假设我们需要知道物体 A 被哪些其他的物体引用。那就遍历所有物体的依赖项,把依赖项中包含物体 A 的物体筛选出来即可。

上代码

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

// to find the reference of the target unity object

public static class FindReferencesInProject
{
public static bool TryFindReferenceOfUobject(UnityEngine.Object uobject)
{
bool result = false;
string path = AssetDatabase.GetAssetPath(uobject);
if (AssetDatabase.IsValidFolder(path))
{
// dun do it for a folder :)
return false;
}

FindObjectReference(uobject);

return result;
}

// why the order is like this?
[MenuItem("Assets/Find References In Project")]
public static void FindSelectedObjectReferences()
{
if (null != Selection.activeObject)
{
string path = AssetDatabase.GetAssetPath(Selection.activeObject);
if (!AssetDatabase.IsValidFolder(path))
{
FindObjectReference(Selection.activeObject);
}
}
}

private static void FindObjectReference(UnityEngine.Object uobject)
{
System.Diagnostics.Stopwatch stopWatcher = new System.Diagnostics.Stopwatch();
stopWatcher.Start();

string targetPath = AssetDatabase.GetAssetPath(uobject);
List<string> referencesPaths = new List<string>();
Dictionary<string, List<string>> referenceCache = new Dictionary<string, List<string>>();
string[] assetGuids = AssetDatabase.FindAssets("");

for (int i = 0; i < assetGuids.Length; i++)
{
string assetPath = AssetDatabase.GUIDToAssetPath(assetGuids[i]);
string[] dependencies = AssetDatabase.GetDependencies(assetPath, false);

for (int j = 0; j < dependencies.Length; j++)
{
if (dependencies[j] == targetPath)
{
referencesPaths.Add(assetPath);
}
}
}

FindReferencesInProjectEditor.OpenWindow();
FindReferencesInProjectEditor window = FindReferencesInProjectEditor.GetWindow();
window.SetPaths(referencesPaths.ToArray());
window.SetTargetUobject(uobject);

stopWatcher.Stop();
Debug.Log($"search references for {uobject.name} cost {stopWatcher.ElapsedMilliseconds}ms");

referenceCache.Clear();
}
}
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

// a simple window to show the result, and also we can ping those result object :D

public class FindReferencesInProjectEditor : EditorWindow
{
private Rect m_viewportRect = default;
private Rect m_listRect = default;
private Rect m_bottomRect = default;

private float m_scrollPosition = default;
private string[] m_targetPaths = null;

private const float ELEMENT_HEIGHT = 20.0f;
private const float ELEMENT_ICON_SIZE = 20.0f;
private const float SCROLLER_WIDTH = 15.0f;

private bool m_hasGetGUIStyle = false;

private GUIStyle m_scrollerStyle = null;
private GUIStyle m_buttonElementStyle = null;

private UnityEngine.Object m_selectedUnityObject = null;
private System.Type m_uobjType = null;

public static void OpenWindow()
{
FindReferencesInProjectEditor window = GetWindow<FindReferencesInProjectEditor>();
Texture2D icon = EditorGUIUtility.Load("icons/UnityEditor.ConsoleWindow.png") as Texture2D;
window.titleContent = new GUIContent("reference find result", icon);
Vector2 minSize = new Vector2(500.0f, 300.0f);
window.minSize = minSize;
}

public static FindReferencesInProjectEditor GetWindow()
{
return GetWindow<FindReferencesInProjectEditor>();
}

public void SetPaths(in string[] paths)
{
m_targetPaths = paths;
}

public void SetTargetUobject(UnityEngine.Object uobject)
{
m_selectedUnityObject = uobject;
}

private void DrawContent()
{
// only use half space of this shit window
m_viewportRect = new Rect(0.0f, 0.0f, this.position.width - SCROLLER_WIDTH, this.position.height * 0.8f);
m_listRect = new Rect(0.0f, 0.0f, m_viewportRect.width, m_viewportRect.height);
int elementCount = m_targetPaths.Length;

if (elementCount < 1)
return;

GUI.BeginClip(m_listRect); // to clip the overflow stuff
int indexOffset = Mathf.FloorToInt(m_scrollPosition / ELEMENT_HEIGHT);
int showCount = Mathf.CeilToInt(m_listRect.height / ELEMENT_HEIGHT);
showCount = showCount > elementCount ? elementCount : showCount;
float startPosY = (indexOffset * ELEMENT_HEIGHT) - m_scrollPosition;

for (int i = 0; i < showCount; i++)
{
Rect elementRect = new Rect(0, 0 + startPosY + i * ELEMENT_HEIGHT, m_listRect.width, ELEMENT_HEIGHT);
DrawElement(elementRect, indexOffset + i);
}
GUI.EndClip();
}

private void DrawElement(Rect elementRect, int dataIndex)
{
if (dataIndex < m_targetPaths.Length && GUI.Button(elementRect, $"path {m_targetPaths[dataIndex]} ;", m_buttonElementStyle))
{
Debug.Log($"{m_targetPaths[dataIndex]}");
UnityEngine.Object targetObj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(m_targetPaths[dataIndex]);
EditorGUIUtility.PingObject(targetObj);
}
}

private void DrawScroller()
{
// draw scroller on the right correctly
int elementCount = m_targetPaths.Length;
float fullElementHeight = elementCount * ELEMENT_HEIGHT;

Rect scrollbarRect = new Rect(m_viewportRect.x + m_viewportRect.width, m_viewportRect.y, SCROLLER_WIDTH, m_viewportRect.height);
m_scrollPosition = Mathf.Max(0, GUI.VerticalScrollbar(scrollbarRect, m_scrollPosition, m_listRect.height, 0, Mathf.Max(fullElementHeight, m_listRect.height)));

int controlId = GUIUtility.GetControlID(FocusType.Passive);
float scrollSensitivity = ELEMENT_HEIGHT;
float maxScrollPos = (fullElementHeight > m_listRect.height) ? (fullElementHeight - m_listRect.height) : 0;

if (EventType.ScrollWheel == Event.current.GetTypeForControl(controlId))
{
m_scrollPosition = Mathf.Clamp(m_scrollPosition + Event.current.delta.y * scrollSensitivity, 0, maxScrollPos);
Event.current.Use();
}
}

private void GetGUIStyle()
{
// scroller
m_scrollerStyle = new GUIStyle(GUI.skin.verticalScrollbar);
m_scrollerStyle.stretchWidth = false;
m_scrollerStyle.fixedWidth = SCROLLER_WIDTH;

// button element
m_buttonElementStyle = new GUIStyle(GUI.skin.button);
m_buttonElementStyle.alignment = TextAnchor.MiddleLeft;

m_uobjType = typeof(UnityEngine.Object);
}

private void DrawBottom()
{
m_bottomRect = new Rect(0.0f, this.position.height * 0.8f, this.position.width, this.position.height * 0.2f);
Rect fieldRect = new Rect(10.0f, m_bottomRect.y + 10.0f, 100.0f, 20.0f);

if (null == m_uobjType)
m_uobjType = typeof(UnityEngine.Object);

m_selectedUnityObject = EditorGUI.ObjectField(fieldRect, m_selectedUnityObject, m_uobjType, true);

Rect buttonRect = new Rect(150.0f, m_bottomRect.y, 200.0f, 40.0f);
if (GUI.Button(buttonRect, "try find reference"))
{
FindObjectReference();
}

}

private void FindObjectReference()
{
if (null == m_selectedUnityObject)
{
return;
}

Clear();
FindReferencesInProject.TryFindReferenceOfUobject(m_selectedUnityObject);
}

private void Clear()
{
m_targetPaths = System.Array.Empty<string>();
}

private void OnGUI()
{
if (!m_hasGetGUIStyle)
{
GetGUIStyle();
m_hasGetGUIStyle = true;
}

DrawContent();
DrawScroller();
DrawBottom();
}

}

搜索结果我选择使用一个简单的 editor window 来显示。
同时还能填入新的目标物体再做一次搜索。

总结

最近都没有做编辑器这边的开发,感觉直接退回了起点。感觉Unity编辑器的文档没有想象中那么详细,很多时候反而没有问同事来得方便。可能该提高自己的英文水平了。

最近想着做一个音乐游戏的DEMO试试,但是毫无头绪不知道怎么开始。看了下邦邦的视频决定先把节拍器做出来,再用节拍器提供的事件去支撑起其它的部分。

因为完全没做过音乐游戏,而且也对游戏引擎以及声音方面没有什么了解,所以开始在网上海搜节拍器的代码并拿来测试。

最后得到俩解决方案:1.在Unity里写一个;2.使用专业的音频插件(如CRIWARE或者WWISE),使用里面的节拍器。
因为只是尝试做做DEMO,决定先不引入音频插件了。

在冲浪查询的时候发现Unity的官方文档中已经写好了一个!真是太棒了,马上嫖过来用。为了方便之后使用,我也决定把这份代码尽可能看懂。

下面是Unity的源码
MonoBehaviour.OnAudioFilterRead(float[], int)
AudioSettings.dspTime

提取基本代码

首先我大概跑了一下,看了一下,提取出了我认为是节拍器最基本的部分。

signatureHi / signatureLo => 这个就是我们所说的几几拍。
signatureHi是每小节的拍子数,signatureLo是指x分音符为1拍。accent是指当前小结内的拍子数。

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

[RequireComponent(typeof(AudioSource))]
public class MetronomeExample : MonoBehaviour
{
public double bpm = 128.0f;
public int signatureHi = 4;
public int signatureLo = 4;
private double nextTick = 0.0F;
private double sampleRate = 0.0F;
private int accent= 0;
private bool running = false;

public event System.Action<int, int> OnBeatTick;

private void Start()
{
accent = signatureHi;
sampleRate = AudioSettings.outputSampleRate;
nextTick = AudioSettings.dspTime * sampleRate;
running = true;
}

private void OnAudioFilterRead(float[] data, int channels)
{
if (!running)
return;

double samplesPerTick = sampleRate * (60.0f / bpm) * (4.0f / signatureLo);
double sample = AudioSettings.dspTime * sampleRate;
int dataLen = data.Length / channels;
int n = 0;
while (n < dataLen)
{
while (sample + n >= nextTick)
{
nextTick += samplesPerTick;
if (++accent > signatureHi)
{
accent = 1;
}
OnBeatTick?.Invoke(accent, signatureHi);
Debug.Log($"Tick: {accent} / {signatureHi}");
}
n++;
}
}
}
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
nextTick = AudioSettings.dspTime * sampleRate; 
```
从 Start() 开始,先做节拍器的初始化。 因为需要准确的音频数据,所以我们会使用音频相关的函数来操作,而不是使用 Update()。
通过当前的时间 * 当前的采样率,大概就能得到当前的音频信息总量。

``` CSharp
OnAudioFilterRead(float[] data, int channels)
```
接着我们可以把 OnAudioFilterRead() 看作 Update() 来使用。
channels 大概是声道数,比如我平时用的辣鸡机器是双声道(左右的),那么 channels 的值就是2
data[] 中存储的是当前这一次遍历的音频数据,也会包含不同声道的音频信息,比如\[0L, 1R, 2L, 3R....\]之类的。

``` CSharp
double samplesPerTick = sampleRate * (60.0f / bpm) * (4.0f / signatureLo);
double sample = AudioSettings.dspTime * sampleRate;
```
这里是用与计算拍子的间隔时间。(60.0f / bpm) 所计算的是对于 4/4 拍而言的拍子间隔时间。
而 (4.0f / signatureLo) 可以算出一个参数,并乘给 4/4 拍而言的拍子间隔时间。
sample 可以粗浅的认为是当前的时间。

``` CSharp
while (n < dataLen)
{
while (sample + n >= nextTick)
{
// 用起来就相当于
// nextTime = Time.time * timeInterval;
nextTick += samplesPerTick;
if (++accent > signatureHi)
{
accent = 1;
}
OnBeatTick?.Invoke(accent, signatureHi);
Debug.Log($"Tick: {accent} / {signatureHi}");
}
n++;
}

这边遍历一下接下来会播放的音频数据。根据数据的长度来判断是否会需要发出节拍器的 tick。

节拍器音效

在官方源码中还用到了一些其他的变量,并给data赋值,其实这些就是为了播放节拍器音效而存在的。

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

while (n<dataLen)
{
float x = gain * amp * Mathf.Sin(phase);
int i = 0;
while (i < channels)
{
data[n * channels + i] += x;
i++;
}
while (sample + n >= nextTick)
{
nextTick += samplesPerTick;
amp = 1.0F;
if (++accent > signatureHi)
{
accent = 1;
amp *= 2.0F;
}
OnBeatTick?.Invoke(accent, signatureHi);
Debug.Log($"Tick: {accent} / {signatureHi}");
}
phase += amp* 0.3F;
amp *= 0.993F;
n++;
}

摸了

感觉自己的知识面又窄又浅,还是需要多看书才行。

前言

之前在做CloneConsole的时候,会在每次绘制的时候尝试绘制所有的log,然后再交给UnityEditor自己去处理显示的逻辑。因为我们会堆积几百甚至几千条log,这样的做法无疑会带来性能问题,结果就是console卡得不能用。

思路

其实思路也很简单,只需要画那些会显示出来的item就行了。
在这之前需要算出哪些item是可以被显示出来的

  1. 算出所有的item加起来有多高
  2. 用当前滑条的位置来推算出当前需要绘制的第一条item的索引
  3. 绘制item
  4. 绘制滑条并计算其下一次的位置

其实这个思路应该也能应用到UGUI上,下次我也要试试。

上代码

在一个新窗口中尝试画一画就好。
首先要准备好用来测试的一些数据。

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

namespace TempDraw
{
[System.Serializable]
public struct TempDrawData
{
public int IconTag;
public string TempMessage;
}

public class TempDrawWindowTester : MonoBehaviour
{
private TempDrawWindowTester _instance = null;
public TempDrawWindowTester Instance => _instance;

List<TempDrawData> m_data = null;

// used to give out the data
public static event System.Action<List<TempDrawData>> OnDataSpread;
}
}

把测试用的数据都存在这个Mono里,再用它的静态事件把数据传递到窗口中。
代码里的 Button attribute 是用来在editor上创建按钮的。

测试用的数据准备好了,我们来来进行绘制。

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

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace TempDraw
{
public class TempDrawWindow : EditorWindow
{
public static string WINDOW_NAME = "Temp Draw Window";
private static TempDrawWindow _instance = null;
public static TempDrawWindow Instance => _instance;

private List<TempDrawData> m_data = null;
private float m_scrollPosition = 0.0f;

#region temp element
private const float ELEMENT_HEIGHT = 20.0f;
private const float ELEMENT_ICON_SIZE = 20.0f;

#endregion

#region icons
private Texture2D m_infoIconSmall = null;
private Texture2D m_warningIconSmall = null;
#endregion

[MenuItem("Window/Temp Draw Window")]
private static void OpenWindow()
{
TempDrawWindow window = GetWindow<TempDrawWindow>();
Texture2D icon = EditorGUIUtility.Load("icons/UnityEditor.ConsoleWindow.png") as Texture2D;
window.titleContent = new GUIContent(WINDOW_NAME, icon);
}

public void SetData(List<TempDrawData> data)
{
if (null == m_data)
m_data = new List<TempDrawData>();
else
m_data.Clear();

m_data.AddRange(data);
}

private void DrawTempElement(Rect elementRect, int dataIndex)
{
Rect iconRect = new Rect(elementRect.x, elementRect.y, ELEMENT_ICON_SIZE, elementRect.height);
if (m_data[dataIndex].IconTag == 0)
GUI.Label(iconRect, m_infoIconSmall);
else
GUI.Label(iconRect, m_warningIconSmall);

Rect labelRect = new Rect(elementRect.x + ELEMENT_ICON_SIZE, elementRect.y, elementRect.width - ELEMENT_ICON_SIZE, elementRect.height);
GUI.Label(labelRect, $"Tag: {m_data[dataIndex].IconTag} ; Message: {m_data[dataIndex].TempMessage} ;");
}

private void DrawTempRect()
{
// only use half space of this shit window
Rect viewportRect = new Rect(0.0f, 0.0f, this.position.width, this.position.height * 0.5f);

float scrollbarWidth = GUI.skin.verticalScrollbar.fixedWidth;
Rect scrollbarRect = new Rect(viewportRect.x + viewportRect.width - scrollbarWidth, viewportRect.y, scrollbarWidth, viewportRect.height);
Rect currentRect = new Rect(0.0f, 0.0f, viewportRect.width - scrollbarWidth, viewportRect.height);
float viewportHeight = viewportRect.height;
int elementCount = m_data.Count;

GUI.BeginClip(currentRect); // to clip the overflow stuff
int indexOffset = Mathf.FloorToInt(m_scrollPosition / ELEMENT_HEIGHT);
int showCount = Mathf.CeilToInt(currentRect.height / ELEMENT_HEIGHT);
showCount = showCount > elementCount ? elementCount : showCount;
float startPosY = (indexOffset * ELEMENT_HEIGHT) - m_scrollPosition;

for (int i = 0; i < showCount; i++)
{
Rect elementRect = new Rect(0, 0 + startPosY + i * ELEMENT_HEIGHT, currentRect.width, ELEMENT_HEIGHT);
DrawTempElement(elementRect, indexOffset + i);
}
GUI.EndClip();

// do stuff for scroller
float fullElementHeight = elementCount * ELEMENT_HEIGHT;
m_scrollPosition = Mathf.Max(0, GUI.VerticalScrollbar(scrollbarRect, m_scrollPosition, currentRect.height, 0, Mathf.Max(fullElementHeight, currentRect.height)));

int controlId = GUIUtility.GetControlID(FocusType.Passive);
float scrollSensitivity = ELEMENT_HEIGHT;
float maxScrollPos = (fullElementHeight > currentRect.height) ? (fullElementHeight - currentRect.height) : 0;

if (EventType.ScrollWheel == Event.current.GetTypeForControl(controlId))
{
m_scrollPosition = Mathf.Clamp(m_scrollPosition + Event.current.delta.y * scrollSensitivity, 0, maxScrollPos);
Event.current.Use();
}

}

private void GetAsset()
{
m_infoIconSmall = EditorGUIUtility.Load("icons/console.infoicon.sml.png") as Texture2D;
m_warningIconSmall = EditorGUIUtility.Load("icons/console.warnicon.sml.png") as Texture2D;
}

#region life cycle

private void Awake()
{
_instance = this;
m_data = new List<TempDrawData>();
}

private void OnEnable()
{
TempDrawWindowTester.OnDataSpread -= SetData;
TempDrawWindowTester.OnDataSpread += SetData;
GetAsset();
}

private void OnGUI()
{
DrawTempRect();
}

private void OnDestroy()
{
TempDrawWindowTester.OnDataSpread -= SetData;
_instance = null;
}

#endregion

}
}

效果如图