Unity 新手入门 如何理解协程 IEnumerator yield
Unity 新手入門 如何理解協程 IEnumerator
本文包含兩個部分,前半部分是通俗解釋一下Unity中的協程,后半部分講講C#的IEnumerator迭代器
協程是什么,能干什么?
為了能通俗的解釋,我們先用一個簡單的例子來看看協程可以干什么
首先,我突發奇想,要實現一個倒計時器,我可能是這樣寫的:
public class CoroutineTest : MonoBehaviour {public float sumtime = 3;void Update()//Update是每幀調用的{{sumtime -= Time.deltaTime;if (sumtime <= 0)Debug.Log("Done!");}} }我們知道,寫進 Update() 里的代碼會被每幀調用一次,
所以,讓總時間sumtime在Update()中每一幀減去一個增量時間Time.deltaTime(可以理解成幀與幀的間隔時間)就能實現一個簡單的倒計時器
但是,當我們需要多個獨立的計時器時,用同樣的思路,我們的代碼可能就會寫成這樣:
public class CoroutineTest : MonoBehaviour {public float sumtime1 = 3;public float sumtime2 = 2;public float sumtime3 = 1;void Update(){sumtime1 -= Time.deltaTime;if (sumtime1 <= 0)Debug.Log("timer1 Done!");sumtime2 -= Time.deltaTime;if (sumtime2 <= 0)Debug.Log("timer2 Done!");sumtime3 -= Time.deltaTime;if (sumtime3 <= 0)Debug.Log("timer3 Done!");} }重復度很高,計時器越多看的越麻煩
然后有朋友可能會提到,我們是不是可以用一個循環來解決這個問題
for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime) {//nothing } Debug.Log("This happens after 5 seconds");現在每一個計時器變量都成為for循環的一部分了,這看上去好多了,而且我不需要去單獨設置每一個跌倒變量。
但是
但是
但是
我們知道Update() 是每幀調用一次的,我們不能把這個循環直接寫進Update() 里,更不能寫一個方法在Update() 里調用,因為這相當于每幀開啟一個獨立的循環
那么有沒有辦法,再Update()這個主線程之外再開一個單獨的線程,幫我們管理這個計時呢?
好了,你可能知道我想說什么了,我們正好可以用協程來干這個
先來看一段簡單的協程代碼
public class CoroutineTest : MonoBehaviour {void Start(){StartCoroutine(Count3sec());}IEnumerator Count3sec(){for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime)yield return 0;Debug.Log("This happens after 3 seconds");} }你很可能看不懂上面的幾個關鍵字,但不急,我們一個個解釋上面的代碼干了什么
StartCoroutine(Count3sec());這一句用來開始我們的Count3sec方法
然后你可能想問的是
理解以下的話稍有難度,但暫時理解不了問題也不大
詳細的講:
IEnumerator 是C#的一個迭代器,你可以把它當成指向一個序列的某個節點的指針,C#提供了兩個重要的接口,分別是Current(返回當前指向的元素)和 MoveNext()(將指針向前移動一個單位,如果移動成功,則返回true)
通常,如果你想實現一個接口,你可以寫一個類,實現成員,等等。迭代器塊(iterator block) 是一個方便的方式實現IEnumerator,你只需要遵循一些規則,并實現IEnumerator由編譯器自動生成。
一個迭代器塊具備如下特征:
那么yield關鍵字是干嘛的?它用來聲明序列中的下一個值,或者一個無意義的值。如果使用yield x(x是指一個具體的對象或數值)的話,那么movenext返回為true并且current被賦值為x,如果使用yield break使得movenext()返回false(停止整個協程)
看不太懂?問題不大
簡單來說:
你現在只需要理解,上面代碼中,IEnumerator類型的方法Count3sec就是一個協程,并且可以通過yield關鍵字控制協程的運行
一個協程的執行,可以在任何地方用yield語句來暫停,yield return的值決定了什么時候協程恢復執行。通俗點講,當你“yield”一個方法時,你相當于對這個程序說:“現在停止這個方法,然后在下一幀中,從這里重新開始!”
yield return 0;然后你可能會問,yield return后面的數字表示什么?比如yield return 10,是不是表示延緩10幀再處理?
并不!
并不!
并不!
yield return 0表示暫緩一幀,也就是讓你的程序等待一幀,再繼續運行。(不一定是一幀,下面會講到如何控制等待時間)就算你把這個0換成任意的int類型的值,都是都是表示暫停一幀,從下一幀開始執行
它的效果類似于主線程單獨出了一個子線程來處理一些問題,而且性能開銷較小
現在你大致學會了怎么開啟協程,怎么寫協程了,來看看我們還能干點什么:
IEnumerator count5times(){yield return 0;Debug.Log("1");yield return 0;Debug.Log("2");yield return 0;Debug.Log("3");yield return 0;Debug.Log("4");yield return 0;Debug.Log("5");}在這個協程中,我們每隔一幀輸出了一次Hello,當然你也可以改成一個循環
IEnumerator count5times(){for (int i = 0; i < 5; i++){Debug.Log("i+1");yield return 0;}}重點來了,有意思的是,你可以在這里加一個記錄始末狀態的變量:
public class CoroutineTest : MonoBehaviour {bool isDone = false;IEnumerator count5times(){Debug.Log(isDone);for (int i = 0; i < 5; i++){Debug.Log("i+1");yield return 0;}isDone = true;Debug.Log(isDone);}void Start(){StartCoroutine(count5times());} }很容易看得出上面的代碼實現了什么,也就就是我們一開始的需求,計時器
這個協程方法突出了協程一個“非常有用的,和Update()不同的地方:方法的狀態能被存儲,這使得方法中定義的這些變量(比如isUpdate)都會保存它們的值,即使是在不同的幀中
再修改一下,就是一個簡單的協程計時器了
public class CoroutineTest : MonoBehaviour {IEnumerator countdown(int count, float frequency){Debug.Log("countdown START!");for (int i = 0; i < count; i++){for (float timer = 0; timer < frequency; timer += Time.deltaTime)yield return 0;}Debug.Log("countdown DONE!");}void Start(){StartCoroutine(countdown(5, 1.0f));} }在上面的例子我們也能看出,和普通方法一樣,協程方法也可以帶參數
你甚至可以通過yield一個WaitForSeconds()更方便簡潔地實現倒計時
協程計時器
public class CoroutineTest : MonoBehaviour {IEnumerator countdown(float sec)//參數為倒計時時間{Debug.Log("countdown START!");yield return new WaitForSeconds(sec);Debug.Log("countdown DONE!");}void Start(){StartCoroutine(countdown(5.0f));} }好了,可能你已經注意到了,yield的用法還是很多的
在此之前,我們之前的代碼yield的時候總是用0(或者可以用null),這僅僅告訴程序在繼續執行前等待下一幀。現在你又學會了用yield return new WaitForSeconds(sec)來控制等待時間,你已經可以做更多的騷操作了!
協程另外強大的一個功能就是,你甚至可以yeild另一個協程,也就是說,你可以通過使用yield語句來相互嵌套協程,
public class CoroutineTest : MonoBehaviour {IEnumerator SaySomeThings(){Debug.Log("The routine has started");yield return StartCoroutine(Wait(1.0f));Debug.Log("1 second has passed since the last message");yield return StartCoroutine(Wait(2.5f));Debug.Log("2.5 seconds have passed since the last message");}IEnumerator Wait(float waitsec){for (float timer = 0; timer < waitsec; timer += Time.deltaTime)yield return 0;}void Start(){StartCoroutine(SaySomeThings());} } yield return StartCoroutine(Wait(1.0f));這里的Wait指的是另一個協程,這相當于是說,“暫停執行本程序,等到直到Wait協程結束”
協程控制對象行為
根據我們上面講的特性,協程還能像創建計時器一樣方便的控制對象行為,比如物體運動到某一個位置
IEnumerator MoveToPosition(Vector3 target){while (transform.position != target){transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);yield return 0;}}我們還可以讓上面的程序做更多,不僅僅是一個指定位置,還可以通過數組來給它指定更多的位置,然后通過MoveToPosition() ,可以讓它在這些點之間持續運動。
我們還可以再加入一個bool變量,控制在對象運動到最后一個點時是否要進行循環
再把上文的Wait()方法加進來,這樣就能讓我們的對象在某個點就可以選擇是否暫停下來,停多久,就像一個正在巡邏的守衛一樣 (這里沒有實現,各位讀者可以嘗試自己寫一個)
public class CoroutineTest : MonoBehaviour {public Vector3[] path; public float moveSpeed;void Start(){StartCoroutine(MoveOnPath(true));}IEnumerator MoveOnPath(bool loop){do{foreach (var point in path)yield return StartCoroutine(MoveToPosition(point));}while (loop);}IEnumerator MoveToPosition(Vector3 target){while (transform.position != target){transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);yield return 0;}}IEnumerator Wait(float waitsec){for (float timer = 0; timer < waitsec; timer += Time.deltaTime)yield return 0;} }yield其他
這里列舉了yield后面可以有的表達式
null,0,1,...... 暫緩一幀,下一幀繼續執行
StartCoroutine(Another coroutine) - in which case the new coroutine will run to completion before the yielder is resumed 等待另一個協程暫停
值得注意的是 WaitForSeconds()受Time.timeScale影響,當Time.timeScale = 0f 時,yield return new WaitForSecond(x) 將不會滿足
停止協程
總結一下
協程就是:你可以寫一段順序代碼,然后標明哪里需要暫停,然后在指定在下一幀或者任意間后,系統會繼續執行這段代碼
當然,協程不是真多線程,而是在一個線程中實現的
通過協程我們可以方便的做出一個計時器,甚至利用協程控制游戲物體平滑運動
如果你剛接觸協程,我希望這篇博客能幫助你了解它們是如何工作的,以及如何來使用它們
深入講講IEnumerator
基礎迭代器IEnumerator
迭代器是C#中一個普通的接口類,類似于C++ iterator的概念,基礎迭代器是為了實現類似for循環 對指定數組或者對象 的 子元素 逐個的訪問而產生的。
public interface IEnumerator {object Current { get; }bool MoveNext();void Reset(); }以上是IEnumerator的定義
Current() 的實現應該是返回調用者需要的指定類型的指定對象。
MoveNext() 的實現應該是讓迭代器前進。
Reset() 的實現應該是讓迭代器重置未開始位置
就像上文提到的,C#提供了兩個重要的接口,分別是Current(返回當前指向的元素)和 MoveNext()(將指針向前移動一個單位,如果移動成功,則返回true)當然IEnumerator是一個interface接口,你不用擔心的具體實現
注意以上用的都是“應該是”,也就是說我們可以任意實現一個派生自” IEnumerator”類的3個函數的功能,但是如果不按設定的功能去寫,可能會造成被調用過程出錯,無限循環
一個簡單的例子,遍歷并打印一個字符串數組:
public string[] m_StrArray = new string[4];就可以派生一個迭代器接口的子類
public class StringPrintEnumerator : IEnumerator {private int m_CurPt = -1;private string[] m_StrArray;public StringPrintEnumerator(string[] StrArray){m_StrArray = StrArray;}///實現public object Current{get{return m_StrArray[m_CurPt];}}public bool MoveNext(){m_CurPt++;if (m_CurPt == m_StrArray.Length)return false;return true;}public void Reset(){m_CurPt = -1;}///實現ENDpublic static void Run(){string[] StrArray = new string[4];StrArray[0] = "A";StrArray[1] = "B";StrArray[2] = "C";StrArray[3] = "D";StringPrintEnumerator StrEnum = new StringPrintEnumerator(StrArray);while (StrEnum.MoveNext()){(string)ObjI = (string)StrEnum.Current;Debug.Log(ObjI);}} }運行會依次輸出A B C D
但是如果:
不正確的實現Current(返回null,數組下表越界)執行到Debug.Log時候會報錯。
不正確地MoveNext(),可能會出現無限循環(當然如果邏輯上正需要這樣,也是正確的)
不正確地Reset(),下次再用同一個迭代器時候不能正確工作
所以這三個方法如何才是正確的實現,完全要根據由上層的調用者約定來寫
迭代器擴展應用foreach,IEnumerable
C#使用foreach語句取代了每次手寫while(StrEnum.MoveNext())進行遍歷
同時新定了一個接口類來包裝迭代器IEnumerator,也就是IEnumerable,定義為:
public interface IEnumerable {IEnumerator GetEnumerator(); }IEnumerable和IEnumerator的區別
可以看到IEnumerable接口非常的簡單,只包含一個抽象的方法GetEnumerator(),它返回一個可用于循環訪問集合的IEnumerator對象。
IEnumerable的作用僅僅是需要派生類寫一個返回指定迭代器的實現方法,也就是說IEnumerable僅僅是IEnumerator的一個包裝而已。
那么返回的IEnumerator對象呢?它是一個真正的集合訪問器,沒有它,就不能使用foreach語句遍歷集合或數組,因為只有IEnumerator對象才能訪問集合中的項,才能進行集合的循環遍歷。
那么我們回到foreach
foreach
就像上面提到的,foreach需要的是一個定義了IEnumerator GetEnumerator()方法的對象,當然如果他是派生自IEnumerable對象那就更好了。
我們繼續寫上文的StringPrintEnumerator類
這里新定義他的IEnumerable派生類MyEnumerable
轉載于:https://www.cnblogs.com/zhxmdefj/p/10655033.html
總結
以上是生活随笔為你收集整理的Unity 新手入门 如何理解协程 IEnumerator yield的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 花旗银行贷款怎么样?需要哪些条件?
- 下一篇: 信用卡什么时候申请提额好?看2018各银