之前
之前看了一天的博客,各种文章巴拉巴拉,又说到迭代器了,又贴代码了,看的我头都晕了,还是啥都不懂。最后答案还是在微软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 = 0; i < 5; 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" + 1;
//第一次执行foreach操作之后就此打住
yield return "str" + 2;
//第二次执行之后就此打住
yield return "str" + 3;
}
回到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 < 10){
//在某个条件下结束
_goOnUpdate = false;
}
}
}
这么写两个问题,一个是繁琐,程序员最讨厌的事。第二个,就算你_goOnUpdate为false了,照样每一帧要check一次这个布尔值,看着烦。
现在用刚才学到的知识写,可以怎么写?
StartCoroutine(foobar());
//求不要吐槽我偶尔的小驼峰命名,我刚从Java那疙瘩过来的
IEnumerator foobar(){
while(true){
// do sth. xxx可以是随便啥值,我试过好像就算是null也没问题
yield return xxx;
if(bbb < 10){
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代码!!