Unity协程(Coroutine)原理解析
线程(Thread)和协程(Coroutine)
使用协程的作用一共有两点:1)延时(等待)一段时间执行代码;2)等某个操作完成之后再执行后面的代码。总结起来就是一句话:控制代码在特定的时机执行。
很多初学者,都会下意识地觉得协程是异步执行的,都会觉得协程是C# 线程的替代品,是Unity不使用线程的解决方案。
所以首先,请你牢记:协程不是线程,也不是异步执行的。协程和 MonoBehaviour 的 Update函数一样也是在MainThread中执行的。使用协程你不用考虑同步和锁的问题。
Unity中协程的执行原理
UnityGems.com给出了协程的定义:
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
即协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码。
Unity在每一帧(Frame)都会去处理对象上的协程。
『今天意外发现Monobehaviour的函数执行顺序图,发现协程的运行确实是在LateUpdate之后,下面附上:』
整理得到 :通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。
协成中最少需要执行两帧..所以最好不要在协成做一些判断..去执行yield
yield 后面可以有的表达式:
a) null – the coroutine executes the next time that it is eligible
b) WaitForEndOfFrame – the coroutine executes on the frame, after all of the rendering and GUI is complete
c) WaitForFixedUpdate – causes this coroutine to execute at the next physics step, after all physics is calculated
d) WaitForSeconds – causes the coroutine not to execute for a given game time period
e) WWW – waits for a web request to complete (resumes as if WaitForSeconds or null)
f) Another coroutine – in which case the new coroutine will run to completion before the yielder is resumed
值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足。
IEnumerator & Coroutine
协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法 Current 和 MoveNext() ,这里在介绍一个协程的交叉调用类 Hijack(参见附件):
using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using System.Collections; [RequireComponent(typeof(GUIText))] public class Hijack : MonoBehaviour { //This will hold the counting up coroutine IEnumerator _countUp; //This will hold the counting down coroutine IEnumerator _countDown; //This is the coroutine we are currently //hijacking IEnumerator _current; //A value that will be updated by the coroutine //that is currently running int value = 0; void Start() { //Create our count up coroutine _countUp = CountUp(); //Create our count down coroutine _countDown = CountDown(); //Start our own coroutine for the hijack StartCoroutine(DoHijack()); } void Update() { //Show the current value on the screen guiText.text = value.ToString(); } void OnGUI() { //Switch between the different functions if(GUILayout.Button("Switch functions")) { if(_current == _countUp) _current = _countDown; else _current = _countUp; } } IEnumerator DoHijack() { while(true) { //Check if we have a current coroutine and MoveNext on it if we do if(_current != null && _current.MoveNext()) { //Return whatever the coroutine yielded, so we will yield the //same thing yield return _current.Current; } else //Otherwise wait for the next frame yield return null; } } IEnumerator CountUp() { //We have a local increment so the routines //get independently faster depending on how //long they have been active float increment = 0; while(true) { //Exit if the Q button is pressed if(Input.GetKey(KeyCode.Q)) break; increment =Time.deltaTime; value = Mathf.RoundToInt(increment); yield return null; } } IEnumerator CountDown() { float increment = 0f; while(true) { if(Input.GetKey(KeyCode.Q)) break; increment =Time.deltaTime; value -= Mathf.RoundToInt(increment); //This coroutine returns a yield instruction yield return new WaitForSeconds(0.1f); } } }
上面的代码实现是两个协程交替调用,对有这种需求来说实在太精妙了。