16.协同程序原理

16.MonoBehavior中的重要内容-协同程序原理


16.1 知识点

协程的本质

  • 协程可以分成两部分
    • 协程函数本体

    • 协程调度器

    • 协程本体就是一个能够中间暂停返回的函数

    • 协程调度器是Unity内部实现的,会在对应的时机帮助我们继续执行协程函数

    • Unity只实现了协程调度部分

    • 协程的本体本质上就是一个 C#的迭代器方法

    • 通俗理解

      • 协程函数本体就是我们自己写的协程函数 这个要我们自己写逻辑
      • 我们写的协程函数实际上是个迭代器方法
      • 我们把我们写的协程函数丢到 MonoBehaviour中开启协程的方法 StartCoroutine 中
      • 可以理解为 就是把一个迭代器方法的返回值 返回给Unity内部的一个协程管理器里面存储起来
      • 通过协程管理器对应着我们写协程函数的逻辑和啥时候yield return 能在对应时机帮我们继续调用协程函数

协程本体是迭代器方法的体现

测试类

public class TestClass
{
    public int testNum;
    public TestClass(int testNum)
    {
        this.testNum = testNum;
    }
}

声明迭代器函数

    //测试的迭代器(协程)函数
    IEnumerator Test()
    {
        print("第一次执行");
        yield return 1;
        print("第二次执行");
        yield return 999;
        print("第三次执行");
        yield return "666";
        print("第四次执行");
        yield return new TestClass(250);
    }

协程函数本体

//如果我们不通过 开启协程方法执行协程 直接调用协程的话 没有作用
Test();//没有作用
//Unity的协程调度器是不会帮助我们管理协程函数的 我们可以用一个迭代器变量把我们的协程函数装起来
IEnumerator ieTest = Test();

////但是我们可以自己执行手动迭代器函数内容
//ieTest.MoveNext();//第一次执行
////会执行函数中内容遇到 yield return为止的逻辑

//print(ieTest.Current);//1
////得到 yield return 返回的内容 这个Current是Object类型

//ieTest.MoveNext();//第二次执行
//print(ieTest.Current);//999

//ieTest.MoveNext();//第三次执行
//print(ieTest.Current);//666

//ieTest.MoveNext();//第四次执行
//TestClass tc = ieTest.Current as TestClass;//可以把Current对象as成对应的类
//print(tc.testNum);//250

////MonoBehaviour中开启协程的方法StartCoroutine
////其实就是得到我们的迭代器对象 再根据Unity自己的规则一步一步的执行协程函数

////像上面这样一步一步写其实不太好 写把上面的注释了 用while逐个执行

//MoveNext 代表执行下一个yield return之前的逻辑 并且有返回值 返回值代表着字符迭代器是否到了结尾(这个迭代器函数 是否执行完毕)
while (ieTest.MoveNext())
{
    print(ieTest.Current);
}

协程调度器

  • 继承MonoBehavior后 开启协程

    • 相当于是把一个协程函数(迭代器)放入Unity的协程调度器中帮助我们管理进行执行
    • 具体的yield return 后面的规则 也是Unity定义的一些规则
  • 协程本体是迭代器方法的体现总结

    • 你可以简化理解迭代器函数
    • C#看到迭代器函数和yield return语法糖
    • 就会把原本是一个的函数变成”几部分”
    • 我们可以通过迭代器 从上到下遍历这 “几部分” 进行执行
    • 就达到了将一个函数中的逻辑分时执行的目的
  • 而协程调度器就是 利用迭代器函数返回的内容来进行之后的处理

    • 比如Unity中的协程调度器
    • 根据yield return返回的内容 决定了下一次在何时继续执行迭代器函数中的”下一部分”
  • 理论上来说 我们可以利用迭代器函数的特点 自己实现协程调度器来取代Unity自带的调度器

总结

  • 协程的本质 就是利用
    • C#的迭代器函数”分步执行”的特点
    • 加上 Unity自己定义的一些协程调度逻辑
    • 实现的一套分时执行函数的规则

16.2 知识点代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//测试类
public class TestClass
{
    public int testNum;
    public TestClass(int testNum)
    {
        this.testNum = testNum;
    }
}

public class Lesson16_MonoBehavior中的重要内容_协同程序原理 : MonoBehaviour
{
    //测试的迭代器(协程)函数
    IEnumerator Test()
    {
        print("第一次执行");
        yield return 1;
        print("第二次执行");
        yield return 999;
        print("第三次执行");
        yield return "666";
        print("第四次执行");
        yield return new TestClass(250);
    }

    void Start()
    {
        #region 知识点一 协程的本质
        //协程可以分成两部分
        //1.协程函数本体
        //2.协程调度器

        //协程本体就是一个能够中间暂停返回的函数
        //协程调度器是Unity内部实现的,会在对应的时机帮助我们继续执行协程函数

        //Unity只实现了协程调度部分
        //协程的本体本质上就是一个 C#的迭代器方法

        //通俗理解
        //协程函数本体就是我们自己写的协程函数 这个要我们自己写逻辑
        //我们写的协程函数实际上是个迭代器方法
        //我们把我们写的协程函数丢到 MonoBehaviour中开启协程的方法 StartCoroutine 中
        //可以理解为 就是把一个迭代器方法的返回值 返回给Unity内部的一个协程管理器里面存储起来
        //通过协程管理器对应着我们写协程函数的逻辑和啥时候yield return 能在对应时机帮我们继续调用协程函数

        #endregion

        #region 知识点二 协程本体是迭代器方法的体现
        //1.协程函数本体

        //如果我们不通过 开启协程方法执行协程 直接调用协程的话 没有作用
        Test();//没有作用
        //Unity的协程调度器是不会帮助我们管理协程函数的 我们可以用一个迭代器变量把我们的协程函数装起来
        IEnumerator ieTest = Test();

        ////但是我们可以自己执行手动迭代器函数内容
        //ieTest.MoveNext();//第一次执行
        ////会执行函数中内容遇到 yield return为止的逻辑

        //print(ieTest.Current);//1
        ////得到 yield return 返回的内容 这个Current是Object类型

        //ieTest.MoveNext();//第二次执行
        //print(ieTest.Current);//999

        //ieTest.MoveNext();//第三次执行
        //print(ieTest.Current);//666

        //ieTest.MoveNext();//第四次执行
        //TestClass tc = ieTest.Current as TestClass;//可以把Current对象as成对应的类
        //print(tc.testNum);//250

        ////MonoBehaviour中开启协程的方法StartCoroutine
        ////其实就是得到我们的迭代器对象 再根据Unity自己的规则一步一步的执行协程函数

        ////像上面这样一步一步写其实不太好 写把上面的注释了 用while逐个执行

        //MoveNext 代表执行下一个yield return之前的逻辑 并且有返回值 返回值代表着字符迭代器是否到了结尾(这个迭代器函数 是否执行完毕)
        while (ieTest.MoveNext())
        {
            print(ieTest.Current);
        }

        //2.协程调度器
        //继承MonoBehavior后 开启协程
        //相当于是把一个协程函数(迭代器)放入Unity的协程调度器中帮助我们管理进行执行
        //具体的yield return 后面的规则 也是Unity定义的一些规则

        //协程本体是迭代器方法的体现总结
        //你可以简化理解迭代器函数
        //C#看到迭代器函数和yield return 语法糖
        //就会把原本是一个的 函数 变成"几部分"
        //我们可以通过迭代器 从上到下遍历这 "几部分"进行执行
        //就达到了将一个函数中的逻辑分时执行的目的

        //而协程调度器就是 利用迭代器函数返回的内容来进行之后的处理
        //比如Unity中的协程调度器
        //根据yield return 返回的内容 决定了下一次在何时继续执行迭代器函数中的"下一部分"

        //理论上来说 我们可以利用迭代器函数的特点 自己实现协程调度器来取代Unity自带的调度器
        #endregion

        #region 总结
        //协程的本质 就是利用 
        //C#的迭代器函数"分步执行"的特点
        //加上 Unity自己定义的一些协程调度逻辑
        //实现的一套分时执行函数的规则
        #endregion
    }
}

16.3 练习题

请不使用Unity自带的协程协调器开启协程,通过迭代器函数实现每隔一秒执行函数中的一部分逻辑

声明一个协程函数

IEnumerator MyTestCoroutine()
{
    print("1");
    // 如果是用Unity自带的协程协调器开启协程 那么数字代表等待1帧继续执行之后的内容
    // 如果是自己的协程协调器可以自己自定义规则
    yield return 1;
    print("2");
    yield return 2;
    print("3");
    yield return 3;
    print("4");
}

声明用于记录当前协程函数和返回时间的类

//用于记录当前协程函数和返回时间的类
public class NowIEnumeratorAndYieldReturnTime
{
    //记录 下次还要执行的 迭代器接口
    public IEnumerator ie;
    //记录 下次执行的时间点
    public float time;
}

声明自己写的协程管理器,搞成单例,创建开启协程方法,声明用于记录当前协程函数和返回时间的类,在声明这个类的容器,最后实现Update里判断时间的逻辑。

//我的协程管理器
public class MyCoroutineMgr : MonoBehaviour
{
    private static MyCoroutineMgr instance;
    public static MyCoroutineMgr Instance => instance;

    //申明存储 迭代器函数对象的 容器 用于 一会继续执行
    private List<NowIEnumeratorAndYieldReturnTime> list = new List<NowIEnumeratorAndYieldReturnTime>();
    
    void Awake()
    {
        instance = this;
    }

    //我的开启协程的方法
    public void MyStartCoroutine(IEnumerator ie)
    {
        //来进行 分步走 分时间执行的逻辑

        //传入一个 迭代器函数返回的接口 那么应该一来就执行它
        //一来就先执行第一步 执行完了 如果返回的true 证明 后面还有步骤
        if(ie.MoveNext())
        {
            //判断 如果yield return返回的是 数字 是一个int类型 那就证明 是需要等待n秒继续执行
            if(ie.Current is int)
            {
                //进来这里说明yield return返回已经是int类型了 按我们自己的规则要过一会执行协程
                //按思路 应该把 这个迭代器函数 和它下一次执行的时间点 记录下来
                //然后不停检测 时间 是否到达了 下一次执行的 时间点 然后就继续执行它
                //这样我们不妨声明一个类
                //这个类的数据结构是有一个IEnumerator变量用于存储协程函数和float变量用于存储协程要执行的时间

                //创建协程存储类
                NowIEnumeratorAndYieldReturnTime nowIEAndTime = new NowIEnumeratorAndYieldReturnTime();

                //记录迭代器接口
                nowIEAndTime.ie = ie;

                //记录迭代器要执行的时间
                nowIEAndTime.time = Time.time + (int)ie.Current;

                //把记录的信息 记录到数据容器当中 因为可能有多个协程函数 开启 所以 用一个 list来存储
                list.Add(nowIEAndTime);
            }
        }
    }

    void Update()
    {
        //为了避免在循环的时候 从列表里面移除内容 我们可以倒着遍历
        for (int i = list.Count - 1; i >= 0; i--)
        {
            //判断 当前该迭代器函数 是否到了下一次要执行的时间
            //如果到了 就需要执行下一步了
            if( list[i].time <= Time.time  )
            {
                //判断还能不能往后继续执行
                //注意:只要调用了MoveNext其实就执行了yield return之前的逻辑 只不过假如没有yield return就返回false
                if (list[i].ie.MoveNext())
                {
                    //如果是true 那还需要对该迭代器函数 进行处理
                    //如果是 int类型 证明是按秒等待
                    if(list[i].ie.Current is int)
                    {
                        list[i].time = Time.time + (int)list[i].ie.Current;
                    }

                    //如果不是int类型 是别的类型 可以自己加if else 判断是啥类型 可能要声明新的容器存进去 这里就不写了
                    else
                    {
                        //该list 只是存储 处理时间相关 等待逻辑的 迭代器函数的 
                        //如果是别的类型 就不应该 存在这个list中 应该根据类型把它放入别的容器中
                        list.RemoveAt(i);
                    }
                }
                else
                {
                    //后面已经没有可以等待和执行的了 证明已经执行完毕了逻辑
                    list.RemoveAt(i);
                }
            }
        }
    }


}

16.4 练习题代码

Lesson16_练习题

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson16_练习题 : MonoBehaviour
{
    #region 练习题一
    //请不使用Unity自带的协程协调器开启协程,通过迭代器函数实现每隔一秒执行函数中的一部分逻辑

    void Start()
    {
        //Unity自带的协程协调器 开启协程函数(迭代器函数)
        //StartCoroutine(MyTestCoroutine());

        //我们自己写协程协调器 开启协程函数(迭代器函数)
        MyCoroutineMgr.Instance.MyStartCoroutine(MyTestCoroutine());
    }

    IEnumerator MyTestCoroutine()
    {
        print("1");
        //如果是用Unity自带的协程协调器开启协程 那么数字代表等待1帧继续执行之后的内容
        //如果是自己的协程协调器可以自己自定义规则
        yield return 1;
        print("2");
        yield return 2;
        print("3");
        yield return 3;
        print("4");
    }

    #endregion
}

MyCoroutineMgr

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//用于记录当前协程函数和返回时间的类
public class NowIEnumeratorAndYieldReturnTime
{
    //记录 下次还要执行的 迭代器接口
    public IEnumerator ie;
    //记录 下次执行的时间点
    public float time;
}

//我的协程管理器
public class MyCoroutineMgr : MonoBehaviour
{
    private static MyCoroutineMgr instance;
    public static MyCoroutineMgr Instance => instance;

    //申明存储 迭代器函数对象的 容器 用于 一会继续执行
    private List<NowIEnumeratorAndYieldReturnTime> list = new List<NowIEnumeratorAndYieldReturnTime>();
    
    void Awake()
    {
        instance = this;
    }

    //我的开启协程的方法
    public void MyStartCoroutine(IEnumerator ie)
    {
        //来进行 分步走 分时间执行的逻辑

        //传入一个 迭代器函数返回的接口 那么应该一来就执行它
        //一来就先执行第一步 执行完了 如果返回的true 证明 后面还有步骤
        if(ie.MoveNext())
        {
            //判断 如果yield return返回的是 数字 是一个int类型 那就证明 是需要等待n秒继续执行
            if(ie.Current is int)
            {
                //进来这里说明yield return返回已经是int类型了 按我们自己的规则要过一会执行协程
                //按思路 应该把 这个迭代器函数 和它下一次执行的时间点 记录下来
                //然后不停检测 时间 是否到达了 下一次执行的 时间点 然后就继续执行它
                //这样我们不妨声明一个类
                //这个类的数据结构是有一个IEnumerator变量用于存储协程函数和float变量用于存储协程要执行的时间

                //创建协程存储类
                NowIEnumeratorAndYieldReturnTime nowIEAndTime = new NowIEnumeratorAndYieldReturnTime();

                //记录迭代器接口
                nowIEAndTime.ie = ie;

                //记录迭代器要执行的时间
                nowIEAndTime.time = Time.time + (int)ie.Current;

                //把记录的信息 记录到数据容器当中 因为可能有多个协程函数 开启 所以 用一个 list来存储
                list.Add(nowIEAndTime);
            }
        }
    }

    void Update()
    {
        //为了避免在循环的时候 从列表里面移除内容 我们可以倒着遍历
        for (int i = list.Count - 1; i >= 0; i--)
        {
            //判断 当前该迭代器函数 是否到了下一次要执行的时间
            //如果到了 就需要执行下一步了
            if( list[i].time <= Time.time  )
            {
                //判断还能不能往后继续执行
                //注意:只要调用了MoveNext其实就执行了yield return之前的逻辑 只不过假如没有yield return就返回false
                if (list[i].ie.MoveNext())
                {
                    //如果是true 那还需要对该迭代器函数 进行处理
                    //如果是 int类型 证明是按秒等待
                    if(list[i].ie.Current is int)
                    {
                        list[i].time = Time.time + (int)list[i].ie.Current;
                    }

                    //如果不是int类型 是别的类型 可以自己加if else 判断是啥类型 可能要声明新的容器存进去 这里就不写了
                    else
                    {
                        //该list 只是存储 处理时间相关 等待逻辑的 迭代器函数的 
                        //如果是别的类型 就不应该 存在这个list中 应该根据类型把它放入别的容器中
                        list.RemoveAt(i);
                    }
                }
                else
                {
                    //后面已经没有可以等待和执行的了 证明已经执行完毕了逻辑
                    list.RemoveAt(i);
                }
            }
        }
    }


}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏