天天看点

C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程

文章目录

      • 一、Unity协程简单回顾
      • 二、Unity协程的分析
        • 1. C#迭代器
        • 2 游戏循环
        • 3. 协程实现的核心逻辑
      • 三、协程的实现设计
        • 1. 协程的实现设计
        • 2. 协程类的执行逻辑
        • 3. 迭代器栈在每一次MoveNext的运行流程图
        • 4. 两个简单类的实现思路
      • 四、协程代码实现
        • 1. 辅助类Debug
        • 2. CorotineEngine类
        • 3. Coroutine类
        • 4. WaitForSeconds类
        • 5. WaitUntil 类
        • 7. 测试结果
      • 总结

在前面两篇文章,我们了解了C#迭代器的基础知识,分析了延迟执行的本质,并实现了两个LINQ常用扩展,在这一篇里,我将解析Unity中的协程功能并实现一个自己的协程功能

前置知识回顾:

C#迭代器的实现和应用(一)——基础篇

C#迭代器的实现和应用(二)——延迟执行、流式处理与两个基本LINQ扩展的实现

我在github上存放了一份完整的项目,有需要的也可以研究查看,欢迎各路大佬朋友指正。

git链接

一、Unity协程简单回顾

协程是Unity中非常好用和常用的功能之一,它利用C#的迭代器,在主线程中实现了一个并发的效果,虽然Unity的协程和一些其他语言中提供的协程在使用上存在比较大的差别(但又哪有两个完全一样的协程呢,lua和golang这两个语言原生的协程使用也存在区别)。

我们这里编写几个最简单的协程。

public class CoroutineTest : MonoBehaviour {

	void Start () {
        StartCoroutine(TestNull());
        StartCoroutine(TestWaitUntil());
    }

    bool condition = false;
   IEnumerator TestNull()
    {
        Debug.Log("[" + Time.time + "]\t" + "Test Null 1");
        yield return null;
        Debug.Log("[" + Time.time + "]\t" + "Test Null 2");
        yield return TestWaitForSeconds();

    }
    IEnumerator TestWaitForSeconds()
    {
        Debug.Log("[" + Time.time + "]\t" + "TestWaitForSeconds : start WaitForSeconds");
        yield return new WaitForSeconds(5);
        condition = true;
        Debug.Log("[" + Time.time + "]\t" + "TestWaitForSeconds : stop WaitForSeconds");
        yield return 2;
    }

    IEnumerator TestWaitUntil()
    {
        Debug.Log("[" + Time.time + "]\t" + "start WaitUntil");
        yield return new WaitUntil(() => condition);
        Debug.Log("[" + Time.time + "]\t" + "stop WaitUntil");
    }

           
  • TestNull

    方法中使用

    yield return null

    中断了一次,然后嵌套了

    TestWaitForSeconds

    方法
  • TestWaitForSeconds

    方法中创建了一个

    WaitForSeconds

    的协程,等待五秒,完成之后再将成员变量

    condition

    的值变为true,最后再使用

    yield return 2

    返回一个2
  • TestWaitUntil

    方法创建了一个

    WaitUntil

    协程,当

    condition

    为true时进入下一步
  • Start

    方法是Unity自带的“魔法函数”,在脚本初始化完成后被调用,在这个方法中使用

    StartCoroutine

    方法开启了两个协程。

来查看一下输出结果:

C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程

很容易看到,WaitForSeconds启动后,等待了五秒(注意start WaitForSeconds和stop WaitForSeconds的时间差异),之后

condition

被置为true,WaitUntil也测试通过了。但这个过程并不会导致程序卡住,如果你在

update

方法中同样进行log,会发现

update

方法中的log和协程中的log是同步的,整体的表现得就像是异步进行了这些操作一样——但这些操作的写法又是同步的,甚至连运行都是在主线程进行的。

如果对Unity的协程不熟悉,想了解关于Unity协程的更具体内容和使用方法,可以查看官方手册:协程;

二、Unity协程的分析

应该几乎所有的unity开发者都会使用unity的协程,但它是如何实现的呢?

其实很简单——两个关键知识点:C#迭代器和游戏循环

下面我将逐个拆解实现原理。

1. C#迭代器

因为这是关于C#迭代器的系列文章,所以其实在前两篇文章中我就已经对C#的迭代器特点及应用作出了一些讲解,而Unity协程的实现就依赖了在前两篇文章中所讲的知识:延迟处理和yiled 简化迭代器编写。

Unity的协程有两种形式,一种是以

IEnumerator

为返回值的方法,另一种是继承了

IEnumerator

的类型,

IEnumerator

这个接口我们已经很很熟悉了,说白了,Unity的“协程”本质上就是一个迭代器。

2 游戏循环

游戏循环是几乎所有游戏都存在的核心组件,与普通应用不同的是,在整个游戏过程中,游戏循环是重中之重。

在Robert Nystrom写的《游戏编程模式》一书中就专门有一个讲游戏循环的章节,里面有这样一段话:

假如有哪个模式是本书最无法山羊的,那么非游戏循环模式莫属。游戏循环模式是游戏编程模式中的精髓。几乎所有的游戏中都包含着它,无一雷同,相比而言那些非游戏程序中却难见它的身影。

同样,在Jason Gregory著的《游戏引擎架构》一书中,也专门留出了篇幅对游戏循环进行讲解。

不过吹了这么多,游戏循环到底是个什么东西?

简单来说,就是在游戏启动后的Main函数中运行的一个死循环,每一帧都进行一定时间的暂停,防止进程卡死,类似于下面这样:

while (true)
   {
   	ProcessInput();//检测输入
	Update();//更新画面
	Render();//渲染画面
    Thread.Sleep(17);
   }
           

而我们所有的代码,都在这个循环中反复运行。

机智的小伙伴这时候一定意识到了,这个

Update

其实就是Unity里

MonoBehaviour

类中的

Update

当然,实际的游戏循环要更加复杂。下面这是Unity的

MonoBehaviour

生命周期图中的一部分 (完整周期点击查看),其中

Input events

Game logic

Scene rendering

Gizmo rendering

等等都是属于游戏循环中的一部分。

C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程

如果对游戏循环感兴趣,推荐阅读我前面提到过的两本书:

《游戏编程模式》《游戏引擎架构》

3. 协程实现的核心逻辑

那么这个游戏循环跟协程的实现有什么关系呢?

我们把游戏循环和迭代器合在一起看:

  • 游戏循环是一个具有在每一帧都进行一次调用的死循环
  • C#迭代器是一个需要被多次调用以驱动运行并且获取值的集合
  • C#迭代器中可以封装具体的操作,在每次迭代时被调用
  • C#迭代器可以使用

    yield

    来简化封装操作

有了这些条件,我们就可以做到使用游戏循环来驱动迭代器 , 使用yield来编写函数以自动封装操作,也可以直接编写具体的迭代器,通过传入操作来筛选迭代器的结束条件。

三、协程的实现设计

理论到前面为止,下面我们可以开始实现自己的协程了——如果还没有消化,记得再返回去了解一下迭代器的功能特点哦!

1. 协程的实现设计

关于协程是如何运行的,在前面已经解析了,那么我们来设计一下协程的具体运行方式。

  • 在我们的设计里,以

    IEnumerator

    接口为核心,

    WaitUntil

    WaitForSeconds

    两个类均实现接口

    IEnumerator

    作为协程进行调用;
  • Coroutine

    CorotineEngine

    是我们的核心工具类, 其中

    Coroutine

    是协程的包装,

    CorotineEngine

    是驱动

    Coroutine

    的工具类;
  • CorotineEngine

    中包含一个

    StartCoroutine

    方法,传入一个迭代器,在这个方法中将使用

    Coroutine

    对迭代器进行包装,创建一个

    Corotutine

    类型的变量并保存在

    coroutines

    中;每一次

    CoroutineUpdate

    方法被调用,都将驱动

    Coroutine

    运行;
  • Coroutine

    中一个返回值为bool的

    MoveNext

    成员函数,当

    MoveNext

    的返回值为false时,说明这个协程已经运行结束。

下面是简单的类图

2. 协程类的执行逻辑

  • 前面有提到,我们的核心执行工具类是

    Coroutine

    CorotineEngine

    ,

    CorotineEngine

    接近于Unity中

    MonoBehaviour

    的协程部分,它包含开始协程运行的

    StartCoroutine

    函数,并返回一个

    Coroutine

    实例;
  • 在每一次

    CorotineEngine

    Update

    被调用时,所有

    coroutines

    中的包装器都会被驱动运行一次

    MoveNext

    ,当

    MoveNext

    函数的返回值为false,那么说明这个协程已经运行结束,这个协程将会被从列表中移除;
  • Coroutine

    类通过传入一个迭代器进行创建,每一个

    Coroutine

    实例都包含一个用于保存当前运行的迭代器的栈,在迭代器被驱动运行时有以下几种情况:
    1. 如果当前迭代器栈为空,不包含任何迭代器,那么此迭代器已运行结束,协程的

      MoveNext

      返回

      false

    2. 如果当前迭代器栈最上层的迭代器的

      MoveNext

      返回

      false

      ,那么表明此迭代器已迭代结束,此时将这个迭代器从栈中弹出,再返回第一个判断
    3. 如果当前迭代器栈最上层的迭代器的

      MoveNext

      返回

      true

      ,并且

      Current

      也是一个迭代器,那么将把这个迭代器压入栈,再返回第一个判断,并且此协程的

      MoveNext

      返回

      true

    4. 如果当前迭代器栈最上层的迭代器的

      MoveNext

      返回

      true

      ,并且

      Current

      不是一个迭代器,那么此协程的

      MoveNext

      返回

      true

3. 迭代器栈在每一次MoveNext的运行流程图

C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程

4. 两个简单类的实现思路

  • WaitUntil

    在构造函数中传入一个返回值为bool的lamda表达式并保存,每次调用

    MoveNext

    时运行一次这个lamda表达式,需要注意的是,因为MoveNext为false时表示运行结束,所以要对并返回这个值进行取反。
  • WaitForSeconds

    因为直接使用控制台程序运行 ,没有Unity自带的Time类,所以我使用一c#的

    DateTime

    进行计时;

    WaitUntil

    思路很简单,在创建时传入一个以秒为单位的时间,同时使用

    DateTime.Now.AddSeconds

    来计算结束时间,之后在

    MoveNext

    中将当前时间与结束时间对比,如果当前时间超过了结束时间,那么MoveNext返回false,表示迭代结束;

    在迭代器中current仅用于判断是否需要压栈,所以随意返回一个引用类型的值即可——建议不要返回一个值类型,因为会造成一次拆装箱损耗。

解析到此结束,后面就是代码啦。

四、协程代码实现

1. 辅助类Debug

为了方便查看时间,设计了Debug类用于Log

class Debug
    {
        public static void Log(string source, string format = "", params object[] args)
        {
            var f = string.Format("[{0}]\t[{1}] : {2}", DateTime.Now.ToString("HH:mm:ss fff"), source, format);
            Console.WriteLine(f, args);
        }
    }
           

2. CorotineEngine类

我们的简单引擎,有用于保存协程的List、用于更新的Update方法、用于驱动协程的UpdateCoroutine方法、添加协程的StartCoroutine方法,简单来说,可以把这个类看成MonoBehaviour中关于协程的那一部分。

class CoroutineEngine
    {
        public CoroutineEngine()
        {
            Debug.Log("CoroutineEngine", "setup");
        }
        List<Coroutine> coroutines = new List<Coroutine>();
        public void Update()
        {
            //Debug.Log("CoroutineEngine", "Update");
        }

        public void CoroutineUpdate()
        {
            for (int i = 0; i < coroutines.Count; i++)
            {
                if (coroutines[i].MoveNext())
                {
                    //Debug.Log("CoroutineEngine", "CoroutineUpdate");
                }
                else
                {
                    Debug.Log("CoroutineEngine", "remove corotine : " + coroutines[i].Name);
                    coroutines.RemoveAt(i);
                    i--;
                }
            }
        }

        public void StartCoroutine(IEnumerator coroutine)
        {
            coroutines.Add(new Coroutine(coroutine));
        }
    }
           

3. Coroutine类

协程类,每一个被创建的协程都会使用一个Coroutine包装起来,只要反复调用Coroutine中的MoveNext方法就可以驱动迭代器进行迭代,这个类的MoveNext逻辑相对有一点复杂,我使用递归方式来实现,如果一下不理解,可以再回到前面查看它的运行图。

class Coroutine
    {
        public string Name { get; private set; }
        public Coroutine(IEnumerator enumerator)
        {
            Name = enumerator.GetType().Name;
            enumerators.Push(enumerator);
        }

        Stack<IEnumerator> enumerators = new Stack<IEnumerator>();
        public bool MoveNext()
        {
            if (enumerators.Count == 0) return false;
            return MoveNext(enumerators.Peek());
        }

        private bool MoveNext(IEnumerator it)
        {
            if(it.MoveNext())
            {
                if(it.Current is IEnumerator)
                {
                    var next = it.Current as IEnumerator;
                    enumerators.Push(next);
                    MoveNext(next);
                }
                return true;
            }
            else
            {
                enumerators.Pop();
                if (enumerators.Count == 0) return false;
                return false || MoveNext(enumerators.Peek());
            }
        }
    }

           

4. WaitForSeconds类

class WaitForSeconds : IEnumerator
    {
        private long targetTicks;
        public WaitForSeconds(float seconds)
        {
            this.targetTicks = DateTime.Now.AddSeconds(seconds).Ticks;
        }
        
        public object Current => null;

        public bool MoveNext()
        {
            return this.targetTicks > DateTime.Now.Ticks;
        }

        public void Reset()
        {
        }
    }
           

5. WaitUntil 类

class WaitUntil : IEnumerator
    {
        Func<bool> condition;
        public WaitUntil(Func<bool> condition)
        {
            if (null == condition) throw new ArgumentNullException("WaitUntil condition is null");
            this.condition = condition;
        }
        public object Current => null;

        public bool MoveNext()
        {
            return !condition();
        }

        public void Reset()
        {
            
        }
    }
           
  1. 测试类
class Program
    {
        static void Main(string[] args)
        {
            var engine = new CoroutineEngine();
            engine.StartCoroutine(TestNull());
            engine.StartCoroutine(TestWaitUntil());
            while (true)//主循环
            {
                engine.Update();
                engine.CoroutineUpdate();
                Thread.Sleep(33);
            }
        }

        static bool condition = false;
        static IEnumerator TestNull()
        {
            Debug.Log("Test Null", "1");
            yield return null;
            Debug.Log("Test Null", "2");
            yield return TestWaitForSeconds();

        }
        static IEnumerator TestWaitForSeconds()
        {
            Debug.Log("TestWaitForSeconds", "start WaitForSeconds");
            yield return new WaitForSeconds(5);
            condition = true;
            Debug.Log("TestWaitForSeconds", "stop WaitForSeconds");
            yield return 2;
        }

        static IEnumerator TestWaitUntil()
        {
            Debug.Log("TestWaitUntil", "start WaitUntil");
            yield return new WaitUntil(() => condition);
            Debug.Log("TestWaitUntil", "stop WaitUntil");
        }
    }
           

7. 测试结果

C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程

总结

以上就是关于C#迭代器的扩展的所有内容了,这一系列文章写了我很久,主要还是因为准备不足,也对工作量没有概念,好在有番茄工作法的帮助,一点一点完成了这三篇文章,以后还是要继续努力。

继续阅读