
1. 项目概述为什么Unity开发者绕不开迭代器模式如果你在Unity里写过协程用过foreach循环遍历一个ListGameObject或者在Update里处理一个需要分帧执行的复杂逻辑那么恭喜你你已经和迭代器模式打过交道了只是可能没意识到它的“学名”。这个听起来有点“设计模式”范儿的词其实就藏在Unity日常开发最核心的流程控制里。简单来说迭代器模式的核心思想是提供一种方法让你能顺序访问一个聚合对象比如数组、列表、集合中的所有元素而又不需要知道这个对象内部是怎么存储和组织的。在C#和Unity中这主要通过IEnumerator接口和yield关键字来实现。它解决的痛点非常直接当你的数据集合结构复杂或者遍历过程本身需要复杂的控制比如分帧、延迟、条件跳过时传统的for或while循环会显得笨重且耦合度高。迭代器模式将“遍历”这个行为抽象出来让集合和遍历逻辑可以独立变化。对于Unity开发者而言掌握迭代器模式远不止是为了应对面试题。它是理解协程Coroutine工作原理的基石是优化性能、实现流畅游戏体验如分帧加载、AI行为序列的关键工具也是编写更清晰、更易维护的代码的有效手段。很多新手觉得协程很“魔法”其实剥开yield return new WaitForSeconds(1)这层糖衣里面就是迭代器在驱动。接下来我们就从最基础的原理开始彻底拆解它在Unity中的实现与应用。1.1 核心需求解析从“遍历”到“控制”在Unity游戏开发中我们使用迭代器模式通常源于以下几类非常具体的需求而不仅仅是理论上的“解耦”1. 对复杂遍历逻辑的封装想象一下你需要遍历场景中所有敌人但只攻击血量低于30%且不在冷却状态的目标。用普通循环你的Update里可能会塞满if判断代码又臭又长。而迭代器允许你将“找到下一个合格目标”的逻辑封装成一个独立的迭代器方法主循环只需要不断调用MoveNext()代码意图瞬间清晰。2. 实现非阻塞的延时与等待协程的灵魂这是Unity中迭代器模式最闪耀的应用。游戏是实时运行的你不能让一个加载1000个道具的循环卡住主线程一秒钟。通过yield return不同的等待指令如WaitForSeconds,WaitForEndOfFrame,AsyncOperation迭代器可以将一个长任务分解成多个小步骤在多个帧中逐步执行从而保持游戏画面的流畅响应。这就是协程。3. 处理动态或无限的数据流比如一个随机地图生成器你不需要事先生成所有地块可以“按需生成”每帧生成一块。迭代器可以作为一个“生成器”Generator每次调用返回下一个新生成的地块数据直到满足条件为止。这对于内存敏感的场景如大型开放世界非常有用。4. 统一访问接口简化代码当你设计一个关卡系统里面可能有ListWave波次、Dictionaryint, SpawnPoint出生点、自定义的Round集合等多种集合。如果它们都实现了相同的迭代接口那么你写一个通用的StartLevel()方法就可以用同样的方式遍历所有关卡元素而不必关心每个集合的具体类型。理解这些需求就能明白为什么Unity官方大量使用IEnumerator以及为什么它成为中级开发者必须掌握的技能。它不是“可有可无”的设计模式而是解决实际性能与逻辑问题的利器。2. 核心原理与C#/Unity实现机制要玩转Unity中的迭代器必须吃透C#语言层面提供的两个核心机制IEnumerator/IEnumerable接口以及yield上下文关键字。很多教程只讲yield的用法却不讲背后的状态机导致开发者遇到复杂情况就抓瞎。2.1 IEnumerator与IEnumerable迭代的契约在C#中迭代器模式通过一组标准的接口来定义IEnumerator迭代器这是真正的“遍历器”。它定义了如何逐个访问集合中的元素。bool MoveNext(): 将迭代器推进到集合的下一个元素。如果成功推进返回true如果集合已遍历完毕返回false。object Current { get; }: 获取集合中位于迭代器当前位置的元素。在调用MoveNext()之前或MoveNext()返回false之后访问Current是未定义行为通常抛出异常。void Reset(): 将迭代器重置到初始位置位于第一个元素之前。这个接口方法在实践中很少被实现通常直接抛出NotSupportedException。IEnumerable可枚举集合表示一个可以产生迭代器的集合。IEnumerator GetEnumerator(): 返回一个用于遍历该集合的迭代器实例。它们的关系是一个集合IEnumerable可以提供一个或多个迭代器IEnumerator来遍历自己。foreach循环就是基于这个契约的语法糖。当你写foreach (var item in myList)时编译器实质上会将其转换为类似下面的代码IEnumerator enumerator myList.GetEnumerator(); try { while (enumerator.MoveNext()) { var item (YourType)enumerator.Current; // foreach循环中声明的变量 // 循环体 } } finally { IDisposable disposable enumerator as IDisposable; disposable?.Dispose(); // 重要清理资源 }看到finally块里的Dispose了吗这说明一个实现了IDisposable的迭代器编译器为yield方法生成的迭代器就实现了是需要被清理的。这也是为什么绝对不要手动管理yield方法返回的迭代器生命周期而应该交给StartCoroutine或foreach循环的原因之一。2.2 yield关键字编译器魔法与状态机yield是C#为迭代器模式提供的神兵利器。它让开发者无需手动实现IEnumerator接口的那些繁琐方法就能轻松创建迭代器。它的工作原理是编译器魔法当你在一个返回类型为IEnumerator或IEnumerable的方法中使用yield return时C#编译器会为你自动生成一个实现了IEnumerator的隐藏类通常叫YourMethodNamed__X。这个生成类是一个状态机。yield return: 暂停当前方法的执行并返回一个值给迭代器的Current属性。同时状态机会保存当前方法的所有局部变量和执行位置状态。下次调用MoveNext(): 状态机从上次暂停的位置恢复执行局部变量也恢复到暂停时的值然后继续运行到下一个yield return或方法结束。举个例子IEnumerator CountToThree() { Debug.Log(开始计数); yield return null; // 状态1暂停等待一帧 Debug.Log(1); yield return null; // 状态2暂停等待一帧 Debug.Log(2); // 方法结束相当于隐含了 yield break; }编译器会生成一个包含state字段记录当前执行到哪一行和所有局部变量的类。每次MoveNext()被调用它都根据state跳转到对应的代码块执行。yield break: 用于立即终止迭代相当于在普通方法里执行return。当执行到yield break;时迭代器的MoveNext()会直接返回false。重要心得理解yield是状态机就能明白为什么在协程里修改一个局部变量下次恢复时它的值还在。这也是为什么避免在迭代器方法中修改传入的引用类型参数如List的结构增删元素因为恢复时迭代器可能依赖之前保存的状态这会导致不可预知的行为或InvalidOperationException集合已修改可能无法执行枚举操作。2.3 Unity的特别加持YieldInstruction继承链Unity在C#迭代器的基础上扩展了其用于协程等待的语义。这一切的核心是YieldInstruction这个抽象类以及它的众多子类。WaitForSeconds: 等待指定的秒数受Time.timeScale影响。WaitForSecondsRealtime: 等待指定的秒数不受Time.timeScale影响。WaitForEndOfFrame: 等待一帧结束在所有渲染和GUI显示之后。WaitForFixedUpdate: 等待下一个固定帧FixedUpdate。AsyncOperation: 如SceneManager.LoadSceneAsync的返回对象可用于等待异步操作完成。CustomYieldInstruction:这是一个关键类允许你创建自定义的等待条件。你只需要继承它并重写keepWaiting属性当属性返回false时协程继续执行。public class WaitUntilCustom : CustomYieldInstruction { private Funcbool predicate; public override bool keepWaiting !predicate(); // 当条件为true时停止等待 public WaitUntilCustom(Funcbool predicate) { this.predicate predicate; } } // 使用 yield return new WaitUntilCustom(() player.IsReady);Unity的协程管理器StartCoroutine在驱动这些迭代器时会检查Current返回的对象。如果它是YieldInstruction的子类管理器就会根据其类型进行相应的等待而不是简单地推进到下一帧。这极大地丰富了迭代器在游戏逻辑控制中的应用场景。3. 在Unity中实现迭代器模式的四种典型场景理解了原理我们来看实战。在Unity项目中根据不同的目的实现和使用迭代器模式有几种典型方式。3.1 场景一实现自定义集合的迭代器经典模式假设我们有一个自定义的关卡数据集合LevelCollection内部可能用数组、列表混合存储。我们希望外部能像用List一样方便地遍历它。using System.Collections; using System.Collections.Generic; using UnityEngine; public class LevelData { public string levelName; public int difficulty; } public class LevelCollection : IEnumerableLevelData { private ListLevelData tutorialLevels new ListLevelData(); private LevelData[] mainLevels new LevelData[10]; // ... 其他内部数据结构 // 实现GetEnumerator方法经典方式 public IEnumeratorLevelData GetEnumerator() { // 先遍历教程关卡 foreach (var level in tutorialLevels) { yield return level; } // 再遍历主线关卡跳过空引用 foreach (var level in mainLevels) { if (level ! null) yield return level; } // 还可以在这里加入其他逻辑比如按难度过滤 // if (level.difficulty currentPlayerLevel) yield return level; } // 显式实现非泛型接口为了兼容性 IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); // 只需调用泛型版本 } } // 使用 public class GameManager : MonoBehaviour { void Start() { LevelCollection levels new LevelCollection(); // 现在可以用foreach遍历了完全隐藏了内部复杂的存储结构 foreach (LevelData level in levels) { Debug.Log($关卡: {level.levelName}, 难度: {level.difficulty}); } } }这样做的好处GameManager完全不知道LevelCollection内部是List还是数组还是两者混合。如果未来我们把mainLevels从数组改成Dictionary只要GetEnumerator的实现能返回正确的序列所有遍历它的代码都无需修改。这就是迭代器模式“解耦”威力的直接体现。3.2 场景二利用yield构建游戏逻辑协程最常用这是Unity开发中迭代器模式应用最广泛的形式通常不直接暴露IEnumerator接口而是通过MonoBehaviour.StartCoroutine来驱动。public class EnemySpawner : MonoBehaviour { public GameObject enemyPrefab; public Transform[] spawnPoints; public float waveInterval 5f; public int enemiesPerWave 5; void Start() { // 启动一个管理整个波次生成流程的协程 StartCoroutine(SpawnWavesCoroutine()); } IEnumerator SpawnWavesCoroutine() { int waveCount 0; while (true) // 无限波次直到游戏结束 { waveCount; Debug.Log($第 {waveCount} 波敌人开始); // 使用嵌套迭代器协程来处理单波生成 yield return StartCoroutine(SpawnSingleWave(waveCount)); Debug.Log($第 {waveCount} 波结束等待下一波...); // 等待一段时间后开始下一波 yield return new WaitForSeconds(waveInterval); } } IEnumerator SpawnSingleWave(int waveNumber) { for (int i 0; i enemiesPerWave; i) { // 1. 随机选择一个出生点 Transform spawnPoint spawnPoints[Random.Range(0, spawnPoints.Length)]; // 2. 生成敌人 Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation); Debug.Log($生成敌人 {i1}/{enemiesPerWave} 于波次 {waveNumber}); // 3. 每隔0.5秒生成一个避免同一帧内全部生成造成卡顿 yield return new WaitForSeconds(0.5f); } } }关键点分析嵌套协程SpawnWavesCoroutine中通过yield return StartCoroutine(...)来等待一个子协程完成。这使得逻辑可以分层组织非常清晰。分帧执行SpawnSingleWave中的yield return new WaitForSeconds(0.5f)确保了敌人不是在同一帧内瞬间生成避免了性能尖峰也让游戏体验更自然。无限循环与外部控制SpawnWavesCoroutine有一个while(true)循环。停止它的方法通常是在类内部设置一个标志位或者在OnDestroy中调用StopAllCoroutines。3.3 场景三创建可组合的行为序列高级应用迭代器可以用来定义复杂的、可组合的AI行为或动画序列。每个行为都是一个返回IEnumerator的方法然后我们可以像搭积木一样组合它们。public class AdvancedAI : MonoBehaviour { public Transform patrolPointA; public Transform patrolPointB; public float moveSpeed 3f; public float waitTime 2f; void Start() { StartCoroutine(MainAILoop()); } IEnumerator MainAILoop() { while (true) { // 序列1巡逻 yield return StartCoroutine(PatrolSequence()); // 序列2播放警戒动画并等待 yield return StartCoroutine(PlayAlertAnimation()); // 序列3快速移动到随机点 yield return StartCoroutine(RushToRandomPoint()); // 然后循环... } } IEnumerator PatrolSequence() { yield return StartCoroutine(MoveToPoint(patrolPointA.position)); yield return new WaitForSeconds(waitTime); yield return StartCoroutine(MoveToPoint(patrolPointB.position)); yield return new WaitForSeconds(waitTime); } IEnumerator MoveToPoint(Vector3 target) { while (Vector3.Distance(transform.position, target) 0.1f) { Vector3 direction (target - transform.position).normalized; transform.position direction * moveSpeed * Time.deltaTime; yield return null; // 每帧移动一点直到到达目标 } } IEnumerator PlayAlertAnimation() { Debug.Log(AI发现异常); // 这里可以触发动画状态机参数 // animator.SetTrigger(Alert); yield return new WaitForSeconds(1.5f); } IEnumerator RushToRandomPoint() { Vector3 randomPoint new Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10)); float rushSpeed moveSpeed * 2f; while (Vector3.Distance(transform.position, randomPoint) 0.5f) { Vector3 direction (randomPoint - transform.position).normalized; transform.position direction * rushSpeed * Time.deltaTime; yield return null; } } }这种模式的强大之处在于可读性和可维护性。MainAILoop清晰地描述了AI的行为流程而每个具体行为如MoveToPoint都被封装和复用。你可以轻易地调整序列顺序或者插入新的行为模块。3.4 场景四实现异步加载与进度更新迭代器模式非常适合处理需要分步进行并报告进度的异步操作比如资源加载。public class ResourceLoader : MonoBehaviour { public Slider progressBar; public Text progressText; public IEnumerator LoadSceneWithProgress(string sceneName) { // 模拟加载多个资源 string[] assetNames { Prefabs/Enemy, Prefabs/Player, Textures/Background, Audio/Music }; progressBar.value 0; progressText.text 0%; for (int i 0; i assetNames.Length; i) { // 模拟异步加载一个资源实际项目中可能是AssetBundle.LoadAsync或Resources.LoadAsync Debug.Log($开始加载: {assetNames[i]}); yield return SimulateAssetLoad(assetNames[i]); // 更新进度 float progress (i 1f) / assetNames.Length; progressBar.value progress; progressText.text ${(int)(progress * 100)}%; // 每加载完一个资源可以稍微停顿一下让进度条平滑更新 yield return null; } Debug.Log(所有资源加载完成开始切换场景); // 实际切换场景 AsyncOperation asyncLoad SceneManager.LoadSceneAsync(sceneName); while (!asyncLoad.isDone) { // 将场景加载的进度映射到后20%的进度条上 progressBar.value 0.8f asyncLoad.progress * 0.2f; progressText.text ${(int)(progressBar.value * 100)}%; yield return null; } } IEnumerator SimulateAssetLoad(string assetName) { float loadTime Random.Range(0.3f, 1.2f); // 模拟随机加载时间 float elapsed 0f; while (elapsed loadTime) { elapsed Time.deltaTime; // 这里可以更新子进度条如果需要 yield return null; } Debug.Log($加载完成: {assetName}); } // 在UI按钮中调用 public void OnStartGameButtonClicked() { StartCoroutine(LoadSceneWithProgress(GameScene)); } }这个例子展示了如何利用迭代器的“暂停与恢复”特性将一个大任务加载所有资源切换场景分解成许多小步骤并在每一步更新UI反馈。用户体验会感觉加载过程是平滑、有响应的而不是卡住然后突然完成。4. 高级技巧、性能陷阱与最佳实践掌握了基本用法后要写出健壮、高效的迭代器代码还需要了解一些深水区的知识。4.1 迭代器的生命周期与内存管理这是新手最容易栽跟头的地方。由yield方法返回的迭代器对象其生命周期并不直观。创建开销每次调用一个yield方法如GetEnumerator()编译器生成的状态机类都会创建一个新的实例。这个开销很小但在性能关键的循环每帧调用中仍需注意。托管与释放协程迭代器由Unity的协程管理器托管。当你调用StartCoroutine(IEnumerator routine)Unity会管理这个迭代器的执行并在协程结束迭代器MoveNext()返回false后最终由垃圾回收器(GC)回收其内存。但是如果你手动new了一个自定义的IEnumerator并自己调用MoveNext()你需要负责它的生命周期。提前终止与资源泄漏IEnumerator LeakyCoroutine() { SomeExpensiveResource resource new SomeExpensiveResource(); yield return new WaitForSeconds(10); resource.Cleanup(); // 如果协程被提前Stop这行不会执行 }解决方案使用try...finally块确保资源清理。IEnumerator SafeCoroutine() { SomeExpensiveResource resource new SomeExpensiveResource(); try { yield return new WaitForSeconds(10); // ... 使用resource } finally { resource.Cleanup(); // 无论协程是正常结束还是被Stopfinally块都会执行 } }注意StopCoroutine或DestroyGameObject会导致协程停止yield return之后的代码不会执行但finally块会。4.2 协程Coroutine不是线程必须反复强调Unity的协程是运行在主线程上的。yield return null只是把执行权交还给Unity引擎让它在下一帧再回来继续执行这个迭代器。它不会创建新的线程。这意味着可以在协程里安全地访问和修改Unity对象GameObject,Transform,Renderer等因为都在主线程。在协程里执行耗时计算如复杂数学运算、大循环依然会阻塞主线程导致游戏卡顿。yield return只在你主动调用它时才会暂停。如何避免卡顿将耗时任务拆分。IEnumerator ProcessLargeDataSet(ListData dataSet) { for (int i 0; i dataSet.Count; i) { PerformExpensiveCalculation(dataSet[i]); // 耗时操作 // 每处理10个元素让出一帧保持游戏流畅 if (i % 10 9) { yield return null; } } }4.3 使用CustomYieldInstruction实现复杂等待条件Unity内置的等待指令有限CustomYieldInstruction给了我们无限可能。案例等待网络请求public class WaitForWebRequest : CustomYieldInstruction { private UnityWebRequest request; public override bool keepWaiting !request.isDone; // 请求未完成就继续等待 public UnityWebRequest Result request; public WaitForWebRequest(UnityWebRequest req) { request req; req.SendWebRequest(); // 通常在这里启动请求 } } // 在协程中使用 IEnumerator DownloadData(string url) { using (UnityWebRequest request UnityWebRequest.Get(url)) { yield return new WaitForWebRequest(request); if (request.result UnityWebRequest.Result.Success) { string data request.downloadHandler.text; Debug.Log(下载完成: data); } else { Debug.LogError(下载失败: request.error); } } }案例等待多个条件同时满足public class WaitForAll : CustomYieldInstruction { private ListCustomYieldInstruction instructions; public override bool keepWaiting { get { foreach (var instr in instructions) { if (instr.keepWaiting) return true; // 只要有一个还在等待就继续等待 } return false; // 全部完成 } } public WaitForAll(params CustomYieldInstruction[] instructions) { this.instructions new ListCustomYieldInstruction(instructions); } } // 使用等待3个不同的条件 IEnumerator WaitForMultipleEvents() { var waitForPlayer new WaitUntil(() player ! null player.IsReady); var waitForTime new WaitForSeconds(5f); var waitForInput new WaitUntil(() Input.GetKeyDown(KeyCode.Space)); yield return new WaitForAll(waitForPlayer, waitForTime, waitForInput); Debug.Log(所有条件都已满足); }4.4 迭代器与Unity生命周期事件的协同迭代器协程的执行与MonoBehaviour的生命周期紧密相关。Start与协程启动在Start中启动协程是安全的因为此时所有组件都已初始化。OnEnable/OnDisable如果GameObject被禁用SetActive(false)其上的所有协程都会自动暂停。当再次启用时它们会从上次yield的地方继续执行。这一点非常有用可以用来实现暂停/恢复逻辑。OnDestroy当GameObject被销毁时其上运行的所有协程都会自动停止。这意味着yield return之后的代码将不会被执行。这就是为什么重要的清理工作如网络连接断开、文件流关闭必须放在try...finally或OnDestroy方法中。时间缩放Time.timeScaleWaitForSeconds受Time.timeScale影响。如果你需要不受游戏暂停影响的计时比如UI动画请使用WaitForSecondsRealtime。一个常见的坑在场景切换时DontDestroyOnLoad的游戏对象上的协程会继续运行。而非DontDestroyOnLoad的对象会被销毁其协程也随之停止。需要仔细设计跨场景的异步逻辑。5. 实战构建一个基于迭代器的简易状态机最后我们用一个综合案例展示如何用迭代器模式构建一个轻量级、易读的状态机常用于管理角色状态、UI流程或游戏阶段。using System.Collections; using System.Collections.Generic; using UnityEngine; public enum GameState { Menu, Playing, Paused, GameOver } public class GameStateMachine : MonoBehaviour { private IEnumerator currentStateCoroutine; public GameState CurrentState { get; private set; } void Start() { // 初始状态为菜单 ChangeState(GameState.Menu); } void Update() { // 状态机驱动也可以放在Update里根据条件切换状态 // 本例采用事件驱动通过ChangeState方法切换 } public void ChangeState(GameState newState) { if (currentStateCoroutine ! null) { StopCoroutine(currentStateCoroutine); } CurrentState newState; switch (newState) { case GameState.Menu: currentStateCoroutine MenuState(); break; case GameState.Playing: currentStateCoroutine PlayingState(); break; case GameState.Paused: currentStateCoroutine PausedState(); break; case GameState.GameOver: currentStateCoroutine GameOverState(); break; } StartCoroutine(currentStateCoroutine); } IEnumerator MenuState() { Debug.Log(进入菜单状态); UIManager.Instance.ShowMenuPanel(); // 等待“开始游戏”按钮点击事件 bool startGameClicked false; UIManager.Instance.OnStartGameButtonClicked () startGameClicked true; while (!startGameClicked) { // 可以在这里处理菜单动画、音乐等 yield return null; } Debug.Log(退出菜单状态); UIManager.Instance.OnStartGameButtonClicked - () startGameClicked true; ChangeState(GameState.Playing); } IEnumerator PlayingState() { Debug.Log(进入游戏状态); UIManager.Instance.ShowGamePanel(); Time.timeScale 1f; // 恢复游戏时间 // 游戏主循环 while (CurrentState GameState.Playing) // 用状态标识作为循环条件 { // 检查游戏结束条件 if (PlayerHealth.Instance.CurrentHealth 0) { ChangeState(GameState.GameOver); yield break; // 立即退出当前状态协程 } // 检查暂停输入 if (Input.GetKeyDown(KeyCode.Escape)) { ChangeState(GameState.Paused); yield break; } // 在这里执行每帧的游戏逻辑更新... // SpawnSystem.Update(); // ScoreManager.Update(); yield return null; // 等待下一帧 } } IEnumerator PausedState() { Debug.Log(进入暂停状态); Time.timeScale 0f; // 暂停游戏时间 UIManager.Instance.ShowPausePanel(); bool resumeRequested false; bool backToMenuRequested false; UIManager.Instance.OnResumeButtonClicked () resumeRequested true; UIManager.Instance.OnBackToMenuButtonClicked () backToMenuRequested true; while (true) { if (resumeRequested) { ChangeState(GameState.Playing); yield break; } if (backToMenuRequested) { ChangeState(GameState.Menu); yield break; } yield return null; } } IEnumerator GameOverState() { Debug.Log(进入游戏结束状态); Time.timeScale 0f; // 或保持为1播放慢动作 UIManager.Instance.ShowGameOverPanel(); // 等待3秒后自动返回菜单或者等待玩家点击 float timer 3f; bool anyKeyPressed false; while (timer 0 !anyKeyPressed) { timer - Time.unscaledDeltaTime; // 使用不受timeScale影响的时间 if (Input.anyKeyDown) anyKeyPressed true; yield return null; } // 清理游戏现场重置数据... GameManager.Instance.ResetGame(); ChangeState(GameState.Menu); } }这个状态机实现的优点清晰直观每个状态都是一个独立的方法状态内的逻辑集中易于理解和维护。天然支持异步状态内可以轻松使用yield return等待时间、输入或异步操作。易于扩展添加新状态只需增加一个IEnumerator方法并在ChangeState中调用。生命周期管理通过StopCoroutine和StartCoroutine确保同一时间只有一个活跃状态。注意事项状态切换时要记得清理上一个状态订阅的事件避免内存泄漏本例中在状态结束时取消了事件订阅。确保状态退出条件明确避免协程无法结束。对于非常简单的状态机可能有点“杀鸡用牛刀”。但对于有复杂异步逻辑的状态如需要播放一段动画、等待网络响应再切换这种模式非常强大。迭代器模式在Unity中远不止是遍历集合。它是协程的基石是管理复杂异步流程、创建可读行为序列、实现轻量级状态机的瑞士军刀。从理解IEnumerator和yield的状态机本质开始到熟练运用CustomYieldInstruction解决各种等待问题再到规避生命周期和性能上的陷阱这条路需要不断的实践和踩坑。但一旦掌握你编写Unity代码的思维方式和能力都会上一个台阶能够更优雅地处理那些需要“等待”和“顺序执行”的游戏逻辑让代码不仅跑起来而且跑得清晰、健壮。