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的协程分析以及实现自己的协程_第1张图片

很容易看到,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 eventsGame logicScene renderingGizmo rendering等等都是属于游戏循环中的一部分。
C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程_第2张图片
如果对游戏循环感兴趣,推荐阅读我前面提到过的两本书:
《游戏编程模式》《游戏引擎架构》

3. 协程实现的核心逻辑

那么这个游戏循环跟协程的实现有什么关系呢?
我们把游戏循环迭代器合在一起看:

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

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

三、协程的实现设计

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

1. 协程的实现设计

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

  • 在我们的设计里,以IEnumerator接口为核心,WaitUntilWaitForSeconds两个类均实现接口IEnumerator作为协程进行调用;
  • CoroutineCorotineEngine是我们的核心工具类, 其中Coroutine是协程的包装,CorotineEngine是驱动Coroutine的工具类;
  • CorotineEngine中包含一个StartCoroutine方法,传入一个迭代器,在这个方法中将使用Coroutine对迭代器进行包装,创建一个Corotutine类型的变量并保存在coroutines中;每一次CoroutineUpdate方法被调用,都将驱动Coroutine运行;
  • Coroutine中一个返回值为bool的MoveNext成员函数,当MoveNext的返回值为false时,说明这个协程已经运行结束。

下面是简单的类图

«interface» IEnumerator Current object MoveNext() : bool Reset() : void Coroutine +Name : string -enumerators : Stack Coroutine(IEnumerator) +MoveNext() : bool -MoveNext(IEnumerator it) : bool CorotineEngine coroutines : List +CorotineEngine() +CoroutineUpdate() +Update() +StartCoroutine(IEnumerator) WaitUntil condition : Func +Current : object +MoveNext() : bool +Reset() : void WaitForSeconds +Current object +MoveNext() : bool +Reset() : void Program static Main
2. 协程类的执行逻辑
  • 前面有提到,我们的核心执行工具类是CoroutineCorotineEngine,CorotineEngine接近于Unity中MonoBehaviour的协程部分,它包含开始协程运行的StartCoroutine函数,并返回一个Coroutine实例;
  • 在每一次CorotineEngineUpdate被调用时,所有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的协程分析以及实现自己的协程_第3张图片

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的协程分析以及实现自己的协程_第4张图片

总结

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

你可能感兴趣的:(Unity,简单实现,c#,游戏,协程,迭代器,unity)