Unity协程实战:从yield return到WaitUntil的7个高频使用场景解析
在Unity游戏开发中,协程(Coroutine)是实现异步逻辑的重要工具。不同于传统的同步代码执行方式,协程允许我们将任务分解为多个步骤,并在特定条件下暂停和恢复执行。这种特性使得协程特别适合处理需要等待的游戏逻辑,如技能冷却、资源加载、NPC行为等场景。
本文将深入解析Unity协程在实际开发中的7个高频使用场景,帮助开发者理解不同Yield Instruction的适用性,并掌握如何根据具体需求选择合适的等待指令。我们将从基础概念出发,逐步深入到复杂应用场景,提供可直接复用的代码示例和最佳实践建议。
1. 角色技能冷却系统实现
技能冷却(Cooldown)是游戏开发中最常见的协程应用场景之一。通过协程,我们可以优雅地实现技能使用后的等待时间,而不阻塞主线程的执行。
1.1 使用WaitForSeconds实现基础冷却
最简单的技能冷却实现方式是使用WaitForSeconds:
IEnumerator SkillCooldown(float cooldownTime) { isSkillReady = false; skillButton.interactable = false; yield return new WaitForSeconds(cooldownTime); isSkillReady = true; skillButton.interactable = true; Debug.Log("技能已冷却完毕!"); }关键点说明:
WaitForSeconds会暂停协程执行指定的时间(以秒为单位)- 冷却期间可以更新UI显示剩余时间
- 适用于大多数简单的技能冷却需求
1.2 冷却时间显示优化
为了提升玩家体验,我们通常需要在UI上显示冷却剩余时间:
IEnumerator SkillCooldownWithUI(float cooldownTime) { isSkillReady = false; skillButton.interactable = false; float remainingTime = cooldownTime; while (remainingTime > 0) { cooldownText.text = Mathf.Ceil(remainingTime).ToString(); remainingTime -= Time.deltaTime; yield return null; // 每帧执行一次 } isSkillReady = true; skillButton.interactable = true; cooldownText.text = "就绪"; }注意:使用
yield return null可以让协程每帧执行一次,适合需要频繁更新的逻辑。
2. 关卡加载与进度显示
关卡加载是另一个协程大显身手的场景,特别是当我们需要显示加载进度时。
2.1 基础场景加载实现
IEnumerator LoadLevelAsync(string sceneName) { AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); asyncLoad.allowSceneActivation = false; while (!asyncLoad.isDone) { float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f); loadingSlider.value = progress; loadingText.text = $"加载中... {progress * 100}%"; if (progress >= 0.9f) { loadingText.text = "按任意键继续"; if (Input.anyKeyDown) { asyncLoad.allowSceneActivation = true; } } yield return null; } }实现要点:
AsyncOperation.progress范围是0-0.9,需要手动归一化到0-1- 通过
allowSceneActivation控制场景切换时机 - 每帧更新进度条和文本显示
2.2 多资源并行加载优化
对于需要加载大量资源的场景,可以使用多个协程并行执行:
IEnumerator LoadMultipleResources() { List<Coroutine> loadCoroutines = new List<Coroutine>(); // 启动多个资源加载协程 loadCoroutines.Add(StartCoroutine(LoadTextures())); loadCoroutines.Add(StartCoroutine(LoadModels())); loadCoroutines.Add(StartCoroutine(LoadAudio())); // 等待所有协程完成 foreach (var coroutine in loadCoroutines) { yield return coroutine; } Debug.Log("所有资源加载完成!"); }3. NPC行为树与状态切换
协程非常适合实现NPC的复杂行为逻辑,特别是需要按顺序执行多个动作的场景。
3.1 基础巡逻行为实现
IEnumerator NPCPatrolBehaviour() { while (true) { // 移动到下一个巡逻点 yield return StartCoroutine(MoveToPosition(patrolPoints[currentPatrolIndex])); // 在巡逻点停留一段时间 yield return new WaitForSeconds(waitTimeAtPoint); // 选择下一个巡逻点 currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; } } IEnumerator MoveToPosition(Vector3 target) { while (Vector3.Distance(transform.position, target) > 0.1f) { transform.position = Vector3.MoveTowards( transform.position, target, moveSpeed * Time.deltaTime ); yield return null; } }3.2 使用WaitUntil实现条件触发
当NPC需要等待特定条件满足时才执行下一步时,WaitUntil非常有用:
IEnumerator NPCReactionBehaviour() { Debug.Log("NPC正在观察玩家..."); // 等待玩家进入警戒范围 yield return new WaitUntil(() => IsPlayerInRange()); Debug.Log("发现玩家!"); yield return StartCoroutine(ChasePlayer()); // 等待玩家离开视线 yield return new WaitWhile(() => CanSeePlayer()); Debug.Log("玩家已逃离,返回巡逻"); yield return StartCoroutine(ReturnToPatrol()); }4. 游戏流程控制与过场动画
协程可以优雅地控制游戏流程,特别是需要按顺序播放多个动画或事件的场景。
4.1 过场动画序列控制
IEnumerator PlayCutscene() { // 播放开场动画 yield return StartCoroutine(PlayOpeningAnimation()); // 等待玩家按下确认键 yield return new WaitUntil(() => Input.GetButtonDown("Submit")); // 播放剧情对话 yield return StartCoroutine(PlayDialogueSequence()); // 等待2秒后进入游戏 yield return new WaitForSeconds(2f); StartGame(); }4.2 多阶段Boss战实现
IEnumerator BossBattleSequence() { // 第一阶段 yield return StartCoroutine(BossPhaseOne()); // 转场动画 yield return StartCoroutine(PlayPhaseTransition()); // 第二阶段 yield return StartCoroutine(BossPhaseTwo()); // 等待Boss血量归零 yield return new WaitUntil(() => bossHealth <= 0); // 播放死亡动画 yield return StartCoroutine(PlayBossDeath()); }5. UI动画与交互反馈
协程在UI动画和交互反馈方面也有广泛应用,可以实现平滑的过渡效果。
5.1 渐入渐出效果
IEnumerator FadeUI(CanvasGroup group, float targetAlpha, float duration) { float startAlpha = group.alpha; float elapsed = 0f; while (elapsed < duration) { group.alpha = Mathf.Lerp(startAlpha, targetAlpha, elapsed / duration); elapsed += Time.deltaTime; yield return null; } group.alpha = targetAlpha; }5.2 按钮点击效果序列
IEnumerator ButtonPressEffect() { // 缩小动画 float scale = 1f; while (scale > 0.8f) { scale -= Time.deltaTime * 2f; transform.localScale = Vector3.one * scale; yield return null; } // 放大动画 while (scale < 1f) { scale += Time.deltaTime * 2f; transform.localScale = Vector3.one * scale; yield return null; } transform.localScale = Vector3.one; }6. 网络请求与数据加载
协程可以很好地处理异步网络请求,避免阻塞主线程。
6.1 基础数据请求
IEnumerator LoadPlayerData(string playerId) { string url = $"https://api.game.com/players/{playerId}"; UnityWebRequest request = UnityWebRequest.Get(url); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { PlayerData data = JsonUtility.FromJson<PlayerData>(request.downloadHandler.text); UpdatePlayerUI(data); } else { Debug.LogError($"加载失败: {request.error}"); } }6.2 带超时的网络请求
IEnumerator LoadWithTimeout(string url, float timeout) { UnityWebRequest request = UnityWebRequest.Get(url); request.SendWebRequest(); float elapsed = 0f; while (!request.isDone && elapsed < timeout) { elapsed += Time.deltaTime; yield return null; } if (request.isDone) { // 处理成功响应 } else { request.Abort(); Debug.LogError("请求超时"); } }7. 自定义Yield Instruction实现
当内置的Yield Instruction不能满足需求时,我们可以创建自定义实现。
7.1 等待动画播放完成
public class WaitForAnimation : IEnumerator { private Animator animator; private string stateName; private int layerIndex; public WaitForAnimation(Animator animator, string stateName, int layerIndex = 0) { this.animator = animator; this.stateName = stateName; this.layerIndex = layerIndex; } public object Current => null; public bool MoveNext() { var stateInfo = animator.GetCurrentAnimatorStateInfo(layerIndex); return animator.IsInTransition(layerIndex) || !stateInfo.IsName(stateName) || stateInfo.normalizedTime < 1f; } public void Reset() {} } // 使用示例 IEnumerator PlayAnimationAndWait() { animator.Play("Attack"); yield return new WaitForAnimation(animator, "Attack"); Debug.Log("动画播放完成"); }7.2 等待物理碰撞发生
public class WaitForCollision : IEnumerator { private bool collisionOccurred = false; public WaitForCollision(GameObject target) { var listener = target.AddComponent<CollisionListener>(); listener.OnCollision += () => collisionOccurred = true; } public object Current => null; public bool MoveNext() { return !collisionOccurred; } public void Reset() {} } // 使用示例 IEnumerator WaitForPlayerCollision() { yield return new WaitForCollision(playerObject); Debug.Log("玩家发生了碰撞"); }在实际项目中,我发现合理使用协程可以大幅简化异步逻辑的实现。特别是在处理复杂的游戏流程时,协程能够将分散的逻辑组织成线性的代码结构,提高可读性和可维护性。对于性能敏感的场景,需要注意避免创建过多的协程实例,可以考虑使用对象池技术来重用协程。