Unity3d中协程的原理,你要的yield return new xxx的真正理解之道

时间:2021-09-23 23:34:42

之前

之前看了一天的博客,各种文章巴拉巴拉,又说到迭代器了,又贴代码了,看的我头都晕了,还是啥都不懂。最后答案还是在微软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代码!!