之前
之前看了一天的部落格,各種文章巴拉巴拉,又說到疊代器了,又貼代碼了,看的我頭都暈了,還是啥都不懂。最後答案還是在微軟C#的官網找到了,可喜可賀,故發上來給大家看看,興許能賺個幾百評論呢(并沒有)?
還是要說一下疊代器
foreach(a in list)是怎麼實作的呢?
in的其實不是list本身,而是list裡面的一個疊代器。疊代器一般而言會實作以下兩個方法:
bool Next()
還有
object Current()
。在foreach過程中,in會先調用next方法,這個時候假如有下一個值可以傳回,那就把指針(之類的)指到那個object上面去,然後next方法會傳回一個true說,大佬你可取了,最後in調用Current方法,把東西拿走,該列印列印,做愛做的事。當next傳回一個false,好了沒東西拿了,foreach給我結束吧,然後就結束了這個循環。
c#裡的yield return
這裡我就不說我探索的過程那些廢話啦,直接貼微軟官網的意思。先來一段代碼吧,
foreach(string s in GetAllStrings()){
print(string);
print("string printed");
}
IEnumerator<string> GetAllStrings(){
for(int i = ; i < ; i++){
yield return "str" + i;
}
}
讓我們看看GetAllStrings方法,這裡普通人的了解肯定是,為啥傳回類型是個IEnumerator,但是yield return的卻是一個string??
好了不用猜了,微軟的文檔說了,in GetAllStrings()并不是在執行GetAllStrings這個方法,而實質上是把這個方法體用一個疊代器抱起來了。
還記得前面說的疊代器的兩個方法麼?這裡編譯器對代碼做了點小處理,我實在是懶得去看了,是以隻能告訴你結論,就是當每次in操作外面執行next和current之後,GetAllStrings這裡面就會自動執行到下一句yield return語句,把你要的東西傳到外邊去,然後停住,注意,會馬上停住,不會往下執行了。
然後呢?
然後這一次的内容外面的foreach拿到了,做了愛做的事兒,然後繼續循環他,調他的next和current,于是方法體内的代碼繼續執行,由于yield return我下面沒寫啥語句,是以會繼續for循環,i從0變成1,然後又傳回一個值。假如我yield return下面寫了東西了,就會在下一次的疊代中被執行啦。
再看一下微軟的示例代碼,一個醜陋的疊代器也可以這麼寫
IEnumerator<string> GetAllStrings(){
yield return "str" + ;
//第一次執行foreach操作之後就此打住
yield return "str" + ;
//第二次執行之後就此打住
yield return "str" + ;
}
回到unity3d
從上面的分析,有些同學已經可以猜到了某個用法,對了,就是自己手動調用next和current來控制程式的執行。假如調了第一次的foreach操作之後,我們用某種方式讓程式n秒後再調用這個foreach操作,不就可以在五秒後再執行yield return 後面的語句了嗎?
沒有錯!u3d就是這麼妙,就是用了這麼妙的設計(妙啊.jpg)。
首先,一個遊戲引擎每一幀會調用一次update這就不用我講了吧,如果你連這個也不知道,emmmm…不如Ctrl+w走一個?
設想這種情形:
StartCoroutine(foobar());
IEnumerator foobar(){
yield return WaitForSeconds();
print("hello qiangpozheng");
}
這個時候引擎做了啥事呢?首先來看一下這個WaitForSeconds的類,他和WWW以及其他某些類一樣,繼承了個YieldInstucment接口,這個接口裡面有個方法
bool keepWaiting()
傳回一個布爾值。首先,程式把foobar這個疊代器給了引擎。引擎接到了這個疊代器,二話不說先疊代他一次,得到一個WaitForSeconds,保住,存起來。下一幀的update到啦,update之後不打豆豆,update之後問一下那個疊代器傳回的WaitForSecond的keepWaiting方法,還要不要繼續等呀。此時兩種結果,一種是時間還沒到,傳回false,那好這幀不關它事了。而…
YieldInstrucment傳回了true啦
可以卷錢跑了。
ok,那代表我條件達成啦。這擱在WaitForSeconds是時間到了,擱在Resource是資源載入完成了,擱在WWW就是網絡操作搞定啦。
題外話: 一般别的語言我們的操作就是搞定了,回調一下剛才傳進來的函數吧,沒錯這邊的操作也很像。
好了傳回true了,這個時候引擎把剛才存下來又黑又亮的寶貝疊代器掏了出來,然後執行一次foreach。如果你腦袋還算靈光沒忘記我在上文講的東西的話,你就會發現yield return的代碼,被執行啦。
好了,至此,WaitForSeconds的原理就此結束啦。
好玩的應用
假設我們需要在遊戲裡每一幀執行某個操作,直至某個條件失效,沒有協程我們一般是怎麼寫的呢?
void update(){
if(_goOnUpdate){
//巴拉巴拉
if(bbb < ){
//在某個條件下結束
_goOnUpdate = false;
}
}
}
這麼寫兩個問題,一個是繁瑣,程式員最讨厭的事。第二個,就算你_goOnUpdate為false了,照樣每一幀要check一次這個布爾值,看着煩。
現在用剛才學到的知識寫,可以怎麼寫?
StartCoroutine(foobar());
//求不要吐槽我偶爾的小駝峰命名,我剛從Java那疙瘩過來的
IEnumerator foobar(){
while(true){
// do sth. xxx可以是随便啥值,我試過好像就算是null也沒問題
yield return xxx;
if(bbb < ){
yield break;
}
}
}
這裡不用while true循環行不行呢?我以前也想過這個問題,不行。程式當是這樣,當你執行完本幀想做的操作之後, yield return,于是這段代碼被暫停,然後下一幀(為啥是下一幀?因為你傳回的是null,不是YieldInstrucment或者别的東西,沒有上面所說的那個keepWaiting)系統又繼續從下一句調用,執行下面的語句,如果達到某個條件yield break(之前忘了講這個方法可以跳出疊代)結束,如果不符合條件,那繼續執行。繼續執行意味着走到while體的最後一行,然後由于是while true,會重新跳到wihle體的第一行,然後又是執行第一行和yield return之間的代碼。
總結,這樣子就可以每一幀都執行一次啦~
當然如果你要每一幀都調用的話,那就别寫yield break啦。
應用之二
設想我們需要在背景開個線程做什麼東西,然後更改UI。更改UI是絕對不允許在主線程之外做的,是以我們根據上面的知識,實作
CustomYieldInstructment
接口來實作。好了寫的好累了,直接貼代碼。順便說一句這個代碼是不能用的,因為AssetDatabase不允許在非主線程調用。
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System.Threading;
using UnityEngine;
#endif
public class AssetDatabaseThreadHolder {
#if UNITY_EDITOR
string _url;
public bool IsDone;
public Texture2D ResourceObject{ get; set;}
public AssetDatabaseThreadHolder (string url){
_url = url;
}
public void StartThread(){
new Thread (GetResource).Start ();
}
private void GetResource(){
IsDone = false;
ResourceObject = AssetDatabase.LoadAssetAtPath<Texture2D>(_url);
IsDone = true;
}
#endif
}
然後是實作接口的地方
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
public class AssetDatabaseAsync:CustomYieldInstruction
{
#if UNITY_EDITOR
private AssetDatabaseThreadHolder _threadHolder;
public AssetDatabaseAsync(string url){
_threadHolder = new AssetDatabaseThreadHolder (url);
_threadHolder.StartThread ();
}
public override bool keepWaiting{
get{
return !(_threadHolder.IsDone);
}
}
public Texture2D GetResourceObject{
get{
return _threadHolder.ResourceObject;
}
}
#else
public override bool keepWaiting{
get{
return true;
}
}
#endif
}
最後是調用的地方:
StartCoroutine(_UseAssetDBAsync());
IEnumerator _UseAssetDBAsync(string url){
//實際效果是做不到的,這個鬼東西不允許在非主線程運作
AssetDatabaseAsync adba = new AssetDatabaseAsync(url);
yield return adba;
Texture2D t2d = adba.GetResourceObject;
_SetImageByTexture(t2d);
}
強行總分總
寫完!舒暢!學u3d之類的東西果然要多看國外的原始文檔啊,國内的部落格什麼的太難了解了,這篇除外。希望大家夥兒看到這兒能真正懂得yield return new xx的原理,然後寫出自己滿意的bug代碼!!