How to Develop a Game
Unity Asynchronous and Coroutine
PublishDate: 2025-06-01 | CreateDate: 2025-06-01 | LastModify: 2025-06-01 | Creator:ljf12825

在Unity中,异步编程主要应用于长时间运行的操作或I/O操作,例如加载场景、资源(如纹理、音频文件)、进行网络请求或其他非阻塞操作。Unity提供了几种常见的方式来实现异步操作,通常通过协程和异步编程API(如async/await)来实现

Asynchronous

Unity 从 2017 版本开始支持 async/await 异步编程方式,它是 C# 的一部分,适用于处理 耗时的异步操作,如网络请求、文件操作等。通过 async 标记方法,并在需要等待的地方使用 await,可以简化代码并使其更加可读

示例:异步加载资源(UnityWebRequest)
假设你要从网络上下载文件,可以使用async/await来实现非阻塞的异步操作:

using UnityEngine;
using UnityEngine.Networking;
using System.Threading.Task;

public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        string url = "https://example.com/resource";
        string result = await DownloadDataAsync(url);
        Debug.Log("下载完成:" + result);
    }

    // 异步下载数据
    private async Task<string> DownloadDataAsync(string url)
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
        {
            // 发送请求并等待结果
            await webRequest.SendWebRequest();

            if (webRequest.result == UnityWebRequest.Result.Success)
                return webRequest.downloadHandler.text; // 返回下载的文本内容
            else return "错误:" + webRequest.error;
        }
    }
}

在这个例子中,DownloadDataAsync使用async/await来处理异步操作。awatiwebRequest.SendWebRequest()会等待请求完成,避免阻塞主线程

异步操作的常见用途:

异步编程优缺点:

Coroutine

Unity Coroutine是一种允许在多帧中分布执行代码的机制,它通常用于处理一些需要在多个帧之间等待的任务,比如延时操作、动画播放、资源加载等
协程本质上是通过一种特殊的方式执行代码,它可以在执行过程中“暂停”并在后续的帧继续执行

协程是通过StartCoroutine()来启动的。协程通常返回一个IEnumerator类型的方法

using UnityEngine;
using System.Collections;

public class CoroutineExample : MonoBehaviour
{
    void Start() => StartCoroutine(MyCoroutine());

    IEnumerator MyCoroutine()
    {
        //在这里执行某些操作
        Debug.Log("协程开始");

        // Wait 2 seconds
        yield return new WaitForSeconds(2);

        // 等待结束后继续执行
        Debug.Log("2秒后继续执行");

        // 继续执行其他操作
        yield return null; // 等待下一帧
        Debug.Log("协程执行完毕");
    }
}

在这个例子中,MyCoroutine协程将在开始时打印“协程开始”,然后等待2秒后打印“2秒后继续执行”,最后在下一帧打印“协程执行完毕”

协程可以通过yield return暂停执行,直到某个条件满足。常见的暂停类型有:

协程并不是自动停止的,你需要显示地停止它
使用StopCoroutine()方法可以停止某个协程,或者通过StopAllCoroutine()停止当前对象的所有协程

StartCoroutine(MyCoroutine());
StopCoroutine(MyCoroutine());

协程通常返回一个IEnumerator,但也可以有不同的返回类型,比如WaitForSeconds或其他等待条件类型

协程的优缺点

优点:

缺点:

注意

进阶应用

协程不仅仅仅限于等待固定时间,也可以与其他逻辑结合实现复杂的功能

等待某个条件满足后继续执行

IEnumerator WaitForCondition()
{
    while (!someCondition) yield return null;

    Debug.Log("条件满足,继续执行");
}

动态修改等待时间

IEnumerator DynamicWait(float time)
{
    yield return new WaitForSeconds(time);
    Debug.Log("等待结束");
}

实现动画或缓动 可以用协程来实现逐个改变的某个值,例如实现一个平滑的动画过渡

IEnumerator LerpPosition(Vector3 targetPosition, float duration)
{
    Vector3 startPosition = transform.position;
    float timeElapsed = 0f;

    while (timeElapsed < duration)
    {
        transform.position = Vector3.Lerp(startPosition, targetPosition, timeElapsed / duration);
        timeElapsed += Time.deltaTime;
        yield return null;
    }

    transform.position = targetPosition;
}

协程的底层机制

在Unity中,协程的执行是通过IEnumerator类型的函数来定义的,协程的调用、暂停、恢复都与Unity的主线程紧密结合
协程不是传统意义上的线程,而是通过Unity引擎内部的协程调度系统来管理的

通过StartCoroutine()方法启动,这个方法接收一个IEnumerator类型的函数,或者是一个字符串(表示方法名)

StartCoroutine(MyCoroutine());

StartCoroutine("MyCoroutine");

当调用StartCoroutine()时,Unity会为该协程分配一个任务,并把它加入到协程调度队列中
之后Unity的主循环会负责在每一帧执行协程的代码

协程本质上时被Unity的引擎框架所调度的,协程代码并不会一次性执行完,而是会按需执行
调度流程
1.挂起状态:当协程执行到yield return语句时,Unity会暂停协程的执行,并把协程的执行状态保存下来(即当前的执行位置和上下文)
2.等待状态:协程会等待指定的时间、条件、或事件。在等待期间,协程的执行被挂起
3.恢复执行:当协程等待的条件满足,Unity会再次将协程的执行任务加入到下一帧的调度队列中,并从挂起点继续执行

协程调度的底层实现机制

大致底层实现:

协程的性能与限制

Unity中协程的状态是如何被保存的

  1. Coroutine对象 协程的状态是通过Coroutine对象来管理的,每个协程在运行时都会创建一个Coroutine对象,Unity会使用这个对象来跟踪协程执行状态

  2. IEnumerator状态机 协程通常返回一个IEnumerator对象,这实际上就是一个状态机的实现。在协程函数中,你可以通过yield return语句控制协程的执行。当协程遇到yield语句时,Unity会保存当前的执行上下文(如执行位置、局部变量等),并在下一帧继续从这个位置开始执行

  3. 内存和堆栈 在协程执行过程中,Unity会使用内存中的堆栈来保存函数调用的上下文,每次协程被挂起时,它的局部变量、执行位置等信息会被保存在堆栈中,当协程恢复时,这些信息会被取出,协程继续执行

  4. 协程调度器 Unity会管理一个协程调度器,它负责跟踪所有活动的协程,并在每一帧更新它们。协程调度器会检查每个协程的状态,如果协程已经完成,它就会被销毁

  5. 状态保存与恢复 Unity通过以下方式来保存和恢复协程状态:

结束和清理

当协程结束时,Unity会清理相关的资源并移除协程。若协程被手动停止,Unity会在下一帧停止协程的执行,并释放相关资源

IEnumerator接口

IEnumerator是C#中的一个接口,它广泛用于实现迭代器模式,通过IEnumerator,我们可以控制集合的遍历、生成懒加载的数据、以及其他的一些需要“暂停”和“恢复”的操作,比如Unity中的协程

IEnumerator接口概述

IEnumerator是C#中用来实现迭代器模式的接口,它定义了两种方法和一个属性:

public interface IEnumerator
{
    bool MoveNect();
    object Current { get; }
    void Reset();
}

IEnumerator在迭代器中的使用

IEnumerator主要用于构建“迭代器”,用于按顺序遍历集合中的元素。一个典型的实现例子是一个自定义集合类,它提供了一个迭代器来遍历集合中的元素:

public class MyCollection : IEnumerable
{
    private int[] numbers = {1, 2, 3, 4, 5};
    
    public IEnumerator GetEnumerator() => return new MyEnumerator(numbers);

    private class MyEnumerator : IEnumerator
    {
        private int[] _numbers;
        private int _index = -1;

        public MyEnumerator(int[] numbers) => _numbers = numbers;

        public bool MoveNext()
        {
            _index++;
            return _index < _numbers.Length;
        }

        public object Current => _numbers[_index];

        public void Reset() => _index = -1;
    }
}

在这个例子中,MyCollection实现了IEnumerable接口,这样它就可以被foreach遍历
GetEnumerator方法返回一个IEnumerator实例,负责控制迭代过程

yield returnIEnumerator

在C#中,yield return是实现IEnumerator接口的一种简化方式,当你使用yield return时,编译器会自动生成一个迭代器类,并且每次yield作为一个暂停点来保存当前的状态
示例:使用yield return返回值

public class MyCollection
{
    public IEnumerable<int> GetNumbers()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
}

GetNumbers返回了一个IEnumerable<int>类型的对象,这意味着可以通过foreach来遍历这个集合。
每次迭代时,yield return会暂停方法的执行,并将当前值返回给调用者,这个过程在每次调用MoveNext时自动恢复

IEnumerator与Unity协程

Unity中的协程是基于IEnumerator实现的,它是延迟执行的核心。协程的执行通过yield return来暂停,Unity引擎管理协程的调度和恢复

yield return与状态机

在使用yield return时,C#编译器会将方法转化为状态机,每次遇到yield return时,编译器会保存当前方法的执行状态(局部变量、执行信息等),并返回一个IEnumerator迭代器,这个迭代器控制着何时继续执行代码,何时返回控制权

状态机生成示例 假设有协程:

IEnumerator ExampleCoroutine()
{
    Debug.Log("Step 1");
    yield return new WairForSeconds(1f);
    Debug.Log("Step 2");
}

编译器会将这个代码转化成类似以下的状态机:

private class ExampleCoroutineStateMachine : IEnumerator
{
    private int _state = 0;
    private bool _isPaused = false;

    public bool MoveNext()
    {
        switch (_state)
        {
            case 0:
                Debug.Log("Step 1");
                _state = 1;
                return true; // 暂停,等待外部恢复
            case 1:
                if (_isPaused)
                {
                    _isPaused = false; // 模拟等待1秒
                    Debug.Log("Step 2");
                    return false; // 完成
                }
                return true; // 继续执行
            default:
                return false; // 协程结束
        }
    }
    public object Current => null;
}

每次协程执行时,状态机会检查当前状态,并决定是否继续执行或暂停

状态保存与恢复:IEnumerator和Unity协程

当Unity执行协程时,每次遇到yield return,它会将协程的执行状态(包括局部变量、执行栈、当前指令位置等)保存到内存中。
Unity引擎管理一个协程调度器,这个调度器负责在每一帧检查协程的状态,并恢复或继续执行这些协程
yield表达式在背后通过IEnumerator实现,并通过Unity的调度器继续执行

自定义IEumerator

可以自定义自己的IEnumerator来控制更复杂的行为

public class WaitForCondition : CustomYieldInstruction
{
    private Func<bool> _condition;

    public WaitForCondition(Func<bool> condition) => _condition = condition;

    public override bool keepWaiting => !_condition(); // 直到条件满足才结束
}