C# 学习笔记:迭代器:协程

协程是Unity一个刚开始看起来很诡异但是实际用起来很方便的一种用法。

在游戏运行的时候,总是需要读秒,我之前写过一个游戏,就是炮塔看见人物就等三秒然后开枪,最开始用Time类去读秒,效果勉强能用,但是炮塔多了之后整个脚本逻辑看起来痛苦不已。但是协程就非常好的实现了读秒、读帧、读逻辑的功能。

我们在脚本里实现一遍协程是很简单的,但是协程背后是不容易整明白的。协程的语法对于曾经的我来说特别晦涩。什么IEnumerator、IEnumerable这种东西又不会读又不会写,还不知道干嘛的,再加上yield return,整个人虽然会用,提起协程来也是蒙圈的。

我上一篇文章就专程讲了迭代器,IEnumerator和IEnumerable都是在枚举接口,它们在C# 2.0以后支持了迭代器的功能,即yield return的逻辑休克。这一篇文章我们来讲讲协程在Unity中对迭代器的实现。

协程

协程在Unity里的C#中,主要就是暂停一段逻辑等待某一个功能完成后再执行剩下的逻辑,由于我们Unity脚本中调用得最多的是Update函数,这个函数是没有任何理由条件来暂停它的。所以Unity引入了协程的概念。

1.协程由StartCoroutine()来开启协程。使用StopCoroutine()来关闭协程。

2.StartCoroutine和StopCoroutine方法参数类型为IEnumerator。但也可以支持字符串类型。以下是StartCoroutine的重载版本:

C# 学习笔记:迭代器:协程_第1张图片

3.StartCoroutine是没有返回值的,当碰到迭代器里的yield return后,它会等待我们的返回类型完成,自动为我们执行MoveNext方法,来让协程可以在yield return后执行下去。

StartCoroutine虽然参数只能是IEnumerator,但是我们可以创建IEnumerable的迭代器给它,两种迭代器完成同样的功能,但是IEnumerable的迭代器在给协程的时候需要一些转换的。我把两种迭代器放在一起看看用法,例子如下:

public class CSharpTest : MonoBehaviour
{
    public int value = 11;
    void Start()
    {
        IEnumerable enumerable = Countable(value);
        IEnumerator enumerator = enumerable.GetEnumerator();
        StartCoroutine(enumerator);
        StartCoroutine(Countor(value));

    }
    IEnumerable Countable(int value)
    {
        yield return new WaitForSeconds(3f);
        Debug.Log("IEnumerable" + value);
    }
    IEnumerator Countor(int value)
    {
        yield return new WaitForSeconds(3f);
        Debug.Log("IEnumerator" + value);
    }
}

两个的效果是一模一样的的 我们执行看看:

两个都是同样可以执行的。

迭代器实现协程功能

协程背后就是迭代器,但是我们要自己使用迭代器实现协程的功能却是不容易,整了老半天,我自己整了个和官方功能相似的延时:

public class CSharpTest : MonoBehaviour
{
    private IEnumerator enumerator;

    void Start()
    {
        StartCoroutine(CountTime());
        enumerator = HandEnum();
        enumerator.MoveNext();
    }
    void FixedUpdate()
    {
        if(enumerator.Current is myWaitForSecond)
        {
            var Current = enumerator.Current as myWaitForSecond;
            if(Time.time>Current.waitTime)
            {
                enumerator.MoveNext();
            }
        }
    }
    IEnumerator CountTime()
    {
        Debug.Log("官方协程:开始计时!");
        for (int i = 1; i <= 12;i++)
        {
            Debug.Log("官方协程:过了" + i + "秒");
            yield return new WaitForSeconds(1f);
        }
    }
    IEnumerator HandEnum()
    {
        Debug.Log("手动开启协程,等待三秒");
        yield return new myWaitForSecond(3f);
        Debug.Log("手动协程三秒等待完毕!");

        Debug.Log("手动又让协程等待五秒");
        yield return new myWaitForSecond(5f);
        Debug.Log("手动协程五秒等待完毕");
    }
}
class myWaitForSecond
{
    public float waitTime;
    public myWaitForSecond(float t)
    {
        waitTime = Time.time + t;
    }
}

二者效果是一样的,看看效果:

C# 学习笔记:迭代器:协程_第2张图片

我们可以看到,在协程读秒的时候,我们手动协程一样也完成了它的工作,在过了三秒和五秒的时候准时输出了。

但是,我们定义自己的myWaitForSecond的时候,里面并没有太多的内容,除了一个字段和一个构造函数外别无他物,我们在官方的waitForSecond里面也可以看到,里面实际上非常简单:

C# 学习笔记:迭代器:协程_第3张图片

那么实际上的那些时延操作是谁来完成的呢,留给我们想象空间的只剩下一个,就是一开始的StartCoroutine。

一般而言,StartCoroutine就是简单的对某个IEnumerator 进行MoveNext()操作,但如果他发现IEnumerator其实是一个WaitForSeconds类型的话,那么他就会进行特殊等待,一直等到WaitForSeconds延时结束了,才进行正常的MoveNext调用,而至于WWW或者WaitForFixedUpdate等类型,StartCoroutine也是同样的特殊处理。

那么,我们没有使用StartCoroutine的IEnumerator就只能自己写逻辑决定什么情况来MoveNext了。

协程注意事项

1.

Unity在LateUpdate结束后才会检测协程的yield return是否满足然后再确定是否执行MoveNext。

对于协程来说,按理说我们设定了某个条件,那么协程会在该条件完成后进行MoveNext。

但是我们游戏是一帧一帧来算的。在协程中,会在LateUpdate后对条件是否满足进行判断,我们可以写个例子验证一下:

public class CSharpTest : MonoBehaviour
{
    private bool isUpdateCor = false;
    private bool isLateUpdateCor = false;
    void Start()
    {
        StartCoroutine(Startfunction());
    }
    IEnumerator Startfunction()
    {
        Debug.Log("Start协程开始");
        yield return null;
        Debug.Log("Start协程结束");
    }

    void Update()
    {
        if(!isUpdateCor)
        {
            StartCoroutine(UpdateFunction());
            isUpdateCor = true;
        }
    }
    IEnumerator UpdateFunction()
    {
        Debug.Log("Update协程开始");
        yield return null;
        Debug.Log("Update协程结束");
    }
    void LateUpdate()
    {
        if(!isLateUpdateCor)
        {
            StartCoroutine(LateUpdateFunction());
            isLateUpdateCor = true;
        }
    }
    IEnumerator LateUpdateFunction()
    {
        Debug.Log("LateUpdate协程开始");
        yield return null;
        Debug.Log("LateUpdate协程结束");
    }
}

按照设想,协程一旦在yield return后的条件满足就MoveNext,而且我们这里是返回null,按理说应该执行完yield return后立马输出后面那个debug语句,但是事实上:

C# 学习笔记:迭代器:协程_第4张图片

如上图,直到LateUpdate函数结束后,我们的StarFunction的yield return后的语句才开始返回它的声明。

这个例子表明:Unity在LateUpdate函数结束后才会检测协程的yield return是否满足然后再确定是否执行MoveNext。

以下这张图效果很好:

C# 学习笔记:迭代器:协程_第5张图片

2.

关于StopCoroutine的大坑

StopCoroutine是协程里的一个大坑,我们通常以为只要StopCoroutine里的参数表达形式和StartCoroutine对应就好了。。。

但不是这样!!!!

关闭一个协程有三个方法,这三个方法必须一一对应

例如我们有个迭代器CountTime,那么协程必须是这样声明才能关闭:

1.创建IEnumerator实例开启协程,StopCoroutine里的参数必须是IEnumerator实例。

IEnumerator x = CountTime();
StartCoroutine(x);
......//此处省略逻辑
StopCoroutine(x);

2.创建Coroutine实例,StopCoroutine里的参数必须是Coroutine实例。

Coroutine x;
x = StartCoroutine(CountTime());
......//此处省略逻辑
StopCoroutine(x);

3.使用字符串开关协程。

StartCoroutine("CountTime");
......//此处省略逻辑
StopCoroutine("CountTime");

这个是真的坑,坑炸了。

3.

除了StopCoroutine以外,可以停止协程脚本所在的游戏对象来停止协程(gameObject.SetActive(false)),

只停止协程所在脚本不能停止协程。(MonoBehaviour.enabled = false

有:

使用MonoBehaviour.enabled = false 协程会照常运行,但 gameObject.SetActive(false) 后协程却全部停止,即使在Inspector把gameObject 激活还是没有继续执行并且当我们使用gameObject.SetActive(false) 后,协程不会立即暂停,而是会等待协程执行到下一次yield return后才会关闭协程。

我们看个例子:

public class CSharpTest : MonoBehaviour
{
    private int i;
    void Start()
    {
        StartCoroutine(CountTime());
    }
    IEnumerator CountTime()
    {
        Debug.Log("官方协程:开始计时!");
        for (i = 0; i <= 12;i++)
        {
            Debug.Log("这是第" + i + "次哈哈哈");
            yield return new WaitForSeconds(1f);
        }
    }
    void Update()
    {
        if(i>6)
        {
            this.gameObject.SetActive(false);
        }
    }
}

输出为:

C# 学习笔记:迭代器:协程_第6张图片

我们可以看到,我们i>6的情况是在for循环的i++后就等于7了,此时游戏对象关闭,但是还是输出了“这是第7次哈哈哈”。

协程的功能

我们在协程yield return new以后可以看到,有以下这些类可以接在后面

  • null - 立马继续执行
  • WaitForEndOfFrame - 暂停该协程直到帧上所有渲染和GUI完成之后才继续执行。(可以用于截屏功能)
  • WaitForFixedUpdate - 暂停该协程在下一个FixedUpdate步骤才继续执行。
  • WaitForSeconds - 使协程等待固定的时间段后执行。它受制于我们设定的TimeScale。
  • WaitForSecondsRealtime - 使协程等待固定的时间段后执行,它里面的时间与现实世界时间一致。
  • WWW - 暂停协程等待Web请求完成后继续执行(这个有时间细细写)
  • WaitUntil - WaitUntil是一个bool返回值委托,暂停协程直到该委托包装的bool返回值的函数返回false之后才继续执行。
  • WaitWhile - WaitWhile是一个bool返回值委托,暂停协程直到该委托包装的bool返回值的函数返回true之后才继续执行。
  • 其他的协程 - 等待其他的协程完全执行完逻辑后才继续执行。

我们写一下最后一个功能的情况:

public class CSharpTest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Function());
    }
    IEnumerator Function()
    {
        yield return StartCoroutine(CountTime());
        Debug.Log("主委托等待另一个委托读秒完成");
    }
    IEnumerator CountTime()
    {
        Debug.Log("我是另一个委托,读一秒");
        yield return new WaitForSeconds(1f);
        Debug.Log("另一个委托读秒结束");

    }
}

C# 学习笔记:迭代器:协程_第7张图片

如图,主委托Function直到另一个委托CountTime逻辑全部执行完毕后才接着执行自己的逻辑。

 

参考文章:https://www.iteye.com/blog/dsqiu-2029701

 

你可能感兴趣的:(Unity*C#,C#基础)