什么是协程:
从代码上来看,协程,就是一个返回值为IEnumerator的函数。它主要运用于在Unity中希望某个功能能够分几帧来完成,而不是在一帧中完成的情况。
简单来说,协程就是一种特殊的函数,它可以主动的请求暂停自身并提交一个唤醒条件,Unity会在唤醒条件满足的时候去重新唤醒协程。
// 这段代码将会在一帧中循环一百次,并直接在这一帧中输出100个数字
void Fun()
{
for(int i = 0; i < 100; i++)
{
Debug.Log(i);
}
}
// 这段代码将在每帧中执行一次循环,然后等待下一帧再执行,即每帧只输出一个数字
IEnumerator Fun()
{
for(int i = 0; i < 100; i++)
{
Debug.Log(i);
yield return null; // 尽量不要用0,会导致装箱拆箱
}
}
为什么协程能够实现分帧实现一段代码?是因为协程能够实现多线程吗?
在
Unity
中,只能在主线程中获取物体的组件、方法、对象,如果脱离这些,Unity
的很多功能无法实现,那么多线程的存在与否意义就不大了
协程的执行时间:
根据Unity的生命周期图可知协程函数并不是一个独立的执行单元,它与Update、FixedUpdate和LateUpdate等函数一样,被Unity依次执行。因此,一旦一个协程函数发生死循环,就会阻碍整个游戏的运行,所以,协程并不是新开的一个线程,它也不是异步执行的,它同样是运行在主线程之中的。
协程的底层原理
前面已经提到,协程函数是一个返回值是IEnumerator的函数,因此可以猜测,它应该与迭代器有关。实际上,协程分为两部分,协程与协程调度器:协程仅仅是一个能够中间暂停返回的函数,而协程调度是在MonoBehaviour的生命周期中实现的。 准确的说,Unity只实现了协程调度部分,而协程本身其实就是用了C#原生的”迭代器方法“。
迭代器:
迭代器(iterator)又称光标(cursor),它提供一个方法来顺序访问一个集合中的各个元素而又不暴露其内部的标识。可以用foreach遍历的集合都是实现了迭代器的。
举个例子:
// 自定义一个待访问的类
class Test
{
private int[] list;
public Test()
{
list = new int[]{1, 2, 3, 4, 5, 6, 7, 8 };
}
}
//此时需要在Test类的外部来访问test
class Program
{
static void Main(string[] args)
{
Test test = new Test();
foreach(int item in test)
{
Debug.Log(item);
}
}
}
此时在foreach的语句中已经出现了报错。因为foreach便是由迭代器实现的,在foreach中,会先获取in后面的对象的迭代器IEnumerator(通过GetEnumerator方法获取),因此Test类需要作以下修改:
// 自定义一个待访问的类
class Test : IEnumerable
{
// 该接口只有GetEnumerator一个方法,继承的目的就是为了实现该方法,也可以不继承该接口,只要实现
// 了这个方法就行
private int[] list;
public Test()
{
list = new int[]{1, 2, 3, 4, 5, 6, 7, 8 };
}
public IEnumerator GetEnumerator() // 给待访问的类实现该函数即可
{
throw new NotImplementedException();
}
}
在实现了GetEnumerator后foreach便不报错了。
foreach在获取了对象的迭代器IEnumerator之后,便会执行该IEnumerator对象中的MoveNext方法,只要MoveNext方法的返回值是true,就会得到Current,然后赋值给item。此时就完成了foreach的一轮遍历。
那么,MoveNext方法和Current是从哪里得到的?
首先来看看IEnumerator接口
// 简单的写一个IEnumerator接口
public interface IEnumerator
{
object? Current { get; }; // 当前迭代器所指向的值
bool MoveNext(); // 集合中是否还有数据,并且按顺序移动迭代器
void Reset(); // 重置迭代器位置
}
让Test类继承该接口并实现所有方法
// 自定义一个待访问的类
class Test : IEnumerable, IEnumerator
{
// 该接口只有GetEnumerator一个方法,继承的目的就是为了实现该方法,也可以不继承该接口,只要实现
// 了这个方法就行
private int[] list;
private int pos = -1; // 光标(迭代器)位置
public Test()
{
list = new int[]{1, 2, 3, 4, 5, 6, 7, 8 };
}
// 在foreach首次执行时调用
public IEnumerator GetEnumerator() // 给待访问的类实现该函数即可
{
// 重置光标
Reset();
// 自己已经继承了IEnumerator,并且也实现了MoveNext等方法,因此直接返回自己即可
return this;
}
public object Current
{
get
{
return list[pos];
}
}
public bool MoveNext()
{
// 移动光标
++pos;
// 是否溢出,溢出则不合法
return pos < list.Length;
}
// 一般在GetEnumerator()函数中调用
public void Reset()
{
pos = -1;
}
}
结果如下:
可以看出,迭代器主要由MoveNext方法和Current属性来实现功能,所谓的yield也只是一个语法糖,它的实现逻辑也是MoveNext方法和Current属性。
协程本体:C#的迭代器函数
C#中的迭代器方法其实就是一个协程,可以使用yield来暂停,使用MoveNext()来继续执行。 当一个方法的返回值写成了IEnumerator类型,他就会自动被解析成迭代器方法(后文直接称之为协程),调用此方法的时候不会真的运行,而是会返回一个迭代器,需要用MoveNext()来真正的运行,例如:
using System;
static void Main(string[] args)
{
IEnumerator it = Test(); // 返回一个指向Test的迭代器,此时不会执行Test()函数。
Console.ReadKey();
it.MoveNext(); // 执行Test函数,直到遇到第一个yield,在第一个yield处跳出协程
Console.WriteLine(it.Current); // 输出1(yield return返回的值)
Console.ReadKey();
it.MoveNext(); // 执行Test函数,从之前跳出的yield return处开始执行,直到遇到第二个yield
Console.WriteLine(it.Current); // 输出2(yield return返回的值)
Console.ReadKey();
it.MoveNext(); // 执行Test函数,从之前跳出的yield return处开始执行,直到遇到第三个yield
Console.WriteLine(it.Current); // 输出test3(yield return返回的值)
Console.ReadKey();
}
static IEnumerator Test()
{
Console.WriteLine("第一次执行");
yield return 1;
System.Console.WriteLine("第二次执行");
yield return 2;
Console.WriteLine("第三次执行");
yield return "test3";
}
协程调度:MonoBehaviour生命周期中实现
我们已经知道,使用WaitForSeconds方法可以让协程延迟执行。但假如延迟的时间设为0,协程依然有最小的执行间隔时间——1帧的时间。无论协程函数怎么编写,它执行的频率都不会超过Update被调用的频率。
其实,Start和Update也支持协程式调用,如下所示:
using System.Collections;
using UnityEngine;
public class HelloCoroutine : Monobehaviour
{
IEnumerator Start()
{
Debug.Log(1);
yield return new WaitForSeconds(1f);
Debug.Log(2);
yield return new WaitForSeconds(1f);
Debug.Log(3);
}
}
以上代码让Start方法也具备了延时执行的功能,而且不需要用StartCoroutine启动。这是因为Start方法并不是主动调用的,而是被Unity引擎识别并调用的。这里把Start方法的返回值从void改为了IEnumerator,也同样被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)
{
// 初始化迭代器位置(执行协程直到遇见下一个yield return)
ie.MoveNext();
// 若yield return返回的值是WaitForSeconds
if(ie.Current is WaitForSeconds)
{
list.Add(new YieldInstruction
{
// 初始化迭代器以及延迟时间
ie = ie,
executeTime = Time.time + (ie.Currentas WaitForSeconds).second,
});
}
}
void Update()
{
// 倒序遍历方便移除
for(int i = list.Count - 1; i >= 0; i--)
{
// 满足唤醒条件
if(ie.Current is WaitForSeconds && list[i].executeTime <= Time.time)
{
// 唤醒协程
if(ie.MoveNext();)
{
// 执行对应的功能
}
// 唤醒失败,说明该协程已被唤醒,移出列表
else
{
list.RemoveAt(i);
}
}
}
}
}
参考资料: