赞
踩
推荐阅读
大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。
最近有小伙伴问协程怎么用、怎么写,我也是会用会写,但是原理不是很明白。
学习了一下,总结出来分享给看到这篇文章的人。
如果觉得本篇文章有用别忘了点个关注,关注不迷路,持续分享更多Unity干货文章。
协程就相当于C#的线程。
Unity3D是支持多线程的,只是线程不能访问主线程中的对象,虽然说线程不能访问主线程中的对象,但是可以将一些复杂的算法计算、网络连接等逻辑抛给一个线程去处理,将处理的数据放到公共的内存模块中。
Unity3D主线程就可以使用了。
那么协程是什么呢,协程就是Unity针对上面的问题提出的解决方案,协程又叫做协同程序,使用的场景主要有资源、场景的异步加载,但是可以访问主线程中的对象。
协程的本质是迭代器,能够暂停协程执行,暂停后立即返回主函数,执行主函数剩余的部分,直到中断执行完成后,从中断指令的下一行继续执行协程剩余的函数,函数全部执行完成,协程结束。由于中断执行的出现,可以将一个函数分割成多个帧中去执行。
画了一个图来说明:
看起来没有什么难的,接下来就来解析一下执行顺序以及协程原理。
执行顺序:
协程中的所有初始代码,从协程开始到中断执行的位置,可以中断,协程代码中的其他部分,也就是中断执行后面的代码将出现在Unity主循环DelayeCallManager
中。
协程由 C# 编译器自动生成的类实例提供支持。
此对象用于跟踪单个方法的多次调用之间的协程状态。
因为协程中的局部作用域变量必须在 yield 调用中保持一致,所以这些局部作用域变量将被保存到上一级的生成的它们的类中,从而保证在协程的存活期内保留在堆上的地址分配。
该对象还会跟踪协程的内部状态:它会记住协程暂停后必须从代码中的哪一点恢复。
因此,启动协程引起的内存压力等于固定开销成本加上其局部变量的消耗。
启动协程的代码将构造并调用此对象,然后 Unity 的DelayedCallManager
在每当满足协程的暂停条件时再次调用此对象。
由于协程通常在其他协程之外启动,因此它们的执行成本将分担到上述两个位置,这两个位置又叫做协程函数和协程调度器。
执行原理:
协程函数将执行成本分给了协程函数
和协程调度器
,协程函数
使用的是C#的迭代器,协程调度器
则使用了MonoBehaviour中的生命周期函数来实现。
协程函数
实现了分步,协程调度器
实现了分时。
再来了解一下迭代器:
迭代器中有一个MoveNext函数,协程函数
实现了迭代器,那么协同程序就是一步步的执行迭代器对象中国男的MoveNext函数,调用MoveNext函数会执行下一个yield return之前的逻辑,并且根据MoveNext()的返回值判断是否全部执行完毕。
而yield return通过返回Current对象,来判断执行MoveNext()的时机,这部分的工作就是通过协程的另一个部分,也就是协程调度器
来实现的,协程调度器
是Unity引擎实现的,理论上我们可以自己去实现一个协程调度器,感兴趣的可以自己实现一个,能进一步加深对协程的理解。
我们已经清楚了协程的原理,以及协程的组成部分,也就是协程函数
和协程调度器
,我们可以试着去实现一个,来加深对协程的理解:
using System; using System.Collection; using System.Collection.Gernic; using UnityEngine; public class YieldInstruction { public IEnumerator ie; public float executeTime; } public class CoroutineMgr : MonoBehaviour { private List<YieldInstruction> list = new List<YieldInstruction>(); public void StartCoroutine(IEnumerator ie) { ie.MoveNext(); if((ie.Current is null) || (ie.Current is int)) { list.Add(new YieldInstruction{ ie=ie,executeTime=0; }); } else if(ie.Current is WaitForSeconds) { list.Add(new YieldInstruction{ ie=ie, executeTime=Time.time+(ie.Currentas WaitForSeconds).second }); } else if (...) {...} } void Update() { // 倒序遍历方便移除 for(int i=list.Count-1; i>=0; i--) { if(list[i].executeTime<=Time.time) { if(list[i].ie.MoveNext()) { // 如果是已定义的类型 if((ie.Current is null) || (ie.Current is int)) || (ie.Current is WaitForSeconds)) { // 继续指定执行时机 } else { list.RemoveAt(i); } } else { list.RemoveAt(i); } } } } }
实现的代码主体就是这样了,当然还有一些GC回收没有做,感兴趣的可以继续优化。
首先,来看一个简单的程序:
using System.Collections; using UnityEngine; public class TestCoroutine : MonoBehaviour { void Start() { Debug.Log("在协程之前执行函数"); StartCoroutine(m_Ien()); Debug.Log("在协程之后执行函数"); } IEnumerator m_Ien() { Debug.Log("执行函数"); yield return new WaitForSeconds(1);//等待1秒 Debug.Log("执行后面的函数"); } }
运行结果:
PS:这个例子演示了,协程的执行顺序,协程的写法,协程的调用
执行顺序:
(1)执行协程之前的代码
(2)执行协程中的协程函数直到中断程序
(3)执行协程之后的代码
(4)中断程序结束执行后面的协程函数
协程写法:
(1)声明是IEnumerator 迭代器类型返回值
(2)返回值为yield return new,也就是中断程序,就跟int的返回值是0123一样,没有返回值会报错
(3)执行中断程序后面的函数
协程的yield return new返回值:
返回值 | 介绍 |
---|---|
yield return null; yield retun x(x代表任意数字) | 下一帧再执行后续代码 |
yield break; | 结束该协程 |
yield return new WaitForSeconds(0.3f); | 等待固定时间执行后续代码 |
yield return FunctionName(); | 函数执行完毕后执行后续代码 |
yield return AsyncOperation; | 异步执行完毕后执行后续代码 |
yield return Coroutine; | 协程执行完毕后执行后续代码 |
yield return new WaitForEndOfFrame(); | 帧渲染完成后执行后续代码 |
yield return new WaitForFixedUpdate(); | 物理帧更新后执行后续代码 |
yield return new WaitUntil(arg); | 参数为true时执行后续代码 |
协程的调用:
(1)调用协程使用StartCoroutine(m_Ien());
(2)调用协程还可以这么写StartCoroutine(“m_Ien”);
(3)终止协程用StopCoroutine(m_Ien());
(4)终止协程还可以这么写StopCoroutine(“m_Ien”);
(5)声明协程再终止协程,mCoroutine= StartCoroutine(m_Ien());StopCoroutine(mCoroutine);
(6)终止所有协程StopAllCoroutines();
协程的所能达到的效果就是在指定的时间点上执行需要执行的代码,Unity中开始一个协程的函数是StartCoroutine,而提供的延迟的类有以下几种分别是:
new WaitForEndOfFrame; //等待一帧
new WaitForFixedUpdate; //等待一个FixedUpdate(固定时间间隔)
new WaitForSeconds; //等待X秒
new WWW; //等待外部资源加载完毕
本节就针对其中的WaitForSeconds实现进行探究。
因为在开发过程中,很多时候会遇到一种情况就是,超时或者是符合某种条件就继续运行,使用系统提供WaitForSeconds已经无法满足要求了。
这时候有两种解决方法,一种是使用StopCoroutine来停止协程,但是对于Unity来说,这种行为会造成很大的开销;第二种就是可以采用重写WaitForSeconds,使它能达到我们的要求。
以下是重写WaitForSeconds的代码:
/// <summary> /// 任务扩展 /// </summary> static class CTaskExtend { static public IEnumerator WaitForSeconds(float second) { DateTime init_dt = DateTime.Now; TimeSpan time; while (true) { time = DateTime.Now - init_dt; if (time.TotalSeconds <= second) { yield return null; } else { break; } } } }
调用的方法与Unity差不多:
yield return CTaskExtend.WaitForSeconds(delayTime);
那么如果遇到之前说的那一种情况(超时或者是符合某种条件就继续运行),这里需要做怎么样的改动呢?如下:
/// <summary> /// 任务扩展 /// </summary> static class CTaskExtend { public delegate bool CondDelegate(); static public IEnumerator WaitForSeconds(float second, CondDelegate cond = null) { DateTime init_dt = DateTime.Now; TimeSpan time; while (true) { time = DateTime.Now - init_dt; if (time.TotalSeconds <= second && !cond()) { yield return null; } else { break; } } } }
加上了一个回调函数,每次都会检查这个函数是否为true,如果为true则停止等待。
我发现协程的返回值有这个:
yield return Coroutine;//协程执行完毕后执行后续代码
也就是协程类型的返回值,我在想,是不是可以通过扩展Coroutine,来写一个自定义的中断指令,也就是yield new return。
我们假设这样一种情况,当一个动画播放后,再执行其他函数。
参考代码如下:
using UnityEngine; using System.Collections; public class WaitForEndOfAnim : IEnumerator { AnimationState m_animState; public WaitForEndOfAnim(AnimationState animState) { m_animState = animState; } //-- IEnumerator Interface public object Current { get { return null; } } //-- IEnumerator Interface public bool MoveNext() { return m_animState.enabled; } //-- IEnumerator Interface public void Reset() { } }
这里面核心的逻辑就在“MoveNext”函数中,我通过m_animState.enabled来判断动画是否播放完了。
完整的测试代码如下:
using UnityEngine; using System.Collections; public class UnitTest : MonoBehaviour { // Use this for initialization void Start() { } void OnGUI() { GUILayout.BeginArea(new Rect(6, 6, 200, 300)); GUILayout.BeginVertical(); GUILayout.Box("Conrountinue测试"); if (GUILayout.Button("启动")) { StartCoroutine(DoTest()); } GUILayout.EndVertical(); GUILayout.EndArea(); } IEnumerator DoTest() { Animation anim = GetComponentInChildren<Animation>(); AnimationState animAttack = anim["attack"]; animAttack.speed = 0.1f; AnimationState animHit = anim["hit"]; animHit.speed = 0.1f; AnimationState animDie = anim["die"]; animDie.speed = 0.1f; Debug.Log("1.开始播放攻击动画。" + Time.time * 1000); anim.Play(animAttack.name); yield return StartCoroutine(new WaitForEndOfAnim(animAttack)); Debug.Log("2.开始播放受击动画。" + Time.time * 1000); anim.Play(animHit.name); yield return StartCoroutine(new WaitForEndOfAnim(animHit)); Debug.Log("3.开始播放死亡动画。" + Time.time * 1000); anim.Play(animDie.name); yield return StartCoroutine(new WaitForEndOfAnim(animDie)); } }
这篇文章详细讲解了Unity3D的协程的原理以及使用。
以及重写协程程序的返回值和自定义协程返回值。
对于某些代码来说难度比较高,推荐多理解多练习。
你的点赞就是对博主的支持,有问题记得留言:
博主主页有联系方式。
博主还有跟多宝藏文章等待你的发掘哦:
专栏 | 方向 | 简介 |
---|---|---|
Unity3D开发小游戏 | 小游戏开发教程 | 分享一些使用Unity3D引擎开发的小游戏,分享一些制作小游戏的教程。 |
Unity3D从入门到进阶 | 入门 | 从自学Unity中获取灵感,总结从零开始学习Unity的路线,有C#和Unity的知识。 |
Unity3D之UGUI | UGUI | Unity的UI系统UGUI全解析,从UGUI的基础控件开始讲起,然后将UGUI的原理,UGUI的使用全面教学。 |
Unity3D之读取数据 | 文件读取 | 使用Unity3D读取txt文档、json文档、xml文档、csv文档、Excel文档。 |
Unity3D之数据集合 | 数据集合 | 数组集合:数组、List、字典、堆栈、链表等数据集合知识分享。 |
Unity3D之VR/AR(虚拟仿真)开发 | 虚拟仿真 | 总结博主工作常见的虚拟仿真需求进行案例讲解。 |
Unity3D之插件 | 插件 | 主要分享在Unity开发中用到的一些插件使用方法,插件介绍等 |
Unity3D之日常开发 | 日常记录 | 主要是博主日常开发中用到的,用到的方法技巧,开发思路,代码分享等 |
Unity3D之日常BUG | 日常记录 | 记录在使用Unity3D编辑器开发项目过程中,遇到的BUG和坑,让后来人可以有些参考。 |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。