浅谈Unity协程的工作机制

一. 什么是协程

协程概述

在 Unity 中,协程(Coroutine)是一种非常常用的机制,用于非阻塞地处理需要跨越多个帧、等待某些条件或延迟一段时间才能完成的任务。Unity 的协程通过 C# 的 IEnumerator 和 yield return 实现,使得你可以在游戏主线程中以一种简洁的方式执行异步操作,而不需要使用复杂的多线程或回调。

协程允许你暂停代码的执行,并在稍后的某个时间点恢复执行。适合处理游一些常见需求,比如:

1延迟一定时间后执行代码。

2等待某个条件(如动画完成、玩家输入)后继续执行。

3 逐帧执行任务,而不是一次性完成,避免阻塞主线程。

下面一些示例

cs 复制代码
void Start()
{
    // 启动协程
    StartCoroutine(MyCoroutine());
    Debug.Log("因为是协程,所以不会阻塞打印");
}

IEnumerator MyCoroutine()
{
    Debug.Log("Before waiting");

    // 暂停协程 2 秒
    yield return new WaitForSeconds(2);

    Debug.Log("After waiting 2 seconds");
}

执行流程:

当 StartCoroutine(MyCoroutine()) 被调用时,MyCoroutine 方法开始执行,先输出 "Before waiting".

在遇到yield return时会立即返回控制权给上级调用者,这里上级调用者是主线程,打印"因为是协程,所以不会阻塞打印",并且执行到这里的状态会被保存下来,以便条件(这里的条件是等待两秒)完成后继续向下执行.

2 秒后,协程恢复,继续执行,输出 "After waiting 2 seconds"。

yield return 的使用

协程中的 yield return 是关键,它决定了协程什么时候暂停以及在什么条件下恢复。

yield return null://暂停协程直到下一帧。它相当于让协程在下一帧继续执行。

yield return new WaitForSeconds(x)://暂停协程 x 秒后继续执行。

yield return new WaitUntil(predicate)://等待直到某个条件为 true 时继续执行。

如yield return new WaitUntil(() => someCondition == true); // 等待某个条件成立

yield return new WaitWhile(predicate)://等待直到某个条件为 false 时继续执行。

yield return StartCoroutine(otherCoroutine)://等待另一个协程执行完毕后再继续执行当前协程。

yield return WaitForEndOfFrame():

//暂停协程执行,直到当前帧的渲染完成。可以在本帧所有操作完成后再进行一些后处理。

yield return WaitForFixedUpdate()

//暂停协程执行,直到下一次物理更新(即 FixedUpdate 调用)发生时继续。常用于与物理更新频率保持同步的操作。

常见用例

1. 延迟执行任务

协程经常被用来在一段时间后执行某个任务。以下是一个延迟 3 秒后执行的例子:

cs 复制代码
IEnumerator DelayedAction()
{
    yield return new WaitForSeconds(3);  // 等待 3 秒
    Debug.Log("This happens after 3 seconds");
}
2. 等待玩家输入

协程可以暂停代码执行,直到玩家做出某些输入,比如按下某个键:

cs 复制代码
IEnumerator WaitForPlayerInput()
{
    Debug.Log("Waiting for player to press Space");
    yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space));  // 等待玩家按下空格键
    Debug.Log("Player pressed Space");
}
3. 多协程依赖执行

你可以让一个协程等待另一个协程执行完成后再继续。例如,下面的例子展示了如何等待两个协程执行完成后再执行后续逻辑:

cs 复制代码
IEnumerator Sequence()
{
    yield return StartCoroutine(Task1());
    yield return StartCoroutine(Task2());
    Debug.Log("Both tasks completed");
}

IEnumerator Task1()
{
    Debug.Log("Task 1 started");
    yield return new WaitForSeconds(2);
    Debug.Log("Task 1 completed");
}

IEnumerator Task2()
{
    Debug.Log("Task 2 started");
    yield return new WaitForSeconds(1);
    Debug.Log("Task 2 completed");
}
4. 逐帧处理任务

有时我们希望任务能在多个帧内逐渐完成,而不是一次性完成以避免卡顿。协程可以轻松实现这一点:

cs 复制代码
IEnumerator DoTaskOverTime()
{
    for (int i = 0; i < 10; i++)
    {
        Debug.Log("Processing step: " + i);
        yield return null;  // 等待到下一帧再继续执行
    }
}

协程的注意事项

协程不是真正的多线程:

虽然协程可以暂停执行,并在之后恢复,但它们始终在主线程中运行。也就是说,协程不会并发执行,只是通过暂停和恢复的方式避免了阻塞主线程的情况。

Unity 的协程是始终在主线程上运行的,并不涉及多线程处理。但因为协程使用了 yield return 进行暂停,所以它可以让其他代码继续执行,而不会阻塞主线程的运行。这种非阻塞的行为有时会让人误以为协程运行在另一个线程上,但实际上它仍然依赖主线程的调度。

假设你启动了一个协程,在协程中等待 2 秒后打印一段话,而协程下面是一个耗时的方法。

cs 复制代码
void Start()
{
    // 启动协程
    StartCoroutine(MyCoroutine());
    
    // 耗时操作
    HeavyOperation();
}

IEnumerator MyCoroutine()
{
    Debug.Log("Before waiting");
    
    // 暂停协程 2 秒
    yield return new WaitForSeconds(2);
    
    Debug.Log("After waiting 2 seconds");
}

void HeavyOperation()
{
    // 模拟一个非常耗时的操作,比如计算密集型任务
    Debug.Log("Heavy operation started");
    for (int i = 0; i < 1000000000; i++) { }  // 假设这是一个耗时的操作
    Debug.Log("Heavy operation finished");
}

这里就会出现一个问题:

因为 HeavyOperation() 占用了大量的 CPU 时间,主线程处理其他任务(包括更新协程的进度)可能会延迟。这意味着即使 2 秒时间已经过去了,但如果 HeavyOperation() 还没执行完,协程中的 "After waiting 2 seconds" 也会推迟打印。

当协程遇到 yield return 时,执行会挂起并交出控制权,让上级调用者(通常是 Unity 的主线程)继续执行其他任务。然后,当条件满足后,协程会恢复执行,继续从挂起的地方往下执行。

注意这个条件满足之后,说的不清晰,让我们进一步说清楚协程恢复的时机:当协程的条件满足时(例如等待时间结束),Unity 不会立即打断主线程的操作。协程的恢复是在主线程的当前帧所有任务结束之后,才会被调度执行。

上面的例子是等待两秒,但是下面正在执行一个需要大量cpu的操作,两秒已经过去了还没有执行完,Unity并不会打断这个操作,所以这个等待秒数的行为并不是绝对靠谱的.

这是协程与多线程的关键区别之一。协程不会在任何时间打断主线程的工作,而是等到主线程"空闲"时(例如,一帧的任务全部完成),才会恢复执行。

在大多数情况下,协程的等待时间(比如 WaitForSeconds(2) 等待 2 秒)是靠谱的,但确实存在一些特殊情况可能导致时间的精确度不如帧数那样可靠。具体来说,帧是更为精确和可控的,而秒的等待可能会受到主线程的负载、性能瓶颈等因素的影响。

在 Unity 中,协程的恢复是基于帧更新的。Unity 的主线程在每一帧都会处理脚本、物理、渲染等任务,如果主线程负载过重或者有一些耗时操作,帧率可能会降低(例如从 60FPS 降到 30FPS),进而影响时间的精度。

协程的生命周期:

如果启动协程的 MonoBehaviour 被销毁或禁用,协程会自动停止。因此,协程的生命周期依赖于启动它的对象。

如果你希望手动停止协程,可以使用 StopCoroutine() 方法。

cs 复制代码
Coroutine myCoroutine;

void Start()
{
    myCoroutine = StartCoroutine(MyCoroutine());
}

void StopMyCoroutine()
{
    if (myCoroutine != null)
    {
        StopCoroutine(myCoroutine);
    }
}

YieldInstruction

YieldInstruction 类是一个抽象基类,用于控制协程的执行。通过使用 yield 关键字与 YieldInstruction 子类(如 WaitForSeconds, WaitForEndOfFrame, WaitForFixedUpdate 等)配合,可以让协程暂停一段时间,或者直到满足某些条件才继续执行。

CustomYieldInstruction

继承自 YieldInstruction 的类

CustomYieldInstruction 是一个可继承的类,允许开发者自定义协程的等待条件。通过重写 keepWaiting 属性,可以定义更复杂的逻辑,来控制协程何时继续执行

通过继承 CustomYieldInstruction,你可以实现复杂的等待条件。

cs 复制代码
public class WaitForCondition : CustomYieldInstruction
{
    private Func<bool> condition;

    public WaitForCondition(Func<bool> condition)
    {
        this.condition = condition;
    }

    public override bool keepWaiting
    {
        get { return !condition(); }
    }
}

// 使用自定义的协程等待条件
IEnumerator CustomWaitExample()
{
    Debug.Log("Waiting for custom condition...");
    yield return new WaitForCondition(() => Time.time > 5);
    Debug.Log("Condition met, continuing...");
}
相关推荐
ashane131426 分钟前
Java list
java·windows·list
万里沧海寄云帆38 分钟前
Word 插入分节符页码更新问题
windows·microsoft·word
大眼睛姑娘1 小时前
Unity3d场景童话梦幻卡通Q版城镇建筑植物山石3D模型游戏美术素材
unity·游戏美术
dot.Net安全矩阵2 小时前
.NET 通过模块和驱动收集本地EDR的工具
windows·安全·web安全·.net·交互
编程修仙3 小时前
Collections工具类
linux·windows·python
程序员小羊!5 小时前
高级 SQL 技巧讲解
windows
鹿野素材屋5 小时前
Unity Dots下的动画合批工具:GPU ECS Animation Baker
unity·游戏引擎
xiangshangdemayi6 小时前
Windows环境GeoServer打包Docker极速入门
windows·docker·容器·geoserver·打包·数据挂载
a_安徒生7 小时前
window系统改为Linux系统
linux·windows·centos·系统安全
huaqianzkh9 小时前
学习C#中的Parallel类
windows·microsoft·c#