U
19 / 22 · 10 分钟
常见问题

协程:不用 Update 也能随时间做一件事

简短回答

怎么让一件事随时间逐渐发生,或者先等待再执行?

协程是一个可以暂停的方法:yield return new WaitForSeconds(1f) 等一秒,然后从同一行继续往下跑。用 StartCoroutine 启动它,如果将来需要停止就保留它的句柄;并记住:运行该协程的 GameObject 被禁用或销毁时,协程会停下。

Update 每帧跑一次就结束了,所以任何需要时间的事情,都得用能跨帧活着的变量重新搭起来 —— 一个计时器、一个标志、一个阶段。coroutine 是把同样的逻辑写成一整条直线,并且允许它停在中间。

同一件事,两种写法

Flash red for a moment, then go back
// 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 并不能让它停下来。

你能等什么

The four that cover almost everything
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

随时间把东西移动

The pattern behind every fade, slide and pop
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 课。