Unity协程(Coroutine)详解

协程是Unity中一种强大的编程工具,它允许你在不阻塞主线程的情况下执行跨越多个帧的操作。协程提供了一种优雅的方式来处理需要随时间推移执行的任务,如动画、延时操作、分步骤处理等。

1. 协程的基本概念

什么是协程?

协程是一种特殊的函数,它可以在执行过程中暂停(yield),然后在之后的某个时间点恢复执行。这与普通函数不同,普通函数一旦开始执行就会一直运行到完成。

协程的关键特性:

  • 可以暂停执行并在之后恢复
  • 不会阻塞主线程
  • 使用yield语句来指示暂停点
  • 返回类型必须是IEnumerator
  • 必须使用StartCoroutine()方法启动

协程与线程的区别

协程不是线程。关键区别:

  • 协程在主线程上运行,不是并行执行
  • 协程没有线程切换的开销
  • 协程不需要线程同步机制
  • 协程是协作式多任务处理,而不是抢占式

2. 协程的基本语法

定义协程

private IEnumerator MyCoroutine()
{
    Debug.Log("协程开始");
    
    // 等待5秒
    yield return new WaitForSeconds(5f);
    
    Debug.Log("5秒后继续执行");
    
    // 等待下一帧
    yield return null;
    
    Debug.Log("在下一帧继续执行");
}

启动协程

void Start()
{
    // 方法1:直接启动
    StartCoroutine(MyCoroutine());
    
    // 方法2:通过字符串名称启动(不推荐,因为没有编译时检查)
    StartCoroutine("MyCoroutine");
    
    // 方法3:保存协程引用以便之后停止
    Coroutine coroutineHandle = StartCoroutine(MyCoroutine());
}

停止协程

// 方法1:通过协程引用停止特定协程
Coroutine coroutineHandle = StartCoroutine(MyCoroutine());
StopCoroutine(coroutineHandle);

// 方法2:通过方法引用停止
StopCoroutine(MyCoroutine());

// 方法3:通过字符串名称停止(不推荐)
StopCoroutine("MyCoroutine");

// 方法4:停止所有协程
StopAllCoroutines();

3. 协程的yield指令

协程使用yield return语句来指定暂停点和恢复条件。Unity提供了多种内置的yield指令:

yield return null

等待到下一帧的Update函数执行完毕后继续。

IEnumerator WaitForNextFrame()
{
    Debug.Log("当前帧");
    yield return null;
    Debug.Log("下一帧");
}

yield return new WaitForSeconds(float seconds)

等待指定的秒数后继续。这个时间是受Time.timeScale影响的。

IEnumerator WaitForFiveSeconds()
{
    Debug.Log("开始等待");
    yield return new WaitForSeconds(5f);
    Debug.Log("5秒后");
}

yield return new WaitForSecondsRealtime(float seconds)

等待指定的秒数,但不受Time.timeScale影响(真实时间)。

IEnumerator WaitRealtime()
{
    Time.timeScale = 0.5f; // 游戏时间减慢
    Debug.Log("开始等待真实时间");
    yield return new WaitForSecondsRealtime(5f);
    Debug.Log("5秒真实时间后"); // 无论timeScale如何,都是5秒后执行
}

yield return new WaitForFixedUpdate()

等待到下一次FixedUpdate执行完毕后继续。

IEnumerator WaitForPhysicsUpdate()
{
    Debug.Log("等待物理更新");
    yield return new WaitForFixedUpdate();
    Debug.Log("物理更新后");
}

yield return new WaitForEndOfFrame()

等待到当前帧中的所有摄像机和GUI渲染完成后继续。

IEnumerator CaptureScreenshot()
{
    Debug.Log("准备截图");
    yield return new WaitForEndOfFrame();
    // 此时所有渲染已完成,适合截图
    ScreenCapture.CaptureScreenshot("screenshot.png");
    Debug.Log("截图完成");
}

yield return new WaitUntil(Func<bool> predicate)

等待直到指定的条件变为true。

private bool isConditionMet = false;

IEnumerator WaitForCondition()
{
    Debug.Log("等待条件满足");
    yield return new WaitUntil(() => isConditionMet);
    Debug.Log("条件已满足");
}

// 在其他地方设置条件
public void SetCondition()
{
    isConditionMet = true;
}

yield return new WaitWhile(Func<bool> predicate)

等待直到指定的条件变为false。

private bool isProcessing = true;

IEnumerator WaitForProcessToComplete()
{
    StartProcess();
    Debug.Log("等待处理完成");
    yield return new WaitWhile(() => isProcessing);
    Debug.Log("处理已完成");
}

private void StartProcess()
{
    // 启动某个处理过程
    // 在处理完成时设置 isProcessing = false
}

yield return StartCoroutine(IEnumerator routine)

嵌套协程,等待另一个协程完成后继续。

IEnumerator MainCoroutine()
{
    Debug.Log("主协程开始");
    yield return StartCoroutine(SubCoroutine());
    Debug.Log("子协程完成后继续");
}

IEnumerator SubCoroutine()
{
    Debug.Log("子协程开始");
    yield return new WaitForSeconds(2f);
    Debug.Log("子协程完成");
}

yield return new CustomYieldInstruction

创建自定义的yield指令。

public class WaitForKeyPress : CustomYieldInstruction
{
    private KeyCode keyCode;
    
    public WaitForKeyPress(KeyCode key)
    {
        keyCode = key;
    }
    
    public override bool keepWaiting
    {
        get
        {
            return !Input.GetKeyDown(keyCode);
        }
    }
}

// 使用自定义yield指令
IEnumerator WaitForSpaceKey()
{
    Debug.Log("请按空格键继续");
    yield return new WaitForKeyPress(KeyCode.Space);
    Debug.Log("空格键已按下");
}

yield return AsyncOperation

等待异步操作完成,如场景加载、资源加载等。

IEnumerator LoadSceneAsync()
{
    Debug.Log("开始加载场景");
    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("NextScene");
    asyncLoad.allowSceneActivation = false;
    
    while (asyncLoad.progress < 0.9f)
    {
        Debug.Log("加载进度: " + (asyncLoad.progress * 100) + "%");
        yield return null;
    }
    
    Debug.Log("加载完成,等待激活");
    yield return new WaitForSeconds(1f);
    
    asyncLoad.allowSceneActivation = true;
}

4. 协程的高级用法

参数化协程

可以向协程传递参数:

IEnumerator FadeObject(GameObject obj, float duration, float targetAlpha)
{
    CanvasGroup canvasGroup = obj.GetComponent<CanvasGroup>();
    float startAlpha = canvasGroup.alpha;
    float time = 0;
    
    while (time < duration)
    {
        time += Time.deltaTime;
        canvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, time / duration);
        yield return null;
    }
    
    canvasGroup.alpha = targetAlpha;
}

// 使用
StartCoroutine(FadeObject(myPanel, 2.0f, 0.0f));

协程返回值

协程本身不能直接返回值,但可以通过回调或引用传递来获取结果:

IEnumerator CalculateWithCallback(int a, int b, System.Action<int> callback)
{
    Debug.Log("开始计算");
    yield return new WaitForSeconds(2f); // 模拟耗时操作
    
    int result = a + b;
    callback(result);
}

// 使用回调获取结果
void Start()
{
    StartCoroutine(CalculateWithCallback(5, 3, result => {
        Debug.Log("计算结果: " + result);
    }));
}

协程链

连续执行多个协程:

IEnumerator CoroutineChain()
{
    Debug.Log("开始协程链");
    
    yield return StartCoroutine(FirstTask());
    Debug.Log("第一个任务完成");
    
    yield return StartCoroutine(SecondTask());
    Debug.Log("第二个任务完成");
    
    yield return StartCoroutine(ThirdTask());
    Debug.Log("第三个任务完成");
    
    Debug.Log("协程链完成");
}

IEnumerator FirstTask() { /* ... */ }
IEnumerator SecondTask() { /* ... */ }
IEnumerator ThirdTask() { /* ... */ }

协程状态机

使用协程实现状态机:

private IEnumerator currentState;

void Start()
{
    ChangeState(IdleState());
}

void ChangeState(IEnumerator newState)
{
    if (currentState != null)
    {
        StopCoroutine(currentState);
    }
    
    currentState = newState;
    StartCoroutine(currentState);
}

IEnumerator IdleState()
{
    Debug.Log("进入空闲状态");
    
    while (!Input.GetKeyDown(KeyCode.Space))
    {
        yield return null;
    }
    
    Debug.Log("空闲状态结束");
    ChangeState(WalkingState());
}

IEnumerator WalkingState()
{
    Debug.Log("进入行走状态");
    float walkTime = 0;
    
    while (walkTime < 5f)
    {
        walkTime += Time.deltaTime;
        // 执行行走逻辑
        yield return null;
    }
    
    Debug.Log("行走状态结束");
    ChangeState(IdleState());
}

5. 协程的实际应用场景

1. 平滑动画和过渡

IEnumerator SmoothMove(Transform objectToMove, Vector3 targetPosition, float duration)
{
    Vector3 startPosition = objectToMove.position;
    float elapsedTime = 0;
    
    while (elapsedTime < duration)
    {
        objectToMove.position = Vector3.Lerp(startPosition, targetPosition, elapsedTime / duration);
        elapsedTime += Time.deltaTime;
        yield return null;
    }
    
    objectToMove.position = targetPosition;
}

2. 延时执行

IEnumerator DelayedAction(float delay, System.Action action)
{
    yield return new WaitForSeconds(delay);
    action?.Invoke();
}

// 使用
StartCoroutine(DelayedAction(3f, () => {
    Debug.Log("3秒后执行");
}));

3. 分帧处理大量数据

IEnumerator ProcessLargeDataSet(List<Data> dataSet)
{
    int processedCount = 0;
    int totalCount = dataSet.Count;
    
    // 每帧处理10个数据项
    const int itemsPerFrame = 10;
    
    while (processedCount < totalCount)
    {
        int itemsToProcess = Mathf.Min(itemsPerFrame, totalCount - processedCount);
        
        for (int i = 0; i < itemsToProcess; i++)
        {
            ProcessDataItem(dataSet[processedCount + i]);
        }
        
        processedCount += itemsToProcess;
        
        // 更新进度
        float progress = (float)processedCount / totalCount;
        UpdateProgressBar(progress);
        
        yield return null; // 等待下一帧
    }
    
    Debug.Log("所有数据处理完成");
}

4. 异步加载资源

IEnumerator LoadAssetAsync<T>(string path, System.Action<T> onLoaded) where T : UnityEngine.Object
{
    ResourceRequest request = Resources.LoadAsync<T>(path);
    
    while (!request.isDone)
    {
        Debug.Log($"加载进度: {request.progress * 100}%");
        yield return null;
    }
    
    T asset = request.asset as T;
    onLoaded?.Invoke(asset);
}

5. 序列化游戏事件

IEnumerator GameIntroSequence()
{
    // 淡入场景
    yield return StartCoroutine(FadeScreen(0f, 1f, 2f));
    
    // 显示标题
    titleText.gameObject.SetActive(true);
    yield return new WaitForSeconds(3f);
    
    // 播放介绍动画
    introAnimation.Play();
    yield return new WaitForSeconds(introAnimation.clip.length);
    
    // 显示"按任意键继续"
    pressAnyKeyText.gameObject.SetActive(true);
    
    // 等待玩家输入
    yield return new WaitUntil(() => Input.anyKeyDown);
    
    // 开始游戏
    StartGame();
}

6. 实现计时器

private Coroutine timerCoroutine;

public void StartTimer(float duration)
{
    if (timerCoroutine != null)
    {
        StopCoroutine(timerCoroutine);
    }
    
    timerCoroutine = StartCoroutine(TimerCoroutine(duration));
}

IEnumerator TimerCoroutine(float duration)
{
    float remainingTime = duration;
    
    while (remainingTime > 0)
    {
        UpdateTimerDisplay(remainingTime);
        yield return null;
        remainingTime -= Time.deltaTime;
    }
    
    remainingTime = 0;
    UpdateTimerDisplay(remainingTime);
    OnTimerComplete();
}

6. 协程的性能考虑

性能优势

  • 避免了每帧重复检查条件的开销
  • 不需要维护复杂的状态变量
  • 代码更清晰,逻辑更连贯

性能注意事项

  1. 协程创建开销:启动协程有一定开销,不应在每帧大量创建
  2. GC压力:yield指令可能产生垃圾回收压力
  3. 过多活跃协程:同时运行太多协程会影响性能
  4. 协程嵌套深度:过深的嵌套可能导致栈溢出

优化技巧

重用WaitForSeconds对象以减少GC

// 重用WaitForSeconds对象以减少GC
private readonly WaitForSeconds wait1s = new WaitForSeconds(1f);

IEnumerator OptimizedCoroutine()
{
    // 使用缓存的对象而不是每次创建新对象
    yield return wait1s;
    
    // 其他代码...
}

对象池复用协程

// 对象池复用协程(示例)
public class CoroutinePool {
    static Queue<IEnumerator> pool = new Queue<IEnumerator>(10);
    
    public static IEnumerator Get() {
        return pool.Count > 0 ? pool.Dequeue() : new WaitForEndOfFrame();
    }
    
    public static void Release(IEnumerator coroutine) {
        pool.Enqueue(coroutine);
    }
}

7. 协程的限制和注意事项

主要限制

  1. 不能直接返回值:协程不能像普通函数那样返回值
  2. 异常处理:协程中的异常可能难以捕获和处理
  3. 不是真正的异步:协程仍在主线程上运行,不能执行真正的并行操作
  4. 生命周期绑定:协程绑定到MonoBehaviour,如果对象被禁用或销毁,协程会停止

常见陷阱

  1. 忘记启动协程:定义了协程但忘记用StartCoroutine启动
  2. 忘记yield:协程中没有yield语句会立即执行完毕
  3. 协程停止但没有清理:协程被停止时没有适当清理资源
  4. 在禁用的GameObject上启动协程:这样的协程不会执行
// 错误示例:在禁用的对象上启动协程
gameObject.SetActive(false);
StartCoroutine(MyCoroutine()); // 不会执行

// 正确做法:确保对象处于激活状态
gameObject.SetActive(true);
StartCoroutine(MyCoroutine());

8. 协程与其他异步模式的比较

协程 vs Unity事件

  • 协程:适合顺序执行的操作,代码更线性
  • 事件:适合响应式编程,组件间解耦

协程 vs C# async/await

// 协程方式
IEnumerator LoadDataCoroutine()
{
    Debug.Log("开始加载");
    yield return StartCoroutine(FetchData());
    Debug.Log("加载完成");
}

// async/await方式 (需要Unity 2017+)
async void LoadDataAsync()
{
    Debug.Log("开始加载");
    await FetchDataAsync();
    Debug.Log("加载完成");
}

主要区别:

  • async/await更现代,支持直接返回值
  • 协程更适合Unity的生命周期
  • async/await有更好的异常处理
  • 协程与Unity的其他系统集成更好

协程 vs UniTask

UniTask是一个第三方库,提供了更强大的异步编程支持:

// 使用UniTask
async UniTask LoadResourcesAsync()
{
    var texture = await Resources.LoadAsync<Texture>("texture");
    var sprite = await Resources.LoadAsync<Sprite>("sprite");
    
    // 可以直接获取结果
    myImage.texture = texture.asset as Texture;
    mySprite.sprite = sprite.asset as Sprite;
}

UniTask优势:

  • 零GC分配
  • 取消支持
  • 更好的异常处理
  • 与协程兼容但性能更好

9. 实际案例:完整的协程管理系统

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CoroutineManager : MonoBehaviour
{
    private static CoroutineManager _instance;
    
    public static CoroutineManager Instance
    {
        get
        {
            if (_instance == null)
            {
                GameObject go = new GameObject("CoroutineManager");
                _instance = go.AddComponent<CoroutineManager>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }
    
    private Dictionary<string, List<Coroutine>> namedCoroutines = new Dictionary<string, List<Coroutine>>();
    
    // 启动并跟踪协程
    public Coroutine RunCoroutine(string name, IEnumerator routine)
    {
        Coroutine coroutine = StartCoroutine(routine);
        
        if (!namedCoroutines.ContainsKey(name))
        {
            namedCoroutines[name] = new List<Coroutine>();
        }
        
        namedCoroutines[name].Add(coroutine);
        return coroutine;
    }
    
    // 停止特定名称的所有协程
    public void StopCoroutines(string name)
    {
        if (namedCoroutines.TryGetValue(name, out List<Coroutine> coroutines))
        {
            foreach (Coroutine coroutine in coroutines)
            {
                if (coroutine != null)
                {
                    StopCoroutine(coroutine);
                }
            }
            
            coroutines.Clear();
        }
    }
    
    // 延迟执行
    public Coroutine DelayedCall(float delay, System.Action action, string name = "")
    {
        return string.IsNullOrEmpty(name) 
            ? StartCoroutine(DelayedCallRoutine(delay, action)) 
            : RunCoroutine(name, DelayedCallRoutine(delay, action));
    }
    
    private IEnumerator DelayedCallRoutine(float delay, System.Action action)
    {
        yield return new WaitForSeconds(delay);
        action?.Invoke();
    }
    
    // 重复执行
    public Coroutine RepeatCall(float interval, System.Action action, int repeatCount = -1, string name = "")
    {
        return string.IsNullOrEmpty(name)
            ? StartCoroutine(RepeatCallRoutine(interval, action, repeatCount))
            : RunCoroutine(name, RepeatCallRoutine(interval, action, repeatCount));
    }
    
    private IEnumerator RepeatCallRoutine(float interval, System.Action action, int repeatCount)
    {
        int count = 0;
        while (repeatCount < 0 || count < repeatCount)
        {
            yield return new WaitForSeconds(interval);
            action?.Invoke();
            count++;
        }
    }
    
    // 平滑值变化
    public Coroutine LerpValue(float from, float to, float duration, System.Action<float> onUpdate, 
                              System.Action onComplete = null, string name = "")
    {
        return string.IsNullOrEmpty(name)
            ? StartCoroutine(LerpValueRoutine(from, to, duration, onUpdate, onComplete))
            : RunCoroutine(name, LerpValueRoutine(from, to, duration, onUpdate, onComplete));
    }
    
    private IEnumerator LerpValueRoutine(float from, float to, float duration, 
                                        System.Action<float> onUpdate, System.Action onComplete)
    {
        float time = 0;
        while (time < duration)
        {
            float t = time / duration;
            float value = Mathf.Lerp(from, to, t);
            onUpdate?.Invoke(value);
            
            time += Time.deltaTime;
            yield return null;
        }
        
        onUpdate?.Invoke(to);
        onComplete?.Invoke();
    }
    
    // 序列执行多个操作
    public Coroutine Sequence(IEnumerable<IEnumerator> actions, string name = "")
    {
        return string.IsNullOrEmpty(name)
            ? StartCoroutine(SequenceRoutine(actions))
            : RunCoroutine(name, SequenceRoutine(actions));
    }
    
    private IEnumerator SequenceRoutine(IEnumerable<IEnumerator> actions)
    {
        foreach (IEnumerator action in actions)
        {
            yield return StartCoroutine(action);
        }
    }
}

使用示例:

void Start()
{
    // 延迟调用
    CoroutineManager.Instance.DelayedCall(2f, () => {
        Debug.Log("2秒后执行");
    }, "delayedActions");
    
    // 重复调用
    CoroutineManager.Instance.RepeatCall(1f, () => {
        Debug.Log("每秒执行一次");
    }, 5, "repeatedActions");
    
    // 平滑变化值
    CoroutineManager.Instance.LerpValue(0f, 1f, 3f, 
        value => { fadeImage.color = new Color(1, 1, 1, value); },
        () => { Debug.Log("淡入完成"); },
        "fadeEffect");
    
    // 序列执行
    List<IEnumerator> sequence = new List<IEnumerator>
    {
        FadeIn(2f),
        ShowMessage(3f),
        FadeOut(2f)
    };
    
    CoroutineManager.Instance.Sequence(sequence, "introSequence");
}

// 在需要时停止特定组的协程
void OnDisable()
{
    CoroutineManager.Instance.StopCoroutines("introSequence");
}

10. 总结

Unity的协程是一种强大而灵活的工具,适用于各种需要随时间推移执行的任务。它们提供了一种清晰、线性的方式来编写异步代码,而不需要复杂的状态管理。

协程的主要优势

  • 简化时序操作:轻松实现延迟、动画和序列化操作
  • 代码可读性:使异步逻辑更线性、更易理解
  • 与Unity集成:与Unity的生命周期和系统紧密集成
  • 灵活性:可以暂停、恢复和取消操作
  • 低开销:比线程更轻量级

何时使用协程

  • 实现随时间推移的效果和动画
  • 延迟执行操作
  • 分帧处理大量数据
  • 等待异步操作完成
  • 实现游戏事件序列
  • 创建状态机和AI行为

何时避免使用协程

  • 需要真正并行执行的CPU密集型任务
  • 需要直接返回值的操作(考虑使用回调或async/await)
  • 需要精确控制执行时机的操作
  • 需要复杂错误处理的操作

协程是Unity开发中不可或缺的工具,掌握它们可以让你的代码更清晰、更高效,并能实现各种复杂的时序效果和游戏机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值