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(推荐做)
-
✅ 缓存常用 Wait 对象减少GC
-
✅ 使用协程处理异步/时间相关逻辑
-
✅ 为长时间协程添加取消支持
-
✅ 使用协程管理器统一管理
-
✅ 添加错误处理防止崩溃传播
-
✅ 分帧处理大数据避免卡顿
DON'Ts(避免做)
-
❌ 避免每帧都用协程,简单逻辑用Update
-
❌ 不要创建大量短期协程,复用已有的
-
❌ 避免在协程中直接修改已销毁对象
-
❌ 不要依赖协程精确时序,Unity不是实时系统
-
❌ 避免多层嵌套协程,难以维护和调试
进阶建议
-
🚀 考虑使用 UniTask 替代协程获得更好性能
-
🚀 实现协程优先级调度优化资源使用
-
🚀 添加协程监控便于调试和性能分析
-
🚀 使用状态机模式管理复杂协程逻辑
这些优化策略和高级模式可以帮助你构建更健壮、高性能的Unity应用。根据项目规模和需求选择合适的方案。