Unity协程详解

协程(Coroutine)是一种用于异步编程的概念和技术,它允许程序在执行任务的过程中暂停,然后在稍后的时间继续执行,而不会阻塞整个应用程序或线程。协程在游戏开发、图形编程和事件驱动的应用程序中非常有用,因为它们可以处理需要等待、延迟执行、动画效果、网络请求等情况,同时保持应用的响应性和性能。

什么是协程

在 unity 中,我们一般不考虑多线程,因为 Unity 只能在主线程中获取物体的组件、方法、对象等,脱离了这些功能,多线程的意义就不大了。但当主线程在执行一个对资源消耗很大的操作时,如果在一帧中实现这些操作,游戏就会变得十分卡顿,这个时候,我们就可以通过协程,在多帧内完成该工作的处理,同时不影响主任务的进行。所以协程顾名思义,就是辅助主线程的协助程序,用来执行一些资源消耗大的脏活累活,避免主线程卡顿。需要明确协程不是线程,协程依旧是在主线程中进行的

协程和线程

线程:线程是操作系统级别的并发执行单位。每个线程都有自己的堆栈和寄存器上下文,可以在多个 CPU 核心上并行执行。线程通常是由操作系统调度和管理的。
协程:协程是在单个线程内部执行的轻量级任务。它们由开发人员显式地控制,可以通过挂起和恢复来实现多任务切换。

协程的特点

  • 轻量级任务:协程是轻量级的任务,可以在单个线程内顺序执行。它们不需要创建额外的线程,因此不会引入多线程的同步和竞争条件问题。
  • 异步操作:协程常用于处理异步操作,如加载资源、网络请求、延时执行等。通过协程,可以避免阻塞主线程,使应用保持响应性。
  • 非阻塞执行:协程允许在不阻塞主线程的情况下执行代码。这对于需要等待某些条件满足或执行长时间运行的操作非常有用,而不会导致游戏卡顿。
  • 多帧执行:协程的执行是分帧的,它可以*在多个帧之间分散执行。协程可以在代码中使用yield语句来指定何时暂停执行并等待条件满足,然后继续执行。
  • 任务分割:协程使任务分割变得容易。可以将一个复杂的任务分解成多个协程,每个协程负责执行特定部分的任务,从而使代码更易于管理和理解。
  • 事件处理:协程可以用于处理事件,如用户输入、碰撞检测等。例如,可以编写协程以在某个条件满足时触发特定的事件处理逻辑。
  • 动画控制:协程常用于控制动画序列。例如,在游戏中,可以使用协程实现角色的平滑移动、淡入淡出效果等。
  • 易于编写和调试:相对于多线程编程,协程通常更容易编写和调试。它们允许开发人员以顺序方式编写异步逻辑,而无需担心线程同步问题。
  • 不适用于多核 CPU:协程在单线程内执行,因此无法充分利用多核 CPU。它适用于需要处理异步操作和控制流程的场景,但不适用于需要高度并行处理的性能密集型任务。

协程的底层原理

  • 调度器:Unity 引擎内部有一个协程调度器,用于管理和调度协程的执行。多个协程在主线程中以轮换分时的方式运行,这个调度器在主线程中运行,负责管理协程的生命周期和执行顺序,协程调度器会在每一帧中检查协程的状态,并决定哪一个协程应该继续执行。
  • 协程方法:协程是通过迭代器来实现功能的,通过关键字 IEnumerator 来定义一个协程方法,返回 IEnumerator 类型,这是一个接口,表示一个可枚举的集合。当在 Unity 中启动协程时,实际上是向协程调度器注册了一个待执行的协程任务
  • yield 指令:迭代方法必须包含 yield 语句,用于指定协程的挂起点。一个迭代方法中可以有多个 yield 语句,每个 yield 语句都表示协程的一个状态,这个状态可以是等待一段时间、等待一个条件满足、执行一个操作等。协程的执行过程实际上是一个状态机(State Machine),每个 yield 语句都表示协程的一个状态,每次访问时会基于状态知道当前应该执行哪一个 yield。
  • 帧执行:协程任务在每一帧中执行。当一个协程任务被注册后,协程调度器会在每一帧中检查协程的状态,并根据 yield 语句的指令来控制协程的执行。
  • 执行流程:当协程开始执行时,它会一直执行到遇到 yield 语句。在遇到 yield 后,协程会在当前帧结束后,或在等待的时间间隔过后,继续执行。这个过程会持续循环,直到协程执行完毕。

总之,协程的核心概念是迭代器和 yield 关键字,而迭代器是一个特殊的C#方法,其返回值必须是 IEnumerator 接口。当你在协程方法中使用 yield 关键字,编译器会生成一个迭代器,将协程的执行分为多个状态。每个 yield 语句会导致生成一个状态,每个状态都会在调用 MoveNext 时执行一次。所以,一个 yield 语句通常会生成一个 MoveNext。这些状态在不同帧中检测当前帧是否满足协程所定义的条件,一旦满足,当前帧就会抽出 CPU 时间执行你所定义的协程迭代器的 MoveNext。

协程的使用

定义协程方法

首先,你需要定义一个协程方法。这个方法的返回类型通常是 IEnumerator。协程方法内部可以包含yield语句,用于控制协程的执行流程。

不带参数的协程方法:

1
2
3
4
5
6
IEnumerator MyCoroutine()
{
// 协程的执行逻辑
yield return new WaitForSeconds(2.0f);
Debug.Log("Coroutine executed!");
}

带参数的协程方法:

1
2
3
4
5
6
IEnumerator MyCoroutineWithArgs(int value) {
// 在这里使用传递的参数 value
Debug.Log("Received value: " + value);
// 这里可以进行其他协程操作
yield return null;
}

开启协程

  • StartCoroutine(string methodName):这种方式使用协程方法的名称(字符串形式)来启动协程。适用于不需要参数的协程方法。
    示例:StartCoroutine("MyCoroutine");
  • StartCoroutine(IEnumerator routine):这种方式通过协程方法的实际方法来启动协程。适用于带有参数的协程方法或不需要参数的协程方法。
    示例1:StartCoroutine(MyCoroutine());
    示例2:StartCoroutine(MyCoroutineWithArgs(1));
  • StartCoroutine(string methodName, object values):这种方式使用协程方法的名称和参数(以object形式传递)来启动协程。适用于需要传递参数给协程方法的情况。
    示例:StartCoroutine("MyCoroutineWithArgs", someValue);

关闭协程

StopCoroutine

  • 根据协程的方法名:可以使用方法的名称来停止协程,只需传递协程的方法名(作为字符串)给 StopCoroutine 方法。
    示例:StopCoroutine("MyCoroutine");

  • 根据协程的引用:可以使用已经引用的协程的引用来停止它。
    示例:

1
2
3
4
IEnumerator myCoroutineInstance = MyCoroutine();
StartCoroutine(myCoroutineInstance);
// 在需要的时候停止协程
StopCoroutine(myCoroutineInstance);
  • 根据协程返回的协程句柄:当你启动协程时,StartCoroutine 方法会返回一个协程句柄,你可以使用这个句柄来停止协程。
    示例:
1
2
3
Coroutine myCoroutineHandle = StartCoroutine(MyCoroutine());
// 在需要的时候停止协程
StopCoroutine(myCoroutineHandle);

注意:以下这样调用停不掉Coroutine。
StartCoroutine(Test1());
StopCoroutine(Test1());

StartCoroutine(Test2(1));
StopCoroutine(Test2(1));

StartCoroutine(Test3());
StopCoroutine(“Test3”);

StopAllCoroutines

请注意,使用 StopCoroutine 只会停止指定的协程。如果你有多个具有相同方法名的协程在运行,StopCoroutine 只会停止其中一个。如果要停止所有具有相同方法名的协程,可以使用 StopAllCoroutines。
StopAllCoroutines(); // 停止所有协程

注意:将游戏对象的activeself设置为false时,可停掉此GameObject上的所有协程,并且再次激活时协程不会继续。但是将脚本的脚本enabled设置为false时,不可停掉协程。

yield

关键字在 C# 中用于创建协程,它可以在协程中暂停执行,然后在稍后的某个时间点恢复执行。以下是一些常见的 yield 使用方式:

yield 0

yield return 0; //下一帧再执行后续代码
yield return 6;//(任意数字) 下一帧再执行后续代码

yield return null

这是最常见的用法,用于等待一帧的时间,通常用于实现延迟操作。例如,yield return null; 可以使协程等待一帧后再执行下一步。

yield break

该语句用于终止协程的执行。它允许你在协程中的任何地方显式地停止执行协程,就像 return 语句在常规函数中终止执行一样。当调用 yield break,协程将立即结束,不再执行后续的代码。这可以在协程的任何地方用于提前退出协程。

yield return asyncOperation

等异步操作结束后再执行后续代码。注意,不是直接写 yield return asyncOperation,正确示例:

1
2
3
4
5
6
7
AsyncOperation asyncOperation = SomeAsyncMethod();
// 在协程中等待异步操作完成
while (!asyncOperation.isDone) {
yield return null;
}
// 异步操作完成后执行下面的代码
Debug.Log("Async operation is done.");

yield return new WaitForSeconds(seconds)

这用于等待指定的秒数。例如,yield return new WaitForSeconds(2.0f); 可以使协程等待 2 秒钟。

yield return new WaitForSecondsRealtime(seconds)

与 WaitForSeconds 类似,但不受游戏暂停影响,用于在实时时间中等待一段时间。

yield return new WaitForFixedUpdate()

用于等待下一个固定帧更新(FixedUpdate)。
通常在物理更新或与物理相关的操作中使用。

yield return new WaitForEndOfFrame()

用于等待当前帧的结束,然后再执行下一步,通常在需要在渲染完成后执行某些操作时使用。

yield return new WWW(url)

用于等待从指定 URL 下载的内容(通常是网络请求),在下载完成后继续执行协程。

yield return StartCoroutine(MyCoroutine())

用于等待另一个协程完成执行,这可以用于协程的嵌套和顺序执行多个协程。

yield return new WaitUntil(() => someCondition)

用于等待某个条件变为真,可以在协程中等待直到给定条件满足。

yield return new WaitWhile(() => someCondition)

用于等待某个条件变为假,可以在协程中等待直到给定条件不满足。

yield return new YieldInstruction()

也可以创建自定义的 yield 指令,实现协程中的等待操作,YieldInstruction 是基类,也可以派生出自定义的等待条件。

这些 yield 使用方式可以帮助你在协程中实现不同类型的等待和延迟操作。通过合理使用 yield,你可以更好地控制协程的执行顺序和时机。