寫在前面:
寫本系列(自用)的目的是回顧已經學過的知識、記錄新學習的知識或是記錄心得理解,方便自己以后快速復習,減少遺忘。主要是C#代碼部分。
六、四元數
歐拉角具有旋轉約定,也就是說,無論你調整角度的順序是什么,最終物體旋轉的順序都會是約定的旋轉序列。最常見用約定是Y-X-Z約定,也就是物體按Y-X-Z的方向進行旋轉。
無論你如何更改角度,系統都會重新從初始狀態按順序Y-X-Z進行調整,這和萬向鎖相似。也就是說,先轉動的軸會帶動后轉動的軸轉動,但后轉動的軸不能影響先轉動的軸。歐拉角會帶來萬向節死鎖的問題,導致在特定情況下丟失一個自由度。
此外,同樣的角度能使用不同的歐拉角角度來描述,因此,引入四元數來解決歐拉角的這兩個問題。
1、四元數
一個四元數包含一個標量和一個3D向量
[w, v],w為標量,v為3D向量,也就是[w, (x, y, z)],對于給定的任意一個四元數,表示3D空間中的一個旋轉量。
(1)四元數的公式
假設繞著n軸旋轉β度,n軸為(x,y,z),那么可以構成四元數為:
四元數Q = [cos(β/2),sin(β/2)n] = [cos(β/2),sin(β/2)x,sin(β/2)y,sin(β/2)z]
四元數Q表示繞著n軸,旋轉β度的旋轉量。
2、Unity中的四元數
可以有兩種初始化方法:
1、Quaternion q = new Quaternion(sin(β/2)x,sin(β/2)y,sin(β/2)z, cos(β/2));
2、?Quaternion q2 = Quaternion.AngleAxis(旋轉度數, 旋轉軸);
一般使用第二種初始化方法:
void Start()
{//繞著x軸轉60度Quaternion q = new Quaternion(Mathf.Sin(Mathf.Deg2Rad), 0, 0, Mathf.Cos(30*Mathf.Deg2Rad));GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);obj.transform.rotation = q;Quaternion q2 = Quaternion.AngleAxis(60, Vector3.right);
}
(1)四元數和歐拉角轉換
歐拉角轉為四元數使用的API是:Quaternion.Euler(),括號內傳入需要轉化的歐拉角,比如Quaternion.Euler(60, 0, 0),轉化出來的四元數是繞x軸轉60度的四元數。
四元數轉歐拉角使用的API是:.eulerAngles
void Start()
{//歐拉角轉四元數,這個就是繞著x軸轉60度Quaternion q3 = Quaternion.Euler(60, 0, 0);//四元數轉歐拉角print(q2.eulerAngles);
}
最后,需要注意的是,四元數的角度區間是-180~180,因此四元數表示的角度是唯一的。
3、四元數的常用方法
(1)單位四元數
單位四元數表示沒有旋轉量(角位移),當角度為0或者360度時,對于給定軸都會得到單位四元數。
例如[1,(0, 0, 0)]和[-1, (0, 0, 0)]都是單位四元數,表示沒有旋轉量。原因是,由四元數Q = [cos(β/2),sin(β/2)n] = [cos(β/2),sin(β/2)x,sin(β/2)y,sin(β/2)z],角度為0時,cos0 = 1, sin0均為0;或是cos180度等于-1,sin180都等于0。
在Unity中提供了表示單位四元數的API:Quaternion.identity
void Start()
{print(Quaternion.identity);testObj.rotation = Quaternion.identity;
}
單位四元數可用于初始化。初始化在0,0,0點,方向為0, 0, 0的物體:
Instantiate(testObj, Vector3.zero, Quaternion.identity);
(2)四元數插值運算
四元數中的Lerp和Slerp只有一些細微差別,由于算法不同,Slerp的效果會好一些,Lerp的效果相比Slerp更快,但當旋轉范圍較大時,效果不太好,所以建議使用Slerp進行插值運算。與之前的插值運算區別不大,只不過這里插值的是角度,同樣演示了兩種方法:
public Transform target;
public Transform A;
public Transform B;private Quaternion start;
private float time = 0;void Start()
{start = B.rotation;
}void Update()
{A.rotation = Quaternion.Slerp(A.rotation, target.rotation, Time.deltaTime);time+= Time.deltaTime;B.rotation = Quaternion.Slerp(start,target.rotation, time);
}
(3)向量指向轉四元數
向量指向轉四元數的API是:Quaternion.LookRotation(向量)
這個方法可以將傳入的面朝向量轉換為對應四元數角度信息。例如,物體A想要看向物體B,就可以將向量AB傳入。這個方法會返回轉為AB方向的四元數,這時候將物體A的角度信息改為該四元數即可實現A看向B。
public Transform LookA;
public Transform LookB;void Start()
{Quaternion q = Quaternion.LookRotation(LookB.position - LookA.position);LookA.rotation = q;
}
4、四元數計算
(1)四元數相乘
兩個四元數相乘得到一個新的四元數,代表兩個旋轉量的疊加。旋轉相對的坐標系是物體自身的坐標系。
void Start(){Quaternion q = Quaternion.AngleAxis(20, Vector3.up);this.transform.rotation *= q;this.transform.rotation *= q;}
(2)四元數乘向量
四元數乘向量返回一個新向量,相當于將這個向量旋轉了對應四元數的旋轉量。
void Start(){Vector3 v = Vector3.forward;print(v);//四元數×向量順序不能改變v = Quaternion.AngleAxis(45, Vector3.up) * v;print(v);}
七、延遲函數
延遲函數就是會延時執行的函數,我們可以自己設定延時要執行的函數和具體延時時間。
1、延遲函數
延遲函數使用的API是:Invoke(),里面傳入兩個參數,第一個參數是需要延遲執行的函數的名字的字符串,第二個參數是延遲幾秒鐘執行。
void Start()
{Invoke("DelayDoSomething", 5);
}private void DelayDoSomething()
{print("執行");
}
延遲函數無法調用有參數的函數,需要進行包裹。函數名必須是該腳本上聲明的函數。只能和有參函數一樣進行包裹。例如下例,只有這樣才能執行有參數的延遲函數。
void Start()
{Invoke("DelayDoSomething", 5);}private void DelayDoSomething()
{print("執行");TestFunc(2);
}private void TestFunc(int i)
{print("執行" + i);
}
2、延遲重復執行函數
延遲函數可以一直重復執行,使用的API是:InvokeRepeating(),括號內傳入三個參數,第一個參數是重復執行的函數名的字符串,第二個參數是第一次執行延遲多少秒,第三個參數是之后執行延遲多少秒。如下例函數,會在第5秒時執行一次,之后每隔1秒執行一次。
void Start()
{InvokeRepeating("DelayRe", 5, 1);
}private void DelayRe()
{print("重復執行");
}
3、取消延遲函數
(1)取消所有
使用CancelInvoke()后會取消所有延遲函數。
void Start()
{//取消所有CancelInvoke();
}
(2)指定函數名取消
也可以通過傳入函數名的字符串指定取消延遲函數。只要取消了指定延遲,不管之前函數開啟了多少次,延遲執行都會統一取消。此外,如果沒有該延遲函數也不會報錯。
void Start()
{CancelInvoke("DelayRe");
}
4、判斷是否有延遲函數
使用IsInvoking()可以檢查是否存在延遲函數,同樣的,也可以傳入參數判斷是否有指定延遲函數。
void Start()
{if(IsInvoking()){print("存在延遲函數");}if(IsInvoking("DelayDoSomething")){print("存在延遲函數DelayDoSomething");}
}
5、延遲函數受對象失活的影響
腳本掛載的對象失活或是腳本失活,延遲函數只要開啟了,就依然可以執行。腳本或者掛載的對象刪除了或是腳本銷毀了,延遲函數就會失效。
八、協同程序
1、Unity的多線程
Unity是支持多線程的,需要引用命名空間using System.Threading。創建一個新線程使用的是:Thread t = new Thread(),需要注意的是,括號內需要傳入一個委托函數,該函數會在新線程中執行。傳入函數后,t.Start()即可開啟線程。
線程與編輯器的生命周期一樣長,要記得手動關閉多線程。可以在對象銷毀時運行的生命周期函數OnDestroy()中關閉線程。線程的關閉使用t.Abort()即可。
Thread t;void Start()
{t = new Thread(Test);t.Start();
}private void Test()
{while (true){Thread.Sleep(1000);print("新開線程");}
}private void OnDestroy()
{t.Abort();t = null;
}
此外,需要注意的是,新開線程無法訪問Unity相關對象的內容,也就是無法控制場景中的對象,這些對象的邏輯只能在主線程中控制。
2、協同程序
(1)概念
協同程序簡稱協程,它是假的多線程。它將代碼分時執行,不卡主線程,也就是它把可能會讓主線程卡頓的耗時的邏輯分時分布執行。主要使用的場景有:異步加載文件,異步下載文件,場景異步加載,批量創建時防止卡頓。
(2)協程和線程的區別
新開一個線程是獨立的一個管道,和主線程并行執行;新開協程是在原線程之上開啟,進行邏輯分時分步執行。
3、協程的使用
(1)聲明
協同程序函數的返回值必須是 IEnumerator或者繼承了它的類型。協程函數當中必須使用yield return進行返回。例如:
IEnumerator MyCoroutine(int i, string str)
{print(i);yield return new WaitForSeconds(5f);print(str);
}
該協程函數中?yield return new WaitForSeconds(5f);指的是第二部分延遲5s執行。也就是說在print(i)過后會延遲5s再執行print(str)。
此外,yield return可以寫多個,可以把代碼分成幾個部分來執行。
IEnumerator MyCoroutine(int i, string str)
{print(i);yield return new WaitForSeconds(5f);print(str);yield return new WaitForSeconds(1f);print("end");
}
(2)開啟協程
協程函數不能直接調用。有兩種調用方式,第一種先創建一個?IEnumerator變量接收返回值,再將返回值傳入函數StartCoroutine(返回值)。第二種,直接使用StartCoroutine(函數名)。
void Start()
{IEnumerator ie = MyCoroutine(1, "123");StartCoroutine(ie);StartCoroutine(MyCoroutine(1, "123"));
}
(3)關閉協程
有兩種關閉協程的方式:第一種方式為關閉所有協程StopAllCoroutines(),它會關閉你開啟的所有協程。第二種方式為關閉指定協程,可以創建Coroutine c1來存儲協程返回的變量,而后使用StopCoroutine(c1);關閉指定協程。
void Start(){Coroutine c1 = StartCoroutine(MyCoroutine(1, "123"));Coroutine c2 = StartCoroutine(MyCoroutine(1, "123"));Coroutine c3 = StartCoroutine(MyCoroutine(1, "123"));StopCoroutine(c1);StopAllCoroutines();}
4、yield return
1、下一幀執行:yield return 數字、yield return null。過了一幀后,下一次執行會在Update和LateUpdate之間執行。
2、等待指定秒后執行:yield return new WaitForSeconds(秒)。等待指定秒數后,下一次執行會在Update和LateUpdate之間執行。
3、等待下一個固定物理幀更新時執行:yield return new WaitForFixedUpdate()。等待固定物理幀后,會在FixedUpdate和碰撞檢測相關函數之后執行。
4、等待攝像機和GUI渲染完成后執行:yield return newWaitForEndOfFrame()。在LateUpdate之后的渲染相關處理完畢之后執行。
5、一些特殊類型的對象,比如異步加載相關函數返回的對象,后續介紹。一般在Update和LateUpdate之間執行
6、跳出協程 yield break;
5、協程受對象和組件失活銷毀的影響
協程開啟后,組件和物體銷毀,協程不執行;物體失活協程不執行,組件失活協程執行。
6、協同程序原理
(1)協程的本質
協程可以分成兩部分,協程函數本體和協程函數調度器。協程函數本體是一個能夠中間暫停返回的函數,協程調度器是Unity內部實現的,會在對應時機 幫助我們繼續執行協程函數。
(2)協程函數本體
創建一個迭代器IEnumerator ie來接受協程函數,利用迭代器的ie.MoveNext()方法就可以依次調度到下一個yield return為止的函數。利用ie.Current可以獲得yield return中傳入的值。如下:
public class TestClass
{public int time;public TestClass(int time){this.time = time; }
}IEnumerator Test()
{print("第一次執行");yield return 1;print("第二次執行");yield return 2;print("第三次執行");yield return "123";print("第四次執行");yield return new TestClass(10);
}void Start()
{IEnumerator ie = Test();ie.MoveNext();print(ie.Current);ie.MoveNext();print(ie.Current);ie.MoveNext();print(ie.Current);ie.MoveNext();TestClass ts = ie.Current as TestClass;print(ts.time);
}
可見,協程的本質就是一個迭代器,傳入Unity的協程調度器后,Unity自動執行協程。