Unity 中的 IEnumerator协程详解

Unity 中的 IEnumerator 是 C# 迭代器接口,主要用于实现 协程(Coroutines),这是 Unity 中处理异步操作和时间控制的核心机制。

基本概念

1. 什么是协程?

协程是一种特殊的函数,可以在执行过程中暂停,并在稍后恢复执行,而不是一次性执行完毕。

2. 基本语法

csharp

复制代码
using System.Collections;
using UnityEngine;

public class CoroutineExample : MonoBehaviour
{
    void Start()
    {
        // 启动协程
        StartCoroutine(MyCoroutine());
    }
    
    IEnumerator MyCoroutine()
    {
        Debug.Log("协程开始");
        
        // 暂停1秒
        yield return new WaitForSeconds(1f);
        
        Debug.Log("1秒后");
        
        // 暂停到下一帧
        yield return null;
        
        Debug.Log("下一帧");
    }
}

常用 yield 指令

时间控制

csharp

复制代码
IEnumerator TimeControls()
{
    // 等待1秒
    yield return new WaitForSeconds(1f);
    
    // 等待固定物理更新帧
    yield return new WaitForFixedUpdate();
    
    // 等待帧结束
    yield return new WaitForEndOfFrame();
    
    // 等待真实时间(不受Time.timeScale影响)
    yield return new WaitForSecondsRealtime(1f);
}

条件等待

csharp

复制代码
IEnumerator WaitForConditions()
{
    // 等待直到条件为真
    yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
    
    // 等待当条件为真时
    yield return new WaitWhile(() => Input.GetKey(KeyCode.Space));
    
    // 等待异步操作完成
    AsyncOperation asyncOp = SceneManager.LoadSceneAsync("SceneName");
    yield return asyncOp;
}

实际应用示例

1. 延迟执行

csharp

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

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

2. 序列动画

csharp

复制代码
IEnumerator SequenceAnimation()
{
    transform.position = startPos;
    
    yield return new WaitForSeconds(0.5f);
    
    // 移动到目标位置(耗时1秒)
    float elapsed = 0f;
    while (elapsed < 1f)
    {
        transform.position = Vector3.Lerp(startPos, targetPos, elapsed);
        elapsed += Time.deltaTime;
        yield return null;  // 每帧执行
    }
    
    transform.position = targetPos;
}

3. 分帧处理大数据

csharp

复制代码
IEnumerator ProcessLargeData(List<GameObject> objects)
{
    for (int i = 0; i < objects.Count; i++)
    {
        // 处理每个对象
        ProcessObject(objects[i]);
        
        // 每处理10个对象等待一帧,避免卡顿
        if (i % 10 == 0)
            yield return null;
    }
}

4. 网络请求

csharp

复制代码
IEnumerator LoadDataFromServer(string url)
{
    UnityWebRequest request = UnityWebRequest.Get(url);
    yield return request.SendWebRequest();
    
    if (request.result == UnityWebRequest.Result.Success)
    {
        Debug.Log("数据加载成功: " + request.downloadHandler.text);
    }
    else
    {
        Debug.LogError("加载失败: " + request.error);
    }
}

协程控制方法

csharp

复制代码
public class CoroutineControl : MonoBehaviour
{
    private Coroutine myCoroutine;
    
    void Start()
    {
        // 启动协程并保存引用
        myCoroutine = StartCoroutine(RunningCoroutine());
        
        // 带参数的协程
        StartCoroutine(ParameterCoroutine("参数", 123));
    }
    
    void StopMyCoroutine()
    {
        if (myCoroutine != null)
        {
            // 停止特定协程
            StopCoroutine(myCoroutine);
            
            // 或停止所有协程
            // StopAllCoroutines();
        }
    }
    
    IEnumerator RunningCoroutine()
    {
        while (true)
        {
            Debug.Log("运行中...");
            yield return new WaitForSeconds(1f);
        }
    }
    
    IEnumerator ParameterCoroutine(string text, int number)
    {
        Debug.Log($"参数: {text}, {number}");
        yield return null;
    }
}

嵌套协程

csharp

复制代码
IEnumerator MainCoroutine()
{
    Debug.Log("主协程开始");
    
    // 等待子协程完成
    yield return StartCoroutine(SubCoroutine());
    
    Debug.Log("子协程完成");
}

IEnumerator SubCoroutine()
{
    yield return new WaitForSeconds(2f);
    Debug.Log("子协程执行");
}

注意事项

1. 性能考虑

csharp

复制代码
// 避免:每帧创建新的 WaitForSeconds
IEnumerator BadPerformance()
{
    while (true)
    {
        yield return new WaitForSeconds(1f); // 每循环都创建新对象
    }
}

// 推荐:缓存 WaitForSeconds
IEnumerator BetterPerformance()
{
    WaitForSeconds wait = new WaitForSeconds(1f);
    while (true)
    {
        yield return wait; // 复用对象
    }
}

2. 协程生命周期

  • 协程在 GameObject 被禁用时不会自动停止

  • 协程在 GameObject 被销毁时会自动停止

  • 协程在场景切换时会被销毁

3. 错误处理

csharp

复制代码
IEnumerator SafeCoroutine()
{
    try
    {
        yield return StartCoroutine(RiskyOperation());
    }
    catch (System.Exception e)
    {
        Debug.LogError($"协程错误: {e.Message}");
    }
}

高级用法

自定义 YieldInstruction

csharp

复制代码
public class WaitForCustomCondition : CustomYieldInstruction
{
    private System.Func<bool> predicate;
    
    public override bool keepWaiting => !predicate();
    
    public WaitForCustomCondition(System.Func<bool> predicate)
    {
        this.predicate = predicate;
    }
}

// 使用
yield return new WaitForCustomCondition(() => player.IsReady);

协程管理器

csharp

复制代码
public static class CoroutineManager
{
    public static Coroutine Start(IEnumerator coroutine)
    {
        GameObject obj = new GameObject("CoroutineRunner");
        MonoBehaviour runner = obj.AddComponent<MonoBehaviourRunner>();
        Coroutine routine = runner.StartCoroutine(coroutine);
        
        // 协程完成后自动销毁
        runner.StartCoroutine(Cleanup(routine, obj));
        return routine;
    }
    
    static IEnumerator Cleanup(Coroutine coroutine, GameObject obj)
    {
        yield return coroutine;
        GameObject.Destroy(obj);
    }
    
    class MonoBehaviourRunner : MonoBehaviour { }
}

常见问题解决

协程不执行?

  • 检查是否调用了 StartCoroutine()

  • 确保 MonoBehaviour 脚本已启用

  • 检查 yield 指令是否正确

协程内存泄漏?

csharp

复制代码
void OnDestroy()
{
    // 在对象销毁时停止所有协程
    StopAllCoroutines();
}

协程是 Unity 中非常强大的工具,合理使用可以简化很多异步操作的实现,让代码更加清晰易读。

补充与优化

一、性能优化策略

1. 缓存 Yield 指令(关键优化)

csharp

复制代码
public class OptimizedCoroutines : MonoBehaviour
{
    // ❌ 坏做法:每次循环都创建新对象
    IEnumerator BadWay()
    {
        while (true)
        {
            yield return new WaitForSeconds(1f); // GC 分配
        }
    }
    
    // ✅ 好做法:缓存重用
    IEnumerator GoodWay()
    {
        var waitOneSecond = new WaitForSeconds(1f);
        var waitEndOfFrame = new WaitForEndOfFrame();
        var waitFixedUpdate = new WaitForFixedUpdate();
        
        while (true)
        {
            yield return waitOneSecond; // 无GC分配
        }
    }
    
    // 🚀 高级优化:预定义常用时间间隔
    private static readonly Dictionary<float, WaitForSeconds> _waitCache = 
        new Dictionary<float, WaitForSeconds>();
    
    public static WaitForSeconds GetWait(float seconds)
    {
        if (!_waitCache.TryGetValue(seconds, out var wait))
        {
            wait = new WaitForSeconds(seconds);
            _waitCache[seconds] = wait;
        }
        return wait;
    }
    
    IEnumerator OptimizedWay()
    {
        yield return GetWait(0.5f);
        yield return GetWait(1f);
        yield return GetWait(0.5f); // 复用缓存
    }
}

2. 避免每帧的协程开销

csharp

复制代码
public class UpdateVsCoroutine : MonoBehaviour
{
    // ❌ 协程每帧的开销 > Update
    IEnumerator CoroutineUpdate()
    {
        while (true)
        {
            // 每帧执行的逻辑
            transform.Rotate(Vector3.up * Time.deltaTime * 90);
            yield return null; // 分配开销
        }
    }
    
    // ✅ 对于简单每帧逻辑,使用 Update 更高效
    void Update()
    {
        transform.Rotate(Vector3.up * Time.deltaTime * 90);
    }
    
    // 🎯 最佳实践:协程适合不连续的时间操作
    IEnumerator ProperUse()
    {
        // 延迟执行
        yield return GetWait(2f);
        // 一段时间内的动画
        yield return MoveToPosition(Vector3.zero, 1f);
        // 等待条件
        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
    }
}

3. 批量处理与分帧

csharp

复制代码
public class BatchProcessing : MonoBehaviour
{
    // 优化大数据处理
    IEnumerator ProcessLargeBatch<T>(List<T> items, System.Action<T> process, 
                                    int batchSize = 10)
    {
        int processed = 0;
        
        for (int i = 0; i < items.Count; i++)
        {
            process(items[i]);
            processed++;
            
            // 每处理 batchSize 个等待一帧
            if (processed >= batchSize)
            {
                processed = 0;
                yield return null; // 让出一帧,避免卡顿
            }
        }
    }
    
    // 智能自适应批处理
    IEnumerator AdaptiveBatchProcess<T>(List<T> items, System.Action<T> process)
    {
        int batchSize = 10;
        float targetFrameTime = 1f / 60f; // 60FPS
        
        for (int i = 0; i < items.Count; i += batchSize)
        {
            float startTime = Time.realtimeSinceStartup;
            
            // 处理一批
            int end = Mathf.Min(i + batchSize, items.Count);
            for (int j = i; j < end; j++)
            {
                process(items[j]);
            }
            
            // 自适应调整批大小
            float elapsed = Time.realtimeSinceStartup - startTime;
            if (elapsed < targetFrameTime * 0.5f)
            {
                batchSize = Mathf.Min(batchSize * 2, 100); // 加倍,最多100
            }
            else if (elapsed > targetFrameTime)
            {
                batchSize = Mathf.Max(batchSize / 2, 1); // 减半,最少1
            }
            
            yield return null;
        }
    }
}

二、高级协程模式

1. 协程状态机模式

csharp

复制代码
public class CoroutineStateMachine : MonoBehaviour
{
    public enum State { Idle, Moving, Attacking, Dead }
    
    private State currentState;
    private Coroutine currentRoutine;
    
    void ChangeState(State newState)
    {
        if (currentRoutine != null)
            StopCoroutine(currentRoutine);
        
        currentState = newState;
        
        switch (newState)
        {
            case State.Idle:
                currentRoutine = StartCoroutine(IdleState());
                break;
            case State.Moving:
                currentRoutine = StartCoroutine(MovingState());
                break;
            case State.Attacking:
                currentRoutine = StartCoroutine(AttackingState());
                break;
        }
    }
    
    IEnumerator IdleState()
    {
        Debug.Log("进入空闲状态");
        var wait = new WaitForSeconds(Random.Range(1f, 3f));
        
        while (currentState == State.Idle)
        {
            yield return wait;
            
            // 随机切换到移动状态
            if (Random.value > 0.5f)
            {
                ChangeState(State.Moving);
                yield break;
            }
        }
    }
    
    IEnumerator MovingState() { /* ... */ }
    IEnumerator AttackingState() { /* ... */ }
}

2. 链式协程(Promise风格)

csharp

复制代码
public class CoroutineChain
{
    public static IEnumerator Sequence(params IEnumerator[] routines)
    {
        foreach (var routine in routines)
        {
            yield return routine;
        }
    }
    
    public static IEnumerator Parallel(MonoBehaviour runner, params IEnumerator[] routines)
    {
        List<Coroutine> coroutines = new List<Coroutine>();
        
        foreach (var routine in routines)
        {
            coroutines.Add(runner.StartCoroutine(routine));
        }
        
        // 等待所有协程完成
        foreach (var coroutine in coroutines)
        {
            yield return coroutine;
        }
    }
    
    // 使用示例
    IEnumerator ComplexAnimation()
    {
        yield return Sequence(
            MoveTo(new Vector3(0, 0, 0), 1f),
            Parallel(
                Rotate(360f, 1f),
                ScaleTo(Vector3.one * 2, 1f)
            ),
            WaitForSeconds(0.5f),
            FadeOut(1f)
        );
    }
    
    IEnumerator MoveTo(Vector3 target, float duration) { /* ... */ }
    IEnumerator Rotate(float angle, float duration) { /* ... */ }
    IEnumerator ScaleTo(Vector3 scale, float duration) { /* ... */ }
    IEnumerator FadeOut(float duration) { /* ... */ }
}

3. 带取消功能的协程

csharp

复制代码
public class CancellableCoroutine
{
    public class CancellationToken
    {
        public bool IsCancelled { get; private set; }
        public event System.Action OnCancel;
        
        public void Cancel()
        {
            if (!IsCancelled)
            {
                IsCancelled = true;
                OnCancel?.Invoke();
            }
        }
    }
    
    public static IEnumerator WithCancellation(IEnumerator routine, 
                                              CancellationToken token)
    {
        while (routine.MoveNext())
        {
            if (token.IsCancelled)
                yield break;
                
            yield return routine.Current;
        }
    }
    
    // 使用示例
    IEnumerator DownloadWithTimeout(string url, float timeout)
    {
        var cancellation = new CancellationToken();
        
        // 设置超时
        StartCoroutine(TimeoutCoroutine(timeout, cancellation));
        
        // 执行下载
        yield return WithCancellation(DownloadFile(url), cancellation);
    }
    
    IEnumerator TimeoutCoroutine(float delay, CancellationToken token)
    {
        yield return new WaitForSeconds(delay);
        token.Cancel();
        Debug.Log("操作超时");
    }
}

三、协程管理器系统

1. 统一协程管理

csharp

复制代码
public class CoroutineManager : MonoBehaviour
{
    private static CoroutineManager _instance;
    private Dictionary<string, Coroutine> _runningCoroutines = 
        new Dictionary<string, Coroutine>();
    
    public static CoroutineManager Instance
    {
        get
        {
            if (_instance == null)
            {
                GameObject obj = new GameObject("CoroutineManager");
                _instance = obj.AddComponent<CoroutineManager>();
                DontDestroyOnLoad(obj);
            }
            return _instance;
        }
    }
    
    // 启动并跟踪协程
    public Coroutine StartTrackedCoroutine(IEnumerator routine, string id = null)
    {
        if (string.IsNullOrEmpty(id))
            id = Guid.NewGuid().ToString();
            
        var coroutine = StartCoroutine(TrackedRoutine(routine, id));
        _runningCoroutines[id] = coroutine;
        
        return coroutine;
    }
    
    private IEnumerator TrackedRoutine(IEnumerator routine, string id)
    {
        yield return routine;
        _runningCoroutines.Remove(id);
    }
    
    // 按ID停止协程
    public bool StopTrackedCoroutine(string id)
    {
        if (_runningCoroutines.TryGetValue(id, out var coroutine))
        {
            StopCoroutine(coroutine);
            _runningCoroutines.Remove(id);
            return true;
        }
        return false;
    }
    
    // 暂停/恢复所有协程
    public void PauseAllCoroutines()
    {
        foreach (var kvp in _runningCoroutines)
        {
            // 暂停逻辑 - 需要自定义实现
            // 可以使用 Time.timeScale = 0,但会影响所有协程
        }
    }
    
    // 获取运行中协程信息
    public Dictionary<string, System.Type> GetRunningCoroutinesInfo()
    {
        // 返回协程类型信息,便于调试
        return new Dictionary<string, System.Type>();
    }
}

2. 优先级调度系统

csharp

复制代码
public class PriorityCoroutineScheduler : MonoBehaviour
{
    public enum Priority { Low, Normal, High, Critical }
    
    private class CoroutineTask
    {
        public IEnumerator Routine;
        public Priority Priority;
        public float StartTime;
        public Coroutine Coroutine;
    }
    
    private List<CoroutineTask> _pendingTasks = new List<CoroutineTask>();
    private List<CoroutineTask> _runningTasks = new List<CoroutineTask>();
    private int _maxConcurrent = 3; // 最大同时运行数
    
    public void Schedule(IEnumerator routine, Priority priority = Priority.Normal)
    {
        var task = new CoroutineTask
        {
            Routine = routine,
            Priority = priority,
            StartTime = Time.time
        };
        
        _pendingTasks.Add(task);
        _pendingTasks.Sort((a, b) => 
            a.Priority.CompareTo(b.Priority) * -1); // 降序
        
        TryStartNext();
    }
    
    private void TryStartNext()
    {
        while (_runningTasks.Count < _maxConcurrent && _pendingTasks.Count > 0)
        {
            var task = _pendingTasks[0];
            _pendingTasks.RemoveAt(0);
            
            task.Coroutine = StartCoroutine(RunTask(task));
            _runningTasks.Add(task);
        }
    }
    
    private IEnumerator RunTask(CoroutineTask task)
    {
        yield return task.Routine;
        
        _runningTasks.Remove(task);
        TryStartNext();
    }
}

四、调试与监控

1. 协程性能监控

csharp

复制代码
public class CoroutineProfiler : MonoBehaviour
{
    private class CoroutineInfo
    {
        public string Name;
        public float StartTime;
        public float TotalTime;
        public int FrameCount;
        public StackTrace CreationStackTrace;
    }
    
    private Dictionary<Coroutine, CoroutineInfo> _activeCoroutines = 
        new Dictionary<Coroutine, CoroutineInfo>();
    
    public Coroutine StartProfiledCoroutine(IEnumerator routine, string name = null)
    {
        if (name == null)
            name = routine.ToString();
            
        var coroutine = StartCoroutine(ProfiledRoutine(routine, name));
        
        _activeCoroutines[coroutine] = new CoroutineInfo
        {
            Name = name,
            StartTime = Time.realtimeSinceStartup,
            CreationStackTrace = new System.Diagnostics.StackTrace(true)
        };
        
        return coroutine;
    }
    
    private IEnumerator ProfiledRoutine(IEnumerator innerRoutine, string name)
    {
        var startTime = Time.realtimeSinceStartup;
        int frameCount = 0;
        
        while (innerRoutine.MoveNext())
        {
            frameCount++;
            yield return innerRoutine.Current;
        }
        
        float duration = Time.realtimeSinceStartup - startTime;
        Debug.Log($"协程 '{name}' 完成: {duration:F2}s, {frameCount}帧");
    }
    
    void OnGUI()
    {
        if (!Application.isEditor) return;
        
        GUILayout.BeginArea(new Rect(10, 10, 400, 300));
        GUILayout.Label("运行中协程:");
        
        foreach (var kvp in _activeCoroutines)
        {
            float duration = Time.realtimeSinceStartup - kvp.Value.StartTime;
            GUILayout.Label($"{kvp.Value.Name}: {duration:F2}s");
        }
        GUILayout.EndArea();
    }
}

2. 协程异常处理

csharp

复制代码
public class SafeCoroutineRunner : MonoBehaviour
{
    public static IEnumerator WithErrorHandling(IEnumerator routine, 
                                               System.Action<System.Exception> onError = null)
    {
        while (true)
        {
            object current;
            try
            {
                if (!routine.MoveNext())
                    yield break;
                    
                current = routine.Current;
            }
            catch (System.Exception e)
            {
                onError?.Invoke(e);
                Debug.LogError($"协程错误: {e.Message}\n{e.StackTrace}");
                yield break;
            }
            
            yield return current;
        }
    }
    
    // 重试机制
    public static IEnumerator WithRetry(IEnumerator routine, int maxRetries = 3)
    {
        int attempts = 0;
        
        while (attempts < maxRetries)
        {
            try
            {
                yield return routine;
                break; // 成功完成
            }
            catch (System.Exception e)
            {
                attempts++;
                Debug.LogWarning($"第{attempts}次尝试失败: {e.Message}");
                
                if (attempts >= maxRetries)
                {
                    Debug.LogError($"达到最大重试次数");
                    throw;
                }
                
                yield return new WaitForSeconds(Mathf.Pow(2, attempts)); // 指数退避
            }
        }
    }
}

五、Unity 2021+ 新特性

1. UniTask 替代方案(第三方库)

csharp

复制代码
// UniTask 比协程更高效,支持 async/await
using Cysharp.Threading.Tasks;

public class UniTaskExample : MonoBehaviour
{
    async UniTaskVoid Start()
    {
        // 并行执行多个异步任务
        await UniTask.WhenAll(
            LoadAssetAsync("Prefabs/Character"),
            LoadSceneAsync("Level1"),
            WaitForSecondsAsync(1f)
        );
        
        // 取消支持
        var cancellationToken = this.GetCancellationTokenOnDestroy();
        await MoveToPositionAsync(Vector3.zero, 1f, cancellationToken);
    }
    
    async UniTask LoadAssetAsync(string path)
    {
        // 异步加载资源
        await Resources.LoadAsync<GameObject>(path);
    }
    
    async UniTask WaitForSecondsAsync(float seconds)
    {
        await UniTask.Delay((int)(seconds * 1000));
    }
}

2. C# 8.0 Async Streams

csharp

复制代码
// Unity 2021.2+ 支持 C# 8.0
public async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i < 20; i++)
    {
        await UniTask.Delay(100); // 模拟异步操作
        yield return i;
    }
}

async UniTaskVoid ConsumeAsyncStream()
{
    await foreach (var number in GenerateSequence())
    {
        Debug.Log(number);
    }
}

六、最佳实践总结

DOs(推荐做)

  1. 缓存常用 Wait 对象减少GC

  2. 使用协程处理异步/时间相关逻辑

  3. 为长时间协程添加取消支持

  4. 使用协程管理器统一管理

  5. 添加错误处理防止崩溃传播

  6. 分帧处理大数据避免卡顿

DON'Ts(避免做)

  1. 避免每帧都用协程,简单逻辑用Update

  2. 不要创建大量短期协程,复用已有的

  3. 避免在协程中直接修改已销毁对象

  4. 不要依赖协程精确时序,Unity不是实时系统

  5. 避免多层嵌套协程,难以维护和调试

进阶建议

  1. 🚀 考虑使用 UniTask 替代协程获得更好性能

  2. 🚀 实现协程优先级调度优化资源使用

  3. 🚀 添加协程监控便于调试和性能分析

  4. 🚀 使用状态机模式管理复杂协程逻辑

这些优化策略和高级模式可以帮助你构建更健壮、高性能的Unity应用。根据项目规模和需求选择合适的方案。

相关推荐
bugcome_com1 小时前
C# 程序结构详解:从 Hello World 开始
c#
淡海水2 小时前
【节点】[Branch节点]原理解析与实际应用
unity·游戏引擎·shadergraph·图形·branch
唐梓航-求职中2 小时前
编程-技术-算法-leetcode-288. 单词的唯一缩写
算法·leetcode·c#
在路上看风景2 小时前
4.6 显存和缓存
unity
bugcome_com4 小时前
阿里云 OSS C# SDK 使用实践与参数详解
阿里云·c#
Zik----4 小时前
简单的Unity漫游场景搭建
unity·游戏引擎
懒人咖14 小时前
缺料分析时携带用料清单的二开字段
c#·金蝶云星空
在路上看风景15 小时前
4.5 顶点和片元
unity
bugcome_com15 小时前
深入了解 C# 编程环境及其开发工具
c#
wfserial17 小时前
c#使用微软自带speech选择男声仍然是女声的一种原因
microsoft·c#·speech