从零手搓一个简易版Unity协程调度器,彻底搞懂yield return背后的机制
从零手搓一个简易版Unity协程调度器,彻底搞懂yield return背后的机制
在Unity开发中,协程(Coroutine)是处理异步逻辑的利器,但你是否想过Unity是如何在底层驱动这些看似"暂停"又"恢复"的协程?本文将带你从零实现一个迷你协程调度器,通过造轮子的方式深入理解yield return背后的状态机原理。
1. 协程的本质:迭代器与状态机
协程并非Unity的魔法,而是建立在C#迭代器(IEnumerator)基础上的语法糖。每次yield return都会将当前执行状态"冻结",等待下次唤醒。让我们先看一个最简单的协程示例:
IEnumerator SimpleCoroutine() { Debug.Log("第一步"); yield return null; // 暂停点1 Debug.Log("第二步"); yield return new WaitForSeconds(1f); // 暂停点2 Debug.Log("第三步"); }这段代码实际上会被编译器转换为一个状态机类,大致结构如下:
class <SimpleCoroutine>d__0 : IEnumerator { private int <>1__state; // 状态标识 private object <>2__current; // 当前返回对象 bool MoveNext() { switch (<>1__state) { case 0: Debug.Log("第一步"); <>1__state = 1; <>2__current = null; // 对应第一个yield return return true; case 1: Debug.Log("第二步"); <>1__state = 2; <>2__current = new WaitForSeconds(1f); return true; case 2: Debug.Log("第三步"); return false; // 协程结束 } return false; } }关键点:
- 每个
yield return对应一个状态编号 MoveNext()驱动状态机前进- 返回值通过
Current属性暴露
2. 构建基础协程调度器
现在我们来实现一个最简调度器,核心功能是维护一个运行中的协程列表,并在每帧驱动它们前进:
public class MiniCoroutineScheduler : MonoBehaviour { private List<IEnumerator> runningCoroutines = new List<IEnumerator>(); public void StartMiniCoroutine(IEnumerator coroutine) { runningCoroutines.Add(coroutine); } void Update() { for (int i = 0; i < runningCoroutines.Count; ) { IEnumerator coroutine = runningCoroutines[i]; if (!coroutine.MoveNext()) { runningCoroutines.RemoveAt(i); continue; } object yielded = coroutine.Current; // 这里处理不同类型的yield指令 i++; } } }基本用法:
// 替代Unity原生的StartCoroutine scheduler.StartMiniCoroutine(MyCoroutine());3. 实现常见YieldInstruction
Unity内置了多种YieldInstruction,我们来模拟最常用的几种:
3.1 WaitForSeconds
class MiniWaitForSeconds { public float WaitTime { get; private set; } private float timer; public MiniWaitForSeconds(float seconds) { WaitTime = seconds; timer = 0f; } public bool Tick(float deltaTime) { timer += deltaTime; return timer >= WaitTime; } } // 在调度器中处理: if (yielded is MiniWaitForSeconds wait) { if (!wait.Tick(Time.deltaTime)) { // 时间未到,保持当前状态 continue; } }3.2 WaitForEndOfFrame
class MiniWaitForEndOfFrame { /* 标记对象 */ } // 在调度器LateUpdate中: void LateUpdate() { for (int i = 0; i < endOfFrameCoroutines.Count; ) { if (endOfFrameCoroutines[i].MoveNext()) { i++; } else { endOfFrameCoroutines.RemoveAt(i); } } }3.3 WaitUntil/WaitWhile
class MiniWaitUntil { private Func<bool> predicate; public MiniWaitUntil(Func<bool> predicate) { this.predicate = predicate; } public bool ShouldContinue() => predicate(); } // 调度器处理: if (yielded is MiniWaitUntil until) { if (!until.ShouldContinue()) { continue; } }4. 高级功能实现
4.1 协程嵌套
处理yield return StartCoroutine()的情况:
if (yielded is IEnumerator nestedCoroutine) { runningCoroutines[i] = nestedCoroutine; continue; }4.2 协程取消
添加停止协程的能力:
private Dictionary<IEnumerator, CoroutineHandle> handles = new Dictionary<IEnumerator, CoroutineHandle>(); public struct CoroutineHandle { public IEnumerator Coroutine; } public CoroutineHandle StartMiniCoroutine(IEnumerator coroutine) { var handle = new CoroutineHandle { Coroutine = coroutine }; handles[coroutine] = handle; runningCoroutines.Add(coroutine); return handle; } public void StopMiniCoroutine(CoroutineHandle handle) { if (handles.TryGetValue(handle.Coroutine, out var h) && h.Equals(handle)) { runningCoroutines.Remove(handle.Coroutine); handles.Remove(handle.Coroutine); } }4.3 异常处理
增强调度器的健壮性:
bool MoveNextWithExceptionHandling(IEnumerator coroutine) { try { return coroutine.MoveNext(); } catch (Exception e) { Debug.LogError($"协程异常: {e}"); return false; } }5. 性能优化实践
5.1 对象池优化
频繁创建的YieldInstruction可以使用对象池:
static class YieldPool { private static readonly Queue<MiniWaitForSeconds> waitForSecondsPool = new Queue<MiniWaitForSeconds>(); public static MiniWaitForSeconds WaitForSeconds(float time) { if (waitForSecondsPool.Count > 0) { var wait = waitForSecondsPool.Dequeue(); wait.WaitTime = time; wait.timer = 0f; return wait; } return new MiniWaitForSeconds(time); } public static void Release(MiniWaitForSeconds wait) { waitForSecondsPool.Enqueue(wait); } }5.2 分帧处理
大量协程时分帧执行避免卡顿:
int coroutinesPerFrame = 10; // 每帧最多执行10个 void Update() { int processed = 0; for (int i = 0; i < runningCoroutines.Count && processed < coroutinesPerFrame; ) { // ...原有处理逻辑... processed++; } }6. 与Unity生命周期的整合
要让我们的调度器像Unity原生协程一样响应游戏状态变化:
void OnDisable() { // 暂停所有协程 pausedCoroutines.AddRange(runningCoroutines); runningCoroutines.Clear(); } void OnEnable() { // 恢复暂停的协程 runningCoroutines.AddRange(pausedCoroutines); pausedCoroutines.Clear(); } void OnDestroy() { // 清理资源 runningCoroutines.Clear(); pausedCoroutines.Clear(); }7. 实战:用自制调度器实现常见模式
7.1 对象渐隐效果
IEnumerator FadeOut(SpriteRenderer renderer, float duration) { float elapsed = 0f; Color color = renderer.color; while (elapsed < duration) { elapsed += Time.deltaTime; color.a = Mathf.Lerp(1f, 0f, elapsed / duration); renderer.color = color; yield return null; } color.a = 0f; renderer.color = color; }7.2 分帧加载
IEnumerator LoadAssetsInFrames(List<string> assetPaths) { foreach (var path in assetPaths) { var asset = Resources.Load(path); // 处理加载的资源... yield return null; // 每加载一个资源后让出一帧 } }7.3 超时控制
IEnumerator DownloadWithTimeout(string url, float timeout) { var request = UnityWebRequest.Get(url); request.SendWebRequest(); float startTime = Time.time; while (!request.isDone) { if (Time.time - startTime > timeout) { Debug.LogError("下载超时"); yield break; } yield return null; } if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"下载失败: {request.error}"); } else { Debug.Log($"下载完成: {request.downloadHandler.text}"); } }通过这个自制协程调度器的实现过程,我们不仅理解了Unity协程的底层机制,还获得了对异步编程更深入的认识。这种"造轮子"的实践方式往往能带来比单纯阅读文档更深刻的理解。
