FishPlayer

一个喜欢摸鱼的废物

0%

最近在拼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

}
}

效果如图

Awake: This function is always called before any Start functions and also just after a prefabis instantiated. (If a GameObject is inactive during start up Awake is not called until it is made active.)

OnEnable: (only called if the Object is active): This function is called just after the object is enabled. This happens when a MonoBehaviour instance is created, such as when a level is loaded or a GameObject with the script component is instantiated.

check the exec order here

今天做工时候遇到了一个问题,我在Mono A的 Awake 中把Mono A的实例绑定到了GameManager里,然后再Mono B的 OnEnable 中获取Mono A的实例,结果跳了个空引用异常。
把我惊了,原来这一年多来我一直在一个误区里。

我以前一直认为是当场景里所有的 Awake 跑完才开始跑 OnEnable()。但其实不是这样的。

当一个Mono的 Awake 跑完以后会接着跑他的 OnEnable , 接着才去跑其他物体的 Awake 和 OnEnable。

随便写两行测试一下就知道了。

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

public class TesterA1 : MonoBehaviour
{
private Type m_type = null;

private void Awake()
{
m_type = this.GetType();
Debug.Log($"{m_type.Name} awake");
}

private void OnEnable()
{
Debug.Log($"{m_type.Name} on enable");
}
}

public class TesterB1 : MonoBehaviour
{
// Start is called before the first frame update
private Type m_type = null;

private void Awake()
{
m_type = this.GetType();
Debug.Log($"{m_type.Name} awake");
}

private void OnEnable()
{
Debug.Log($"{m_type.Name} on enable");
}
}

public class TesterC1 : MonoBehaviour
{
private Type m_type = null;

private void Awake()
{
m_type = this.GetType();
Debug.Log($"{m_type.Name} awake");
}

private void OnEnable()
{
Debug.Log($"{m_type.Name} on enable");
}
}

都整上一样的内容,A和C绑在物体tester01上,B绑在物体tester02上。

Classes deriving from Unity. Object inherit equality operators that change the behaviour of the == and != operators.
While these operators will perform standard . NET reference equality, if comparing one side to null, these operators will call native code to check if the underlying native engine object is still alive.

这段解释来自Rider的提示:
https://github.com/JetBrains/resharper-unity/wiki/Avoid-null-comparisons-against-UnityEngine.Object-subclasses

UnityEngine 底层是C++写的,所以说,其实我们对UnityObject的空引用检查都会再深入到里面C++那一层去再检查,这个检查是比较消耗的。
之前曾经遇到因为使用了 ?. 语法糖而导致的 this 指针为null的奇妙bug,大概也是因为这个原因。

现在的公司是做手游的,大家对性能都比较看重,不过我也真的好奇这个检查到底有多消耗,决定跑一跑试试。

Test Case

因为是做手游,随意干脆就在手机上测试好了。
大概是这样的环境:
Unity 2019.4 LTS

移动设备这边是
Google Pixel Verzion(俺穷买美版)
Android 9 (已获取Root权限)

TestCase 这边其实我都是瞎弄的。

分别进行三种测试:
Unity Object 的空引用检查;
POCO Object 的空引用检查;
bool 的直接检查;(因为我觉得这是一个代替Unity Object空引用检查的一个方法

每一种检查都有检查到true和false的两种情况,并循环跑多次。

然后,上代码!

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

namespace TempCheckTest
{

public class TempObjA
{
private int m_tempNum = 56;
}

public class NullCheckTester : MonoBehaviour
{
// unity obj null check
private Transform m_emptyTransform = null;
private Transform m_selfTransform = null;

// poco obj null check
private TempObjA m_emptyObjA = null;
private TempObjA m_objA = null;

// bool check
private bool m_boolTrue = true;
private bool m_boolFalse = true;

[Header("test")]
public int m_checkTimes = 100000;

[Header("UI side")]
public UnityEngine.UI.Text m_outPutText = null;
public UnityEngine.UI.Button m_doUnityObjTestButton = null;
public UnityEngine.UI.Button m_doPocoObjTestButton = null;
public UnityEngine.UI.Button m_doBoolTestButton = null;

public UnityEngine.UI.InputField m_inputField = null;

[Button("init awake")]
public void InitTester()
{
m_emptyTransform = null;
m_selfTransform = this.transform;

m_emptyObjA = null;
m_objA = new TempObjA();

m_boolTrue = true;
m_boolFalse = false;

m_outPutText.text = string.Empty;

m_doUnityObjTestButton.onClick.RemoveAllListeners();
m_doPocoObjTestButton.onClick.RemoveAllListeners();
m_doBoolTestButton.onClick.RemoveAllListeners();

m_doUnityObjTestButton.onClick.AddListener(DoUnityObjNullCheckTest);
m_doPocoObjTestButton.onClick.AddListener(DoPOCOObjNullCheckTest);
m_doBoolTestButton.onClick.AddListener(DoBoolObjNullCheckTest);

m_inputField.onValueChanged.RemoveAllListeners();
m_inputField.onValueChanged.AddListener(OnInputNumberChanged);
m_checkTimes = 100000;
m_inputField.text = m_checkTimes.ToString();
}

[Button("do unity.object null check test")]
public void DoUnityObjNullCheckTest()
{
System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
watcher.Start();
for (int i = 0; i < m_checkTimes; i++)
{
if (null == m_emptyTransform) {}

if (null == m_selfTransform) {}
}
watcher.Stop();

string outPut = $"do unity.object null check test 2*{m_checkTimes} cost {watcher.ElapsedMilliseconds}ms";
m_outPutText.text = outPut;
Debug.Log(outPut);
}

[Button("do poco object null check test")]
public void DoPOCOObjNullCheckTest()
{
System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
watcher.Start();
for (int i = 0; i < m_checkTimes; i++)
{
if (null == m_emptyObjA) {}

if (null == m_objA) {}
}
watcher.Stop();

string outPut = $"do poco object null check test 2*{m_checkTimes} cost {watcher.ElapsedMilliseconds}ms";
m_outPutText.text = outPut;
Debug.Log(outPut);
}

[Button("do bool check test")]
public void DoBoolObjNullCheckTest()
{
System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
watcher.Start();
for (int i = 0; i < m_checkTimes; i++)
{
if (m_boolFalse) {}

if (m_boolTrue) {}
}
watcher.Stop();

string outPut = $"do bool check test 2*{m_checkTimes} cost {watcher.ElapsedMilliseconds}ms";
m_outPutText.text = outPut;
Debug.Log(outPut);
}

public void OnInputNumberChanged(string arg0)
{
int result = 0;
if (!Int32.TryParse(arg0, out result))
result = Int32.MaxValue;

m_checkTimes = result;
}

#region mono method

private void Reset()
{
InitTester();
}

private void Awake()
{
InitTester();
}

private void OnEnable()
{
this.enabled = false;
}

#endregion

}

}

Test Result

分别进行10w次和100w次测试看看结果。

10w次:
Unity Object null check 4~7ms
POCO Object null check 0ms
bool check 0ms

100w次:
Unity Object null check 45~60ms
POCO Object null check 3~4ms
bool check 2ms

当检测数量非常多的时候,Unity Object 空引用检查的性能消耗比其他两个大了很多很多。现在的项目里大部分东西其实都是POCO的。
似乎把UnityObject Destroy之后再手动把变量置空是个好习惯(

最近在项目组里做一些小游戏原型,深感自己菜得如虫豸。为了慢慢提升还是决定做一些记录,虽然只是很垃圾的功能。
因为做的是手游,所以对性能蛮看重的,说是原型,为了快也只是项目里开了个单独的新场景做的,3C都有,做好了可以直接打包APK试玩。
废话不多说,这次做的事很简单,在扇形区域内检测一些目标。因为听说Vector3. Angle()里会跑开方的计算,故尝试避免使用这个方法。

思路

需要解决的需求是在检测者 前方 的一个扇形内检测目标物体,其实可以化简为2D平面的检测。
我决定多次使用点乘和叉乘来解决这个问题。

已知数据:
检测者的位置,朝向,检测角度大小,半径大小
检测目标的位置

步骤:

  1. 用检测者的前方向量和检测者到目标的方向向量点乘来判断目标物体是否在检测者前方。
  2. 用检测者前方方向向量和检测区域的两个边界方向分别叉乘得到两个法向向量A,B。
  3. 用检测者到目标的方向向量和检测区域的两个边界方向分别叉乘得到两个法向向量C,D。
  4. 用点乘检测C,D是否同向,是,则在检测区外
  5. 是否同时满足向量A,C同向,向量B,D同向,是,则在检测区内

其实我自己都感觉这中文,写得很乱,还不如直接看代码23333
全部代码:

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

public class TargetCheckTester : MonoBehaviour
{
private Transform m_transform = null;

[SerializeField]
private float m_checkAngle = 60.0f;

[SerializeField]
private float m_checkRadius = 6.0f;

[SerializeField]
private Transform m_targetTransform = null;

private void CheckTarget1()
{
if (null == m_targetTransform)
return;

Vector3 myPos = m_transform.position;
Vector3 myForward = m_transform.forward;
float halfAngle = m_checkAngle * 0.5f;
Vector3 dirUp = Quaternion.Euler(0.0f, -halfAngle, 0.0f) * myForward;
Vector3 dirDown = Quaternion.Euler(0.0f, halfAngle, 0.0f) * myForward;

Vector3 upCheck = Vector3.Cross(myForward, dirUp);
Vector3 downCheck = Vector3.Cross(myForward, dirDown);

Vector3 targetPos = m_targetTransform.position;
// hack, to make them on a 2d plane :)
targetPos.y = myPos.y;
Vector3 toTargetDir = targetPos - myPos;

// draw line
Vector3 point1 = myPos + dirUp * m_checkRadius;
Vector3 point2 = myPos + dirDown * m_checkRadius;
Vector3 point0 = myPos + myForward * m_checkRadius;

Debug.DrawLine(myPos, point1, Color.yellow, Time.deltaTime);
Debug.DrawLine(myPos, point2, Color.yellow, Time.deltaTime);
Debug.DrawLine(myPos, point0, Color.yellow, Time.deltaTime);
Debug.DrawLine(point1, point0, Color.yellow, Time.deltaTime);
Debug.DrawLine(point2, point0, Color.yellow, Time.deltaTime);
Debug.DrawLine(myPos, targetPos, Color.red, Time.deltaTime);
// draw line

if (Vector3.Dot(toTargetDir, myForward) < 0.0f || toTargetDir.sqrMagnitude > m_checkRadius * m_checkRadius)
return;

Vector3 targetUpCheck = Vector3.Cross(toTargetDir, dirUp);
Vector3 targetDownCheck = Vector3.Cross(toTargetDir, dirDown);

float selfCheck = Vector3.Dot(targetUpCheck, targetDownCheck);
float dotCheckUp = Vector3.Dot(upCheck, targetUpCheck);
float dotCheckDown = Vector3.Dot(targetDownCheck, downCheck);
if (selfCheck <= 0.0f && dotCheckUp * dotCheckDown >= 0.0f)
{
Debug.Log("target in sight");
}

}

#region mono method

private void Awake()
{
m_transform = this.transform;
}

private void Update()
{
CheckTarget1();
}

#endregion

}

一个比较简单但是方便的东西

这是啥

我们平时些写了一些函数在MonoBehavior里想测试,这边可以整一个特性放在需要快速调用的方法上(限于无参数的函数)。
这边可以做一个编辑器的按钮,直接放在MonoBehavior的组件面板上。

上代码

先定义我们需要的这个特性,既然是做个按钮,那就叫Button好了。那么这个特性就应该叫做ButtonAttribute。

1
2
3
4
5
6
7
8
9
[AttributeUsage(AttributeTargets.Method)]
public class ButtonAttribute : Attribute
{
public string m_methodName = string.Empty;
public ButtonAttribute(string methodName)
{
m_methodName = methodName;
}
}

既然我们要在Inspector面板上加东西,那我们也要操作一下Inspector面板。
我们做一个CustomEditor面板给我们的MonoBehavior,这个面板会作用于所有的MonoBehavior。

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
[CustomEditor(typeof(MonoBehaviour), true), CanEditMultipleObjects]
public class BaseEditor : Editor
{
private Type m_targetType = null;

public override void OnInspectorGUI()
{
// draw default stuff
base.OnInspectorGUI();

if (null == m_targetType)
m_targetType = target.GetType();

while (m_targetType != null)
{
// try find member function and static function :)
MethodInfo[] methods = m_targetType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
foreach (var method in methods)
{
ButtonAttribute button = method.GetCustomAttribute<ButtonAttribute>();
if (button != null && method.GetParameters().Length > 0)
{
EditorGUILayout.HelpBox("ButtonAttribute: method cant have parameterz.", MessageType.Warning);
}
else if (button != null && GUILayout.Button(button.m_methodName))
{
method.Invoke(target, new object[] { });
}
}
m_targetType = m_targetType.BaseType;
}

}
}

然后新建一个MonoBehavior脚本,试试

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
public class ToxicTester : MonoBehaviour
{

[Button("try exec public method")]
public void ExecPublicMethod()
{
Debug.Log("try exec public method wa");
}

[Button("try exec protected method")]
protected void ExecProtectedMethod()
{
Debug.Log("try exec protected method wa");
}


[Button("try exec private method")]
private void ExecPrivateMethod()
{
Debug.Log("try exec private method wa");
}

[Button("try exec public static method")]
public static void ExecPublicStaticMethod()
{
Debug.Log("try exec public static method wa");
}

}

效果如图