異步
Invoke
public void MonoBehaviour.Invoke(string methodName, float time);
public void MonoBehaviour.InvokeRepeating(string methodName, float time, float repeatRate);
public void MonoBehaviour.CancelInvoke();
public void MonoBehaviour.CancelInvoke(string methodName);
public bool MonoBehaviour.IsInvoking(string methodName);
Invoke是一組將函數交給Unity架構運作的方法,使用Invoke調用的函數是脫離組件生命週期的。
Invoke委派了在物理時間經過time秒後調用methodName方法。
InvokeRepeating委派了物理時間經過time秒後調用methodName方法,隨後每repeatRate秒再調用一次。
CancelInvoke可以停止由本腳本委派的所有Invoke函數和InvokeRepeating函數,如果加上參數,則取消對應類型的函數。
IsInvoking可以返回本腳本是否已經委派了至少一個Invoke函數。
以下是使用Invoke實現燈光閃爍的效果的例子:
public class LightBlink : MonoBehaviour
{
Light light;
private void Start()
{
light = GetComponent<Light>();
TurnOnLight();
}
private void TurnOnLight()
{
light.enabled = true;
Invoke("TurnOffLight",2);
}
private void TurnOffLight()
{
light.enabled = false;
Invoke("TurnOnLight",2);
}
}
以下是使用Invoke實現使物體逐漸縮小的效果的例子:
public class ShrinkEffect : MonoBehaviour
{
private void Start()
{
InvokeRepeating("Shrink", 0, 0.1f);
Invoke("StopShrink",5);
}
private void Shrink()
{
transform.localScale *= 0.95f;
}
private void StopShrink()
{
CancelInvoke("Shrink");
}
}
如果Invoke時不能找到需要找到的函數,不會拋出運行時異常,但會在Console面板輸出一條信息。
Invoke的最大缺點在於被委派的函數只能是無參數函數。但由於Invoke的調用是脫離生命週期的,所以當組件enable爲false或物體activeSelf爲false時,Invoke可以繼續執行,相反的,協程是不可以的。如果需要在SetActive(true)前進行一些與本體無關的額外操作而需要推遲SetActive(true)時(如登場時的光效動畫),可以使用Invoke代替Coroutine。
Coroutine
協程和多線程不同,協程由Unity框架提供,在組件生命週期中被執行,協程的處理在物理循環或邏輯循環之後,具體在何時處理由協程的構建方式決定;而線程則由操作系統調度。所有涉及組件生命週期的功能都應該使用協程而不是線程實現。
協程和線程都可以達成表面上的並行效果,但協程的切片是開發者手動進行的,可以預期切片的次數和位置,以及每個切片在每一幀的運行時間,可以保證在功能A所有語句運行完後再執行功能B的語句;多線程中切片由操作系統控制,線程是交錯運行的,難以保證語句的順序。
協程始終屬於同一個遊戲線程,所以使用大量協程時並不能充分利用硬件多核特性,而多線程可以。
使用線程而不是協程的情況:
(1) 在不用任何插件的情況下實現網絡通訊或http請求。
(2) 加載外部文件,調用其他軟件(比如內付調用支付軟件)。
(3) 十分耗時的,在單線程中難以保證流暢度的運算(大規模查找和排序等)。
我們通過迭代器來實現程序的切片,使用不同yield語句可以將切片分配給不同的處理方式。程序在運行到yield語句時會"掛起",等待調節合適時再繼續運行。
使用StartCoroutine來開啓一個協程:
public Coroutine MonoBehaviour.StartCorountine(IEnumerator routine);
public Coroutine MonoBehaviour.StartCorountine(string methodName; object value = null);
協程開啓後會由本組件的生命週期執行,開啓協程之後的語句不會受協程暫停。
在組件A中使用StartCoroutine調用組件B中的迭代器開啓協程,這個協程將會由組件A的生命週期執行。
構造一個迭代器,我們需要聲明一個返回值爲IEnumerator的方法,然後通過yield return語句來標記需要掛起的位置和喚醒的模式,下圖中的組件展示了協程的邏輯和執行順序:
public class CoroutineExample : MomoBehaviour
{
private void Start()
{
Debug.Log("Invoke corountine.");
StartCoroutine(RoutineExample());
Debug.Log("Finish invoke coroutine.");
}
private IEnumerator RoutineExample()
{
Debug.Log("Coroutine Start.");
yield return new WaitForSeconds(3);
Debug.Log("Coroutine End");
}
}
上述代碼中yield return new WaitForSeconds(3)可以將協程掛起,在3秒後再執行後續代碼。
yield return的方式不同,掛起的方式也不同
yield return 0; yield return null; yield return;
這三種方法可以將協程掛起,在下一個邏輯幀Update後繼續執行後續代碼。它們不會受timeScale影響。
yield return new WaitForFixedUpdate();
將協程掛起,在下一個物理幀FixedUpdate後繼續執行後續代碼。會受timeScale影響。
yield return new WaitForSeconds(float time);
將協程掛起,在物理循環經過time秒後繼續執行後續代碼。會受timeScale影響。
yield return new WaitForSecondsRealtime(float time);
將協程掛起,在邏輯循環經過time秒後繼續執行後續代碼。不會受timeScale影響。
yield return new WaitForEndOfFrame();
將協程掛起,在該邏輯幀的渲染結束後繼續執行後續代碼。
yield return new StartCoroutine(IEnumerator routine);
將該協程掛起,開啓目標協程,待目標協程結束後,繼續執行該協程的後續代碼。
yield return new WaitUntil(()=>bool condition);
每個邏輯幀Update之後檢測一次,當Lambda表達式返回ture時,繼續執行後續代碼。
yield return new WaitUntil(()=>bool condition);
每個邏輯幀Update之後檢測一次,當Lambda表達式返回false時,繼續執行後續代碼。
yield break;
結束協程,等同於普通函數中的return。
注:生命週期中的一些事件可以改爲IEnumerator來使用協程調用,如Start和Awake等。
中斷協程時,所有該組件上正中被掛起的協程會被移除。中斷協程的方法包括:
public void MonoBehaviour.StopAllCoroutines();
public void MonoBehaviour.StopCoroutine(string methodName);
public void MonoBehaviour.StopCoroutine(Coroutine routine);
其中,使用string作爲參數的函數,不能用於停止不是由string作爲參數開啓的協程。
我們可以在開啓協程時將其保存下來以方便將來定向的將其停止:
public class StopCoroutineExample : MonoBehaviour
{
public IEnumerator Start()
{
Coroutine routine = StartCoroutine(LogMessage());
yield return new WaitForSeconds(1.5f);
StopCoroutine(routine);
Debug.Log("Coroutine end manually.");
}
public IEnumerator LogMessage()
{
Debug.Log("Coroutine start.");
yield return new WaitForSeconds(3);
Debug.Log("Coroutine end automatically.");
}
}
使用協程有效的避免了一些垃圾變量和多餘函數,讓代碼更優雅。我們使用協程重新實現了上文中使用Invoke的兩個效果。
使用協程實現燈光閃爍的效果:
public class LightBlink : MonoBehaviour
{
Light light;
private void Start()
{
Coroutine controller = StartCoroutine(LightBlinking);
}
private IEnumerator LightBlinking()
{
for(;;)
{
light.enabled = true;
yield return new WaitForSeconds(2);
light.enabled = false;
yield return new WaitForSeconds(2);
}
}
}
使用協程使物體逐漸縮小:
public class ShrinkEffect : MonoBehaviour
{
private void Start()
{
StartCoroutine(Shrink(5,0.1f));
}
private IEnumerator Shrink(float time; float rate)
{
for(float t = 0; t <= time; t += rate)
{
transform.localScale *= 0.95f;
yield return new WaitForSeconds(rate);
}
}
}