协程:不用 Update 也能随时间做一件事
怎么让一件事随时间逐渐发生,或者先等待再执行?
协程是一个可以暂停的方法:yield return new WaitForSeconds(1f) 等一秒,然后从同一行继续往下跑。用 StartCoroutine 启动它,如果将来需要停止就保留它的句柄;并记住:运行该协程的 GameObject 被禁用或销毁时,协程会停下。
Update 每帧跑一次就结束了,所以任何需要时间的事情,都得用能跨帧活着的变量重新搭起来 —— 一个计时器、一个标志、一个阶段。coroutine 是把同样的逻辑写成一整条直线,并且允许它停在中间。
同一件事,两种写法
// With Update: three fields and a branch, and the logic is scattered.
bool flashing;
float flashEnds;
void Update()
{
if (flashing && Time.time >= flashEnds)
{
mat.color = Color.white;
flashing = false;
}
}
void Hit()
{
mat.color = Color.red;
flashing = true;
flashEnds = Time.time + 0.1f;
}
// With a coroutine: the whole story reads top to bottom, in one place.
IEnumerator Flash()
{
mat.color = Color.red;
yield return new WaitForSeconds(0.1f);
mat.color = Color.white;
}
void Hit() => StartCoroutine(Flash());规则
这个方法返回 IEnumerator,并用 yield return 暂停。正是 yield 让它成为 coroutine,而不是一个普通方法。
光调用它什么都不做。单独写 Flash() 只是造出一个 coroutine 然后把它扔掉;真正让它跑起来的是 StartCoroutine(Flash())。
它会在暂停时所在的那个帧内位置恢复 —— 在 Update 之后、LateUpdate 之前 —— 所以它不是线程,也没有任何东西在并行运行。
它属于启动它的那个 MonoBehaviour。把那个 GameObject 关掉或者销毁,coroutine 就说到一半停在那里,不会跑完。只禁用 component 并不能让它停下来。
你能等什么
yield return null; // one frame
yield return new WaitForSeconds(2f);
yield return new WaitForSecondsRealtime(2f); // two seconds of wall time
yield return new WaitUntil(() => player.isAlive);
yield return StartCoroutine(Other()); // run Other, wait for it to finish随时间把东西移动
IEnumerator MoveTo(Vector3 to, float seconds)
{
Vector3 from = transform.position;
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime / seconds;
// SmoothStep instead of a plain Lerp: starts and ends gently.
transform.position = Vector3.Lerp(from, to, Mathf.SmoothStep(0f, 1f, t));
yield return null;
}
// Land exactly on the target: the loop leaves t slightly over 1.
transform.position = to;
}停掉一个
StopCoroutine("Flash") 这种按名字停的写法,只有你当初也是按名字启动它时才有效,而且那个字符串编译器从来不会检查。应该改成把 handle 存下来。
Coroutine c = StartCoroutine(Flash()); 然后 StopCoroutine(c)。这样写,以后改名也照样能用。
同一个 coroutine 启动两次就会跑两份。做闪光效果时先把旧的 handle 停掉,否则两次闪光叠在一起会留下写错的颜色。
什么时候别用 coroutine
永远每帧都要跑的东西属于 Update。一个 while (true) 循环配 yield return null 的 coroutine,只是绕了远路的 Update。
凡是要碰物理的东西都属于 FixedUpdate,或者属于一个 yield WaitForFixedUpdate 的 coroutine。普通 coroutine 是按帧的时钟恢复,不是按物理的时钟。
几百个 coroutine 同时跑会实实在在吃内存:每个都要分配。如果是几百个一模一样的计时器,用一个 Update 遍历一个列表要便宜得多。
做出这个的那一课
带冷却的技能系统
这篇指南单独成篇,是一份可以直接照着做的配方。在课程里,同样的东西会作为贯穿全部五个章的那个项目的一部分来搭建 —— 角色与战斗,第 04 课。