版本:Unity 6 (6000.0)
语言:English
Inspector 可配置的自定义事件
与 Web 服务器交互

跨帧拆分任务

协程允许您将任务分散到多个帧中。协程是一个可以暂停执行并将控制权返回给 Unity,然后在下一帧继续执行的方法。

在大多数情况下,当您调用一个方法时,它会运行到完成,然后将控制权返回给调用方法,以及任何可选的返回值。这意味着方法内发生的任何操作都必须在单个帧更新内发生。

在您希望使用方法调用来包含过程动画或一段时间内的事件序列的情况下,您可以使用协程。

注意:务必记住,协程不是线程。在协程中运行的同步操作仍然在主线程上执行。如果您想减少在主线程上花费的 CPU 时间,在协程中避免阻塞操作与在任何其他脚本代码中一样重要。如果您想在 Unity 中使用多线程代码,您的选项是

如果您需要处理长时间的异步操作,例如等待 HTTP 传输、资源加载或文件 I/O 完成,最好使用协程。

协程示例

例如,考虑逐渐降低对象 alpha(不透明度)值直至其变得不可见的任务

void Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
    }
}

在此示例中,Fade 方法没有您可能期望的效果。为了使淡出可见,您必须在一系列帧上降低淡出的 alpha 以显示 Unity 渲染的中间值。但是,此示例方法完全在单个帧更新内执行。中间值永远不会显示,并且对象会立即消失。

为了解决这种情况,您可以在 Update 函数中添加代码,以便逐帧执行淡出。但是,对于此类任务,使用协程可能更方便。

在 C# 中,您可以这样声明协程

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return null;
    }
}

协程是一个用 IEnumerator 返回类型声明的方法,并在主体中的某个位置包含 yield 返回语句。yield return null 行是执行暂停并在下一帧恢复的点。要设置协程运行,您需要使用 StartCoroutine 函数

void Update()
{
    if (Input.GetKeyDown("f"))
    {
        StartCoroutine(Fade());
    }
}

Fade 函数中的循环计数器在协程的生命周期内保持其正确的值,并且任何变量或参数在 yield 语句之间都会保留。

协程时间延迟

默认情况下,Unity 在 yield 语句后的帧上恢复协程。如果您想引入时间延迟,请使用 WaitForSeconds

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return new WaitForSeconds(.1f);
    }
}

您可以使用 WaitForSeconds 将效果分散到一段时间内,并且可以使用它作为将任务包含在 Update 方法中的替代方法。Unity 每秒调用 Update 方法几次,因此如果您不需要任务如此频繁地重复,您可以将其放入协程中以获得定期更新,但不是每一帧。

例如,您的应用程序中可能有一个警报,如果敌人附近会警告玩家,代码如下

bool ProximityCheck()
{
    for (int i = 0; i < enemies.Length; i++)
    {
        if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
                return true;
        }
    }

    return false;
}

如果有很多敌人,则每帧调用此函数可能会带来很大的开销。但是,您可以使用协程每十分之一秒调用它一次

IEnumerator DoCheck()
{
    for(;;)
    {
        if (ProximityCheck())
        {
            // Perform some action here
        }
        yield return new WaitForSeconds(.1f);
    }
}

这减少了 Unity 执行的检查次数,而不会对游戏玩法产生任何明显的影响。

停止协程

要停止协程,请使用 StopCoroutineStopAllCoroutines。如果您已将 SetActive 设置为 false 以禁用协程附加到的 GameObjectUnity 场景中的基本对象,可以表示角色、道具、场景、摄像机、路点等。GameObject 的功能由附加到它的组件定义。更多信息
参见 词汇表
,则协程也会停止。调用 Destroy(example)(其中 exampleMonoBehaviour 实例)会立即触发 OnDisable,并且 Unity 会处理协程,从而有效地停止它。最后,在帧结束时调用 OnDestroy

注意:如果您通过将 enabled 设置为 false 禁用了 MonoBehaviour,则 Unity 不会停止协程。

分析协程

协程的执行方式与其他脚本代码不同。Unity 中的大多数脚本代码都出现在性能跟踪中的单个位置,位于特定回调调用的下方。但是,协程的 CPU 代码始终出现在跟踪中的两个位置。

协程中的所有初始代码(从协程方法的开头到第一个 yield 语句)都会在 Unity 启动协程时出现在跟踪中。初始代码最常出现在调用 StartCoroutine 方法时。Unity 回调生成的协程(例如返回 IEnumeratorStart 回调)首先出现在其各自的 Unity 回调中。

协程的其余代码(从第一次恢复到执行完成)出现在 Unity 主循环内的 DelayedCallManager 行中。

发生这种情况是由于 Unity 执行协程的方式。 C# 编译器 自动生成一个支持协程的类的实例。然后,Unity 使用此对象跨单个方法的多次调用跟踪协程的状态。由于协程中的局部作用域变量必须跨 yield 调用持续存在,因此 Unity 将局部作用域变量提升到生成的类中,这些变量在协程期间保持在堆上分配。此对象还跟踪协程的内部状态:它记住在产生后必须在代码的哪个点恢复协程。

因此,协程启动时发生的内存压力等于固定开销分配加上其局部作用域变量的大小。

启动协程的代码构造并调用一个对象,然后 Unity 的 DelayedCallManager 在满足协程的 yield 条件时再次调用它。由于协程通常在其他协程之外启动,因此这会将其执行开销拆分到 yield 调用和 DelayedCallManager 之间。

您可以使用 Unity Profiler帮助您优化游戏的窗口。它显示在游戏的各个区域花费了多少时间。例如,它可以报告渲染、动画或游戏逻辑中花费的时间百分比。更多信息
参见 词汇表
检查和了解 Unity 在您的应用程序中执行协程的位置。为此,请在启用 深度分析 的情况下分析您的应用程序,这会分析脚本代码的每个部分并记录所有函数调用。然后,您可以使用 CPU 使用情况分析器模块 来调查应用程序中的协程。

Profiler session with a coroutine in a DelayedCall
在 DelayedCall 中具有协程的 Profiler 会话

最佳实践是将一系列操作压缩到尽可能少的单独协程中。嵌套协程对于代码清晰度和维护很有用,但它们会带来更高的内存开销,因为协程会跟踪对象。

如果协程每帧运行并且不会在长时间运行的操作上 yield,则用 UpdateLateUpdate 回调替换它会更高效。如果您有长时间运行或无限循环的协程,这很有用。

其他资源

Inspector 可配置的自定义事件
与 Web 服务器交互