FishPlayer

一个喜欢摸鱼的废物

0%

以前弄过一个在项目中查找Asset引用的。虽然很泛用,但跑起来非常的慢。
其实我们实际使用的时候,我们会经常做某一种搜索(比如查找脚本在预制体上的引用)。对于这种需要使用的特定条件的查找,单独做一个小工具可能效率会更高。

本来还在烦恼怎么做,导师很快就做好了,跑起来很快。他告诉我,秘诀是 GetComponent。

思路

代码不方便直接贴出来,但还是想分享(记录)一下几个关键的步骤和思路~

选择脚本

我们需要在编辑器上用一个 field 去放我们想要搜寻的脚本。
但是没办法是用在泛型类上。需要指定实际的类型。

Interface也可以查,放心用!

1
2
3
4
5
6
7
8

targetScript = EditorGUILayout.ObjectField(m_targetScript, typeof(MonoScript), false) as MonoScript;

/*
class SomeMono<TData> : MonoBehaviour
doesnt work on this case, the result will be null
*/
Type targetType = m_targetScript.GetClass();

获取预制体

从我们选择的目录下获取所有的prefab,相信Unity,这一步不会很慢的。

1
2
3
4
5
6
7

// we can also set the folderPath to save our time :D

var paths = AssetDatabase.FindAssets($"t: prefab", new[] { folderPath });
GameObject[] toCheck = new GameObject[paths.Length];
for (int i = 0; i < paths.Length; i++)
toCheck[i] = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(paths[i]));

查询引用

1
2
3
4
5

Type targetType;
GameObject prefab;
var components = prefab.GetComponents(targetType);
// if components is not empty, then you get your stuff :D

总结

如果要说学到了什么的话,大概是做工具前要搞明白它的使用情景,不能太脱节Tw.T

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

被封在鼠窝里两个月了,一直都是在家用自己的老爷备用机做工作。但是老(项)爷(目)机的性(优)能(化)实在是太差了。
打开 Unity 需要5分钟,编译代码3分钟,点一下 play 又是2分钟。这段时间来回修各种BUG,切来切去浪费了很多时间。

决定还是用大奶机工作了,但又非常不愿意把公司的间谍全家桶装在电脑上,咋办呢?

上网查了一下,大家说可以把全家桶关在虚拟机里,然后再把网络分享给宿主机。

Hyper-V

最开始试用了 VirtualBox 无论是装32位还是64位的Win10或者 WinServer 都卡到飞起。即使把虚拟机的虚拟盘放在 SSD 上也还是卡。
了解到 Windows 有这个 Hyper-V 虚拟机就决定试用一下。

结果这个玩意儿性能超棒的哦!放在 SSD 上可以说是不怎么卡。而且也能直接读宿主机的硬盘或油盘,非常的方便,虽然读盘的时候硬盘使用率显示是拉满的。

我记得以前如果要要使用 VM 或者 VBox 的话需要关闭 Hyper-V 的功能,但是自从 Win10 20H2 还是某个版本之后就不需要这么做了。

虚拟机启动小坑

“Boot Issue: Start PXE over IPv4”

装好系统以后俺启动虚拟机就报这个错误。
把硬盘移动到第一位就好了。

配置虚拟网卡

参考了网上的通用方案,需要两张虚拟网卡,一张桥接用来上网,另一个接收全家桶的分享。

桥接

我这边选用的是有线网的接口

内部网络

ipconfig 结果

宿主机配置

接下来就是配置宿主机,这又有一个不一样的情况出现了。

在 Hyper-V 那边添加了1个虚拟交换机以后, 宿主机这边会出现2个适配器。不知道是不是我这边特有的情况。
把内部网络适配器的IP地址设置到 192.168.137.0 网段,我这边修改的是这个名字相同的。

然后宿主机添加转发就可以了,不过速度不够快,全家桶直接装宿主机上还是比较快的,

1
route add x.y.0.0 mask 255.255.0.0 192.168.137.1

参考资料

https://yidianyidi.fun/VirtualBox/yidianyidi-1708121150.html

https://www.youtube.com/watch?v=9Hc-5EOtaJs

经过了一次用户测试之后,拿到的反馈说我们很多页面的UX设计都不行,只能各种重做,不论是我程序这边还是UXUI组那边,我们都需要重新拼好一些 prefab。

然后还有一些旧的 prefab 我们需要换皮改尺寸之类的,这次就来分享一个简单的小工具,用来做一些简单的尺寸适配。

RectTransform 上 anchor 的 Min, Max 都是已经做了归一化的值。依靠调整 anchor 的值就可以设置当前元素在父元素中的相对位置和面积占比。
具体参考下面这篇文章
https://blog.csdn.net/ChinarCSDN/article/details/88983587

代码

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

[InitializeOnLoad]
public static class RectTransformExtendedEditor
{
struct EditContext
{
public RectTransform Transform;
}

private static EditContext m_context;
private static bool m_showTool;

static RectTransformExtendedEditor()
{
Selection.selectionChanged += OnSelectionChanged;
}

private static void OnSelectionChanged()
{
SceneView.duringSceneGui -= OnSceneGUI;
if (Selection.activeTransform is RectTransform rectTransform)
{
m_context = new EditContext()
{
Transform = rectTransform
};

SceneView.duringSceneGui += OnSceneGUI;
}
}

private static void OnSceneGUI(SceneView sceneView)
{
Handles.BeginGUI();
if (GUI.Button(new Rect(0, 0, 20, 20), "T"))
{
m_showTool = !m_showTool;
}
if (m_showTool)
{
GUILayout.BeginArea(new Rect(0, 20, 120, 320));
if (GUILayout.Button("Set Anchors"))
{
RectTransform rectTransform = m_context.Transform;
RectTransform parent = rectTransform.parent as RectTransform;

Undo.RecordObject(rectTransform, "Set Anchors");

if (parent != null)
{
Vector2 anchorMin = new Vector2(
rectTransform.anchorMin.x + rectTransform.offsetMin.x / parent.rect.width,
rectTransform.anchorMin.y + rectTransform.offsetMin.y / parent.rect.height);

Vector2 anchorMax = new Vector2(
rectTransform.anchorMax.x + rectTransform.offsetMax.x / parent.rect.width,
rectTransform.anchorMax.y + rectTransform.offsetMax.y / parent.rect.height);

rectTransform.anchorMin = anchorMin;
rectTransform.anchorMax = anchorMax;
rectTransform.offsetMin = rectTransform.offsetMax = Vector2.zero;
}
}
GUILayout.EndArea();
}

Handles.EndGUI();
}
}

End

有了这个 acnhor 适配真的方便不少。有时候我们做好一个 list element,但在不同的地方用到的时候大小可能不一样。
用这个适配调整以后可以防止各种奇妙出格,而且就点两下按钮,对于 UXUI 团队来说也是非常容易上手的。

为了方便策划在 scriptable object 上配置我们的任务以及事件相关的文本,我做了一个简单的 struct 。里面包含了 I2 的 Lockey 和一个参数数组。
既然要方便策划配置,那肯定要做一个简单的预览,这样策划就能知道自己配置的文本参数是否正确。

既然要做预览,那肯定得得自己复写一下他的 property drawer了。对一个字段的编辑器绘制重写有两种方式,一种是继承 OdinValueDrawer class, 还有一种是 CustomValueDrawer Attribute.

https://odininspector.com/tutorials/how-to-create-custom-drawers-using-odin/how-to-create-a-custom-value-drawer
https://odininspector.com/attributes/custom-value-drawer-attribute

踩坑

我定义的结构是这样的。

在默认情况下他已经能在 inspector上画出我的 Lockey 和我的参数数组。
因为考虑到给整个结构做 OdinValueDrawer 需要自己画数组有点麻烦,所以我选择用 CustomValueDrawer 只重新画 LocalizedString。

LocalizedString 的重画我直接照搬了同事的,但是我发现了一个非常奇怪的问题。

我创建了一个下拉选框,然后在用户选中一个新的 lockey 的时候我对结构体中的 lockey 进行更新。但我发现在下拉选框的选中事件触发后进行赋值,绘制完成再返回新的结构,得到的是没有被更改的结果。
但是同样的绘制代码,在 OdinValueDrawer 中却能得到正确的结果!!!CustomValueDrawer 和 OdinValueDrawer 的原理是不一样的。

OdinValueDrawer 就是相当于一个更先进的 Property Drawer, 在这个绘制器里是可以获取到原本的实际的字段值的。

CustomValueDrawer 则像是一个函数,里面包含了绘制的方法,但是不能够直接操作原本的字段,需要最后把你更新到的值返回出去,这才能够赋值给实际的那个字段。

代码给出,有问题的地方使用注释标明了。

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

[Serializable]
public struct LocalizationStringParmPack
{
[CustomValueDrawer("DrawPreviewText")]
public LocalizedString TextLoc;
public LocalizationParamsManager.ParamValue[] ParmsArray;
public override string ToString()
{
return this.GetRawEnglishString();
}

#if UNITY_EDITOR

static GUIStyle m_previewStyle;
static GUIStyle PreviewStyle
{
get
{
if (m_previewStyle == null)
{
m_previewStyle = new GUIStyle(EditorStyles.textArea);
m_previewStyle.wordWrap = true;
}
return m_previewStyle;
}
}

// use hacky stuff to get selected new lockey
static string s_wtfKey = string.Empty;
static bool s_hasChanged = false;
LocalizedString DrawPreviewText(LocalizedString value, GUIContent label)
{
using (new EditorGUILayout.VerticalScope(UnityEditor.EditorStyles.helpBox))
{
// draw lockey
using (new EditorGUILayout.HorizontalScope())
{
if (label != null)
{
EditorGUILayout.LabelField(label, GUILayout.Width(EditorGUIUtility.labelWidth));
}

// weird stuff happin in dropdown
if (EditorGUILayout.DropdownButton(new GUIContent(value.mTerm), FocusType.Keyboard))
{
var terms = LocalizationManager.GetTermsList();
var selector = new GenericSelector<string>("Select Term", terms);
selector.EnableSingleClickToSelect();
selector.SelectionConfirmed += s =>
{
if (s.Any())
{
s_wtfKey = s.First();
// WEIRD STUFF HERE
/* if I do
LocalizedString next = new LocalizedString(s.First());
and return 'next' and the end of method, it just doesnt work!!!

cuz the GenericSelector is actually a editor window, and
*/
s_hasChanged = true;
}
};
selector.ShowInPopup();
}

int mask = (value.mRTL_IgnoreArabicFix ? 0 : 1) +
(value.mRTL_ConvertNumbers ? 0 : 2) +
(value.m_DontLocalizeParameters ? 0 : 4);

using (var changed = new EditorGUI.ChangeCheckScope())
{
int newMask = EditorGUILayout.MaskField(mask, new string[] { "Arabic Fix", "Ignore Numbers in RTL", "Localize Parameters" }, GUILayout.Width(30));
if (newMask != mask)
{
value.mRTL_IgnoreArabicFix = (newMask & 1) == 0;
value.mRTL_ConvertNumbers = (newMask & 2) == 0;
value.m_DontLocalizeParameters = (newMask & 4) == 0;
}
}

if (GUILayout.Button($"Clear"))
{
/* this will also work
LocalizedString next = new LocalizedString("[NULL]");
*/
s_wtfKey = "[NULL]";
}

}

LocalizedString next = s_hasChanged ? new LocalizedString(s_wtfKey) : value;
s_hasChanged = false;
/*
draw preview
draw preview
draw preview
*/
return next;
}
}

string GetParamValue(string key)
{
for (int i = 0; i < ParmsArray.Length; i++)
{
if (ParmsArray[i].Name == key)
{
return string.IsNullOrEmpty(ParmsArray[i].Value) ? key : ParmsArray[i].Value;
}
}
return key;
}

#endif
}

又踩坑

因为这边使用了静态字段,所以如果需要同时绘制这个字段两次,那就会出问题。所以最后我还是换回OdinValueDrawer。
静态字段最好还是别在这种情况下使用吧。

Unity 的 InputSystem 正式发布也好一段时间了。本来这边也想自己做一套输入处理但是看 Unity 这个官方的还挺方便就打算直接用。

InputAction

说下我对这个 InputAction 的简单理解。

InputAction

- Composite
    - Binding
- Binding
  • Interaction
  • Processor

InputAction 是定义的最上层的交互事件,我认为他是直接和 gameplay 层面交互的,提供响应事件和输入设备的数据(比如鼠标的位置,或是线性按钮的数值)

Binding 是具体到输入设备上按键,线性输入的一个数据结构

Composite 是把 Binding 提供的直接输入数据处理成我们想要的数据并提供给 InputAction。

另外两个可以应用于InputAction,Composite 和 Binding 的组件

Interaction 是用来规定当前的 输入/行为 触发的机制。

Processor 是用来对当前的输入数据做追加处理的。

rebind

我在官方的tank example中做的 rebind save

对于简单的 Button Action 里的 Binding, rebind 的时候我们是追加了 OverridePath 给这个 Button Action 下面的 Binding。

Path 是一个字符串, 大概是这样的 “/buttonSouth”。 里面包含了输入设备类型和具体的按钮名。(其实我觉得, 这样的文本可以提供成一个Const string ,会好用很多)

对于简单的 rebind 存储,我们只需要找到所有的 Binding 里的 override path 保存下来,下次启动游戏加载了原本的 Action Map 之后再把 Override Path 给赋值上去。

代码

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

public static class InputBindSaveHelper
{
static readonly string s_keyMappingPreferKey = "KeyMapping";

public static void SaveActionMapRebinding(InputActionMap actionMap)
{
Dictionary<Guid, string> overridedActionPathDict = new Dictionary<Guid, string>();
UnityEngine.InputSystem.Utilities.ReadOnlyArray<InputAction> actions = actionMap.actions;
for (int i = 0; i < actions.Count; i++)
{
InputAction action = actions[i];
UnityEngine.InputSystem.Utilities.ReadOnlyArray<InputBinding> actionBindings = action.bindings;
for (int j = 0; j < actionBindings.Count; j++)
{
InputBinding actionBinding = actionBindings[j];
if (string.IsNullOrEmpty(actionBinding.overridePath))
continue;
overridedActionPathDict.Add(actionBinding.id, actionBinding.overridePath);
}
}

BinaryFormatter binFormatter = new BinaryFormatter();
MemoryStream mStream = new MemoryStream();
binFormatter.Serialize(mStream, overridedActionPathDict);

byte[] byteArr = mStream.ToArray();
// idk why [Encoding.UTF8.GetString()] just doesnt work
string encodingString = Convert.ToBase64String(byteArr, Base64FormattingOptions.InsertLineBreaks);
PlayerPrefs.SetString(s_keyMappingPreferKey, encodingString);
}

public static void LoadActionMapRebinding(InputActionMap actionMap)
{
string encodingString = PlayerPrefs.GetString(s_keyMappingPreferKey);
byte[] byteArr = Convert.FromBase64String(encodingString);

BinaryFormatter binFormatter = new BinaryFormatter();
MemoryStream mStream = new MemoryStream(byteArr);
Dictionary<Guid, string> rebindSavePairDict = binFormatter.Deserialize(mStream) as Dictionary<Guid, string>;

if (rebindSavePairDict != null)
{
UnityEngine.InputSystem.Utilities.ReadOnlyArray<InputAction> actions = actionMap.actions;
for (int i = 0; i < actions.Count; i++)
{
InputAction action = actions[i];
UnityEngine.InputSystem.Utilities.ReadOnlyArray<InputBinding> actionBindings = action.bindings;
for (int j = 0; j < actionBindings.Count; j++)
{
InputBinding actionBinding = actionBindings[j];
// override
if (rebindSavePairDict.ContainsKey(actionBinding.id))
{
actionBinding.overridePath = rebindSavePairDict[actionBinding.id];
action.ApplyBindingOverride(actionBinding);
}
}
}

}
}
}

当前因为项目里的存档相关的功能还没做完,结构还没定下来,所以我只能把 rebind 存在PlayerPrefs里。

总结

我也只是大概看了一下 Unity 的这个 InputSystem,没有很深的去使用。但是初步上手的时候就感觉他的封装和扩展性对于那些操作要求不高的游戏是已经够用的了。
比较麻烦的一个点就是他的 Interaction, Processor 和 Binding 的覆盖全都需要用 string。直接写的 string 会非常不方便,估计使用者得自己总结一些其常用的 const string 写进变量直接调用。
这样才能比较好的维持代码可读性。

之前做了一个自带简单 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;
}
}