2.3 协程:可以暂停的值班任务 本节摘要:协程(Coroutine)是 Unity 里"做一半歇一下"的任务机制:用 yield 把执行权交还引擎,到约定时点再接着跑。本节讲透它的执行模型、五种常用 yield、停止与销毁的关系,并给出三个生产可用的协程模板。 会泡面的长任务 想象值班员接到一个任务:"等三秒,刷一波敌人;再等两秒,放一段过场动画;动画结束,开闸。"如果写进 Update,就得自己维护一堆计时器和状态标志,代码立刻面条化。协程提供了另一种写法:任务像一份流水账,做到哪一步想等,就写一行 yield,把值班台让出来,引擎到点后从断点续跑。 它的底层并没有魔法:调用 StartCoroutine 时,引擎拿到一个迭代器,把它登记进本帧的任务清单;
本节摘要:协程(Coroutine)是 Unity 里"做一半歇一下"的任务机制:用 yield 把执行权交还引擎,到约定时点再接着跑。本节讲透它的执行模型、五种常用 yield、停止与销毁的关系,并给出三个生产可用的协程模板。
想象值班员接到一个任务:"等三秒,刷一波敌人;再等两秒,放一段过场动画;动画结束,开闸。"如果写进 Update,就得自己维护一堆计时器和状态标志,代码立刻面条化。协程提供了另一种写法:任务像一份流水账,做到哪一步想等,就写一行 yield,把值班台让出来,引擎到点后从断点续跑。
它的底层并没有魔法:调用 StartCoroutine 时,引擎拿到一个迭代器,把它登记进本帧的任务清单;执行到 yield 就暂停并记住位置;之后的每一帧按 yield 约定的条件检查是否恢复。所以协程运行在主线程上,和 Update 共享同一条值班线——它不是多线程,不能拿去做重计算,那是第 5 章异步加载与 Job System 的领地。
背景:Boss 战开场需要一套节奏动作:画面渐暗、三秒倒计时、刷新 Boss、画面渐亮。四个步骤横跨多帧且互相依赖,正适合写成一段协程。
操作:新建 BossIntro 脚本,覆盖 Canvas 的 Image(命名为 fade)做黑场,核心代码:
using UnityEngine; using System.Collections; public class BossIntro : MonoBehaviour { public CanvasGroup fade; // 全屏黑场,Alpha 0 到 1 public float fadeTime = 1f; void Start() { StartCoroutine(IntroSequence()); } IEnumerator IntroSequence() { // 模板一:逐帧渐变(Lean 一点的通用写法) for (float t = 0f; t < fadeTime; t += Time.deltaTime) { fade.alpha = t / fadeTime; yield return null; } fade.alpha = 1f; // 模板二:定时等待 yield return new WaitForSeconds(3f); Debug.Log("倒计时结束,刷新 Boss"); // 模板三:等另一个协程跑完再继续 yield return StartCoroutine(FadeIn()); Debug.Log("开场演出完毕,交还操作权"); } IEnumerator FadeIn() { for (float t = fadeTime; t > 0f; t -= Time.deltaTime) { fade.alpha = 1f - t / fadeTime; yield return null; } fade.alpha = 0f; } }
结果:运行后画面在 1 秒内淡入黑场,停 3 秒,Console 打出刷 Boss 日志,再用 1 秒淡出恢复,最后打印演出完毕。全过程主线程不卡顿,玩家能看到中间每一帧的过渡。
解读:三个模板各司其职。渐变循环用 t 加 Time.deltaTime 累加进度,比 WaitForSeconds 每帧插值更可控;WaitForSeconds 表达"等多久",最简洁但要警惕它受时间缩放影响——暂停菜单把 Time.timeScale 设为 0 时它会无限等待,需要不受暂停影响的计时就用 WaitForSecondsRealtime。嵌套 yield return StartCoroutine 让流水账可以拆函数而不乱时序。还有一条纪律:协程方法体里的变量跨帧存活,是天然的临时状态仓,但别把大量对象塞进去赖着不放,那等于手动制造内存滞留。
变式:把渐变循环里的 Time.deltaTime 换成 unscaledDeltaTime,再在运行中把 timeScale 调成 0.2,观察黑场速度几乎不变——慢动作演出不慢 UI,就是这个写法。
协程有两种死法,搞混会出诡异 bug。其一,StopCoroutine 或 StopAllCoroutines 主动叫停,只影响调用者身上的协程;其二,宿主对象被禁用或销毁,其上所有协程立即终止且不会恢复——SetActive(false) 就足以杀死协程,这是"我的倒计时怎么突然不动了"的头号原因。想做一个不随场景对象生死的长任务,正确做法是把协程挂在常驻的调度对象上(第 1 章讲过 DontDestroyOnLoad 的总调度工位)。
还有一个容易踩的坑:对同一逻辑重复 StartCoroutine 会叠出多份并行实例。防御写法是先停再启,或用协程句柄判重:
Coroutine handle; void TogglePulse() { // 先停旧的再开新的,保证同一时间只有一个实例在跑 if (handle != null) StopCoroutine(handle); handle = StartCoroutine(Pulse()); }
协程适合"节奏与流程":演出时序、分帧加载、延迟触发的补偿逻辑。它不适合三类场景:高频每帧的状态更新(那是 Update 与状态机的活)、后台重计算(主线程跑了照样卡帧)、需要跨对象广播完成通知的流程(用事件更干净,见下一节)。判断标准一句话:这件事是否天然是"一段带等待的流水账",是就用协程,不是就别硬套。
一个值班员总得跟别人说话。下一节的委托与事件,就是值班室的对讲机。