Unity 协程详解
在程序开发时,光是了解协程怎么用是远远不够的,因为当程序出现一些有关于协程的错误时,理解协程的原理就十分有必要性了。
1.协程使用的一些问题
我们知道如果在Unity中编写一个死循环,会造成运行游戏时整个Unity编辑器卡死,而协程函数在使用时好像是可以与Update函数并行不斥的,那如果在一个协程函数里面编写一个死循环会怎么用呢?答案也是程序会卡死。
所以,我们知道了,协程函数并不是一个独立的执行单元,它与Update函数一样,被Unity依次执行。一旦有一个发生了死循环,游戏将会卡死。简而言之,协程不是线程。
从上面可以看到,协程非常像一个自定义的Update函数,只不过内部含有延时逻辑。而有意思的是,Mono的生命周期函数例如Start和Update是可以支持协程调用的,只是不需要使用StartCoroutine就可以直接调用。
public class Coroutine : MonoBehavior{ IEnumerator Start(){ Debug.log(1); yield return new WaitForSeconds(1); Debug.log(2); yield return new WaitForSeconds(1); } }
上述的Start方法并不是主动调用的,而是被Unity引擎识别并调用的,这里把Start的返回值void改成了IEnumerator,也同样被Unity识别了。
2.迭代器
虽然协程是Unity提供的,但是IEnumrator则是C#的语法,所以我们要弄清楚协程就先要知道这两个玩意的机制。事实上,IEnumrator与yield配合实现了一种叫做“可重入函数”的机制,也就是函数可以被打断一会,之后再执行的机制。
public class Itr:MonoBehaviour{ IEnumerator<int> HelloWorld(){ transform.position = new Vector3(1,0,0); yield return 233; transform.position = new Vector3(2,0,0); yeild return 666; } void Start(){ IEnumerator<int> e = HelloWorld(); while(){ if(!e.MoveNext()){ break; }else{ Debug.log("yield 返回值"+e.Current); Debug.log("当前位置"+transform.position); } } } }
每一个迭代器对象一共就三个属性,Current,MoveNext,Reset。
这里HelloWorld就是一个可重入函数,在初次执行时会在第一个yield处卡住,返回值是一个IEnumrator对象,之后程序会把他保存在e变量中。每次调用e.MoveNext()方法时会让函数继续执行到下一个yield处,执行到最后一个yield后,函数彻底执行完毕,并且返回false;
每次执行一步,还可以从变量e中获取到中断时的返回值,即e.Current。这个返回值将非常有用。
yield在生命周期的运行时机:我们知道要使用协程的脚本必须先继承自MonoBehaviour。其中一个原因就是在协程的yield也在Mono生命周期中,且在Update与LatedUpdate之间。以下是常用的
yield return null; 暂停协程等待下一帧继续执行
yield return 0或其他数字; 暂停协程等待下一帧继续执行
yield return new WairForSeconds(时间); 等待规定时间后继续执行
yield return StartCoroutine("协程方法名");开启一个协程(嵌套协程)
yield return new WaitForFixedUpdate()
:等到下一个固定帧数更新
yield return new WaitForEndOfFrame()
:等到所有相机画面被渲染完毕后更新
3.协程的延时执行原理
以下是一个与Unity类似的实现。主要就是实现一个WaitForSeconds来实现延时执行。WaitForSeconds内部其实也是一个计时器的功能。所有的协程都由CoroutineManager进行管理,只有当计时结束才会执行MoveNext,协程内部才会向后继续运行。
IEnumerator IMyWaitForSecond(float time) { Debug.Log("Wait Start"); yield return new MyWaitForSeconds(time); Debug.Log("Wait Stop"); } public interface IWait { bool Tick(); } public class MyWaitForSeconds : IWait { float _seconds = 0f; public MyWaitForSeconds(float seconds) { _seconds = seconds; } public bool Tick() { _second -= Time.deltaTime; return _seconds<=0; } } public class CoroutineManager { private static CoroutineManager _instance = null; public static CoroutineManager Instance() = { _instance = this;} private LinkedList<IEnumerator> coroutineList = new LinkedList<IEnumerator>(); public void StartCoroutine(IEnumerator ie) { coroutineList.AddList(ie); } public void StopCoroutine(IEnumerator ie) { coroutineList.Remove(ie); } public void UpdateCoroutine() { var node = coroutineList.First; while(node!=null) { IEnumerator ir = node.Value; bool ret = true; if(ie.Current is IWait) { IWait wait = (IWait)ie.Current; if(wait.Tick()) { ret = ie.MoveNext(); } }else { ret = ie.MoveNext(); } if(!ret) { coroutineList.Remove(ndoe); } node = node.Next; } }